[Salesforce] カスタムREST APIをApexで作成する

# Apex REST Web サービス
Apexを使用してカスタムREST APIを公開することができます。

カスタムのREST APIのコーディングについて扱います。
REST APIの定義書については[こちら](https://qiita.com/TaaaZyyy/items/c7464dbab19c710a0709)。
外部システムからのREST APIの呼び出しについては[こちら](https://qiita.com/TaaaZyyy/items/7feb7d03cb1f5248d2e9)。

## REST Web サービスのApexクラスを作成する
- クラスは`global`にする必要があります。
- URLはクラス単位で指定します。
- クラスに`@RestResource(urlMapping='/hoge')`を追加する必要があります。
    - 指定できるURLは`https://instance.salesforce.com/services/apexrest/`以降になります。
        - 管理パッケージに含まれるリソースの場合は`https://instance.salesforce.com/services/apexrest/【packageNamespace】/`以降になります。
    - URLは大文字、小文字が区別されます。
    - URLパスは`/`から始める必要があります。
    - ワイルドカード`*`を使用できます。ワイルドカードの前後には`/`が必要です(ただし末尾の`/`は不要)。
    - 常に完全一致が優先されます。完全一致がない場合、ワイルドカードを使用して一致するすべてのパターンを検索し、そのうち文字列の長さが最も長いものが選択されます。

## Apex REST メソッドを作成する
- メソッドは`global static`にする必要があります。
- メソッドにアノテーション`@HttpGet`、`@HttpPost`、`@HttpPut`、`@HttpPatch`、`@HttpDelete`をつけることで、RESTリソースとしてメソッド(Apex REST メソッド)を公開できます。それぞれ、GET、POST、PUT、PATCH、DELETEのHTTPメソッドに対応します。
- 一つのクラスでアノテーション`@HttpGet`、`@HttpPost`、`@HttpPut`、`@HttpPatch`、`@HttpDelete`を重複して使用することはできません。

## リクエストを処理する
### リクエストデータの取得
リクエストデータは`RestContext.request`で取得できます。

```java:ApexRestメソッド
global static void doPost() {
    RestRequest  req = RestContext.request;
    System.debug(LoggingLevel.DEBUG, '===== request =====');
    System.debug(LoggingLevel.DEBUG, '▼ PATH');
    System.debug(LoggingLevel.DEBUG, req.resourcePath);
    System.debug(LoggingLevel.DEBUG, '▼ URI');
    System.debug(LoggingLevel.DEBUG, req.requestURI);
    System.debug(LoggingLevel.DEBUG, '▼ METHOD');
    System.debug(LoggingLevel.DEBUG, req.httpMethod);
    System.debug(LoggingLevel.DEBUG, '▼ HEADER');
    System.debug(LoggingLevel.DEBUG, req.headers);
    System.debug(LoggingLevel.DEBUG, '▼ PARAM');
    System.debug(LoggingLevel.DEBUG, req.params);

    // ===== request =====
    // ▼ PATH
    // /services/apexrest/test/*
    // ▼ URI
    // /test
    // ▼ METHOD
    // POST
    // ▼ HEADER
    // {Accept=*/*, Accept-Encoding=gzip, deflate, br, ...}
    // ▼ PARAM
    // {p=hoge}
}
```

### リクエストボディの受け取り方
リクエストボディの受け取り方は下記の2つがあります。なお、`@HttpGet`と`@HttpDelete`はリクエストボディは使用できません。

1. 引数への並列化を利用
2. `RestContext.request.requestBody`から取得する

#### 1. 引数への並列化を利用
* リクエストボディがJSONまたはXMLの場合、Apex REST メソッドに引数を定義すると、引数に並列化されます。
    * レスポンスヘッダに`Content-Type: application/json`または、`Content-Type: application/xml`が設定されている必要があります。
    * 対応していない`Content-Type`を指定するとエラーになります。
        * `[{"errorCode": "UNSUPPORTED_MEDIA_TYPE","message": "Content-Type header specified in HTTP request is not supported: text/plain"}]`
* 引数を定義すると、`RestContext.request.requestBody`の値はnullになります。
* 逐次化は下記のオブジェクトが対象となります。
    * Apexプリミティブ型 (SObject、Blobは除く)
    * SObjectまたはApexプリミティブ型のList
    * SObjectまたはApexプリミティブ型のMap(キーはStringのみ可能)
    * 上記の型のメンバ変数を含むユーザ定義型
        * クラスはglobalで宣言されている必要があります。
        * public、private、globalのメンバ変数に並列化します。
        * static、transientの変数には並列化されません。
* `List<List<String>>` など、コレクションのList、コレクションのMapはXMLでは使用できません。
* Apex REST メソッドで定義していないパラメータをリクエストボディに含めるとエラーが返却されます。
    * `[{"errorCode": "JSON_PARSER_ERROR","message": "Unexpected parameter encountered during deserialization: productName at [line:1, column:61]"}]`
    * `[{"errorCode": "XML_PARSER_ERROR","message": "Unexpected parameter encountered during deserialization: productName"}]`
* Apex REST メソッドで定義しているパラメータをリクエストボディに含めなかった場合、エラーにはならず、値はnullになります。

##### 引数への並列化を利用 ①引数にApexプリミティブ型を設定した例
```java:ApexRestメソッド
global static String postConsuming(String productCode, Integer consumedNumber) {
    return productCode + 'の商品を' + consumedNumber + '個消費しました。';
    // "ETA6497-770の商品を5個消費しました。"
}
```

```json:リクエストボディ(JSON)
{"productCode":"ETA6497-770","consumedNumber":5}
```

```xml:リクエストボディ(XML)
<request>
  <productCode>ETA6497-770</productCode>
  <consumedNumber>5</consumedNumber>
</request>
```

##### 引数への並列化を利用 ②引数にユーザ定義型を設定した例
```java:ApexRestメソッド
global static String postConsuming(Consuming consumingData) {
    return consumingData.getProductCode() + 'の商品を' + consumingData.getConsumedNumber() + '個消費しました。';
    // "ETA6497-770の商品を5個消費しました。"
}

global class Consuming {
    private String productCode;
    private Integer consumedNumber;

    private String getProductCode() {
        return this.productCode;
    }

    private Integer getConsumedNumber() {
        return this.consumedNumber;
    }
}
```

```json:リクエストボディ(JSON)
{"consumingData": {"productCode":"ETA6497-770","consumedNumber":5}}
```

```xml:リクエストボディ(XML)
<request>
  <consumingData>
    <productCode>ETA6497-770</productCode>
    <consumedNumber>5</consumedNumber>
  </consumingData>
</request>
```

#### 2. `RestContext.request.requestBody`から取得する
引数を設定しなかった場合、`RestContext.request.requestBody`でリクエストボディを取得できます。`requestBody`はBlob型です。

```java:ApexRestメソッド
@HttpPatch
global static void insertContact() {
    RestRequest req = RestContext.request;
    // req.requestBody はBlob型なのでStringに変換。
    String reqString = req.requestBody.toString();
    // JSONを並列化
    Map<String, Object> reqJson = JSON.deserializeUntyped(reqString);
    String name = (String) reqJson.get('name');
    String customerNo = (String) reqJson.get('customerNo');
}
```

## レスポンスを返す
### レスポンスデータの設定
レスポンスデータは`RestContext.response`で設定できます。
設定できる値はステータスコードとヘッダーとボディーの3つです。
ステータスコードに設定可能な値は[こちら](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_methods_system_restresponse.htm?search_text=RestResponse)。

```java:ApexRestメソッド
@HttpGet
global static void getProducts() {
    RestResponse res = RestContext.response;
    List<Product2> products = [SELECT ProductCode, Name FROM Product2];

    res.statusCode = 200;
    res.addHeader('Content-Type', 'application/json;charset=UTF-8');
    res.responseBody = Blob.valueOf(JSON.serialize(products));
}
```

### レスポンスボディの設定
レスポンスボディの設定方法は下記の2つがあります。

1. 戻り値の逐次化を利用
2. `RestContext.response.responseBody`に設定する

#### 1. 戻り値の逐次化を利用
* Apex REST メソッドの戻り値が`void`以外の場合、戻り値がレスポンスボディに逐次化されます。
* リクエストヘッダーに`Accept:application/xml`が含まれる場合、レスポンスボディがXML形式になります。指定がない場合、もしくは`Accept:application/json`が指定されている場合、レスポンスボディがJSON形式となります。レスポンスヘッダーの値はレスポンスボディの形式に影響しません。
    * 対応していない`Accept`の値を指定するとエラーになります。
        * `[{"errorCode": "NOT_ACCEPTABLE", "message": "Accept header specified in HTTP request is not supported: text/plain"}]`
* 戻り値には下記の方を使用できます。
    * Apexプリミティブ型 (SObject、Blobは除く)
    * SObjectまたはApexプリミティブ型のList
    * SObjectまたはApexプリミティブ型のMap(キーはStringのみ可能)
    * 上記の型のメンバ変数を含むユーザ定義型
        * クラスはglobalで宣言されている必要があります。
        * public、private、globalのメンバ変数に並列化します。
        * static、transientの変数には並列化されません。
* `RestContext.response.responseBody`ではBlob型で設定しますが、戻り値の逐次化を使用する場合、Blob型は使用できません。
* 戻り値が`void`以外を設定し他状態で、`RestContext.response.responseBody`に値を設定しても、エラーにはなりませんが、レスポンスには反映されません。

##### 戻り値の逐次化を利用 ①Apexプリミティブ型を戻り値に設定した例
```java:ApexRestメソッド
@HttpGet
global static String getProducts() {
    return 'test';
}
```

```json:レスポンスボディ(JSON)
"test"
```

```xml:レスポンスボディ(XML)
<?xml version="1.0" encoding="UTF-8"?>
<response>test</response>
```

##### 戻り値音逐次化を利用 ②ユーザ定義型を戻り値に設定した例
```java:ApexRestメソッド
@HttpGet
global static ProductItem getProduct() {
    ProductItem item = new ProductItem('ETA6497-770', 'Mainspring');
    return item;
}

global class ProductItem {
    private String code;
    private String name;
    private ProductItem(String pCode, String pName) {
        this.code = pCode;
        this.name = pName;
    }
}
```


```json:レスポンスボディ(JSON)
{
    "name": "Mainspring",
    "code": "ETA6497-770"
}
```

```xml:レスポンスボディ(XML)
<?xml version="1.0" encoding="UTF-8"?>
<response xmlns:ProductItem="http://soap.sforce.com/schemas/class/RestApiSample">
    <ProductItem:code>ETA6497-770</ProductItem:code>
    <ProductItem:name>Mainspring</ProductItem:name>
</response>
```

#### 2. `RestContext.response.responseBody`に設定する
戻り値に`void`を設定した場合、`RestContext.response.responseBody`でレスポンスボディを設定できます。`responseBody`はBlob型です。

```java:ApexRestメソッド
@HttpGet
global static void getProduct() {
    RestResponse res = RestContext.response;
    ProductItem item = new ProductItem('ETA6497-770', 'Mainspring');
    res.addHeader('Content-Type', 'application/json');
    res.responseBody = Blob.valueOf(JSON.serialize(item));
}

global class ProductItem {
    private String code;
    private String name;
    private ProductItem(String pCode, String pName) {
        this.code = pCode;
        this.name = pName;
    }
}
```

```json:レスポンスボディ
{
    "name": "Mainspring",
    "code": "ETA6497-770"
}
```


## 参考
* [Apex クラスを REST Web サービスとして公開 | Apex開発者ガイド](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_rest.htm)
* [JSON サポート | Apex開発者ガイド](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_methods_system_json_overview.htm)
* [XML サポート | Apex開発者ガイド](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_xml_support.htm)