public class Swagger { private static final String HEADER_CONTENT_TYPE = 'Content-Type'; private static final String HEADER_ACCEPT = 'Accept'; private static final String HEADER_ACCEPT_DELIMITER = ','; private static final Map DELIMITERS = new Map { 'csv' => ',', 'ssv' => ' ', 'tsv' => '\t', 'pipes' => '|' }; public class Param { private String name, value; public Param(String name, String value) { this.name = name; this.value = value; } public override String toString() { return EncodingUtil.urlEncode(name, 'UTF-8') + '=' + EncodingUtil.urlEncode(value, 'UTF-8'); } } public interface Authentication { void apply(Map headers, List query); } public interface MappedProperties { Map getPropertyMappings(); } public abstract class ApiKeyAuth implements Authentication { protected final String paramName; protected String key = ''; public void setApiKey(String key) { this.key = key; } @TestVisible private String getApiKey() { return key; } } public class ApiKeyQueryAuth extends ApiKeyAuth { public ApiKeyQueryAuth(String paramName) { this.paramName = paramName; } public void apply(Map headers, List query) { query.add(new Param(paramName, key)); } } public class ApiKeyHeaderAuth extends ApiKeyAuth { public ApiKeyHeaderAuth(String paramName) { this.paramName = paramName; } public void apply(Map headers, List query) { headers.put(paramName, key); } } public class HttpBasicAuth implements Authentication { private String username = ''; private String password = ''; public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setCredentials(String username, String password) { setUsername(username); setPassword(password); } @TestVisible private String getHeaderValue() { return 'Basic ' + EncodingUtil.base64Encode(Blob.valueOf(username + ':' + password)); } public void apply(Map headers, List query) { headers.put('Authorization', getHeaderValue()); } } public class OAuth2 implements Authentication { private String accessToken = ''; public void setAccessToken(String accessToken) { this.accessToken = accessToken; } @TestVisible private String getHeaderValue() { return 'Bearer ' + accessToken; } public void apply(Map headers, List query) { headers.put('Authorization', getHeaderValue()); } } public class ApiException extends Exception { private final Integer code; private final String status; private final Map headers; private final String body; public ApiException(Integer code, String status, Map headers, String body) { this('API returned HTTP ' + code + ': ' + status); this.code = code; this.status = status; this.headers = headers; this.body = body; } public Integer getStatusCode() { return code; } public String getStatus() { return status; } public Map getHeaders() { return headers; } public String getBody() { return body; } } public virtual class ApiClient { protected String preferredContentType = 'application/json'; protected String preferredAccept = 'application/json'; protected final String basePath; @TestVisible protected final Map authentications = new Map(); public virtual Authentication getAuthentication(String authName) { return authentications.get(authName); } public virtual void setUsername(String username) { for (Authentication auth : authentications.values()) { if (auth instanceof HttpBasicAuth) { ((HttpBasicAuth) auth).setUsername(username); return; } } throw new NoSuchElementException('No HTTP basic authentication configured!'); } public virtual void setPassword(String password) { for (Authentication auth : authentications.values()) { if (auth instanceof HttpBasicAuth) { ((HttpBasicAuth) auth).setPassword(password); return; } } throw new NoSuchElementException('No HTTP basic authentication configured!'); } public virtual void setCredentials(String username, String password) { for (Authentication auth : authentications.values()) { if (auth instanceof HttpBasicAuth) { ((HttpBasicAuth) auth).setCredentials(username, password); return; } } throw new NoSuchElementException('No HTTP basic authentication configured!'); } public virtual void setApiKey(String apiKey) { for (Authentication auth : authentications.values()) { if (auth instanceof ApiKeyAuth) { ((ApiKeyAuth) auth).setApiKey(apiKey); return; } } throw new NoSuchElementException('No API key authentication configured!'); } public virtual void setAccessToken(String accessToken) { for (Authentication auth : authentications.values()) { if (auth instanceof OAuth2) { ((OAuth2) auth).setAccessToken(accessToken); return; } } throw new NoSuchElementException('No OAuth2 authentication configured!'); } public List makeParams(String name, List values) { List pairs = new List(); for (Object value : new List(values)) { pairs.add(new Param(name, String.valueOf(value))); } return pairs; } public List makeParam(String name, List values, String format) { List pairs = new List(); if (values != null) { String delimiter = DELIMITERS.get(format); pairs.add(new Param(name, String.join(values, delimiter))); } return pairs; } public List makeParam(String name, Object value) { List pairs = new List(); if (value != null) { pairs.add(new Param(name, String.valueOf(value))); } return pairs; } public virtual void assertNotNull(Object required, String parameterName) { if (required == null) { Exception e = new NullPointerException(); e.setMessage('Argument cannot be null: ' + parameterName); throw e; } } public virtual Object invoke( String method, String path, Object body, List query, List form, Map pathParams, Map headers, List accepts, List contentTypes, List authMethods, Type returnType) { HttpResponse res = getResponse(method, path, body, query, form, pathParams, headers, accepts, contentTypes, authMethods); Integer code = res.getStatusCode(); Boolean isFailure = code / 100 != 2; if (isFailure) { throw new ApiException(code, res.getStatus(), getHeaders(res), res.getBody()); } else if (returnType != null) { return toReturnValue(res.getBody(), returnType, res.getHeader('Content-Type')); } return null; } @TestVisible protected virtual Map getHeaders(HttpResponse res) { Map headers = new Map(); List headerKeys = res.getHeaderKeys(); for (String headerKey : headerKeys) { headers.put(headerKey, res.getHeader(headerKey)); } return headers; } @TestVisible protected virtual Object toReturnValue(String body, Type returnType, String contentType) { if (contentType == 'application/json') { Object o = returnType.newInstance(); if (o instanceof MappedProperties) { Map propertyMappings = ((MappedProperties) o).getPropertyMappings(); for (String baseName : propertyMappings.keySet()) { body = body.replaceAll('"' + baseName + '"\\s*:', '"' + propertyMappings.get(baseName) + '":'); } } JsonParser parser = Json.createParser(body); parser.nextToken(); return parser.readValueAs(returnType); } return body; } @TestVisible protected virtual HttpResponse getResponse( String method, String path, Object body, List query, List form, Map pathParams, Map headers, List accepts, List contentTypes, List authMethods) { HttpRequest req = new HttpRequest(); applyAuthentication(authMethods, headers, query); req.setMethod(method); req.setEndpoint(toEndpoint(path, pathParams, query)); String contentType = setContentTypeHeader(contentTypes, headers); setAcceptHeader(accepts, headers); setHeaders(req, headers); if (method != 'GET') { req.setBody(toBody(contentType, body, form)); } return new Http().send(req); } @TestVisible protected virtual void setHeaders(HttpRequest req, Map headers) { for (String headerName : headers.keySet()) { req.setHeader(headerName, String.valueOf(headers.get(headerName))); } } @TestVisible protected virtual String toBody(String contentType, Object body, List form) { if (contentType.contains('application/x-www-form-urlencoded')) { return paramsToString(form); } else if (contentType.contains('application/json')) { return Json.serialize(body); } return String.valueOf(body); } @TestVisible protected virtual String setContentTypeHeader(List contentTypes, Map headers) { if (contentTypes.isEmpty()) { headers.put(HEADER_CONTENT_TYPE, preferredContentType); return preferredContentType; } for (String contentType : contentTypes) { if (preferredContentType == contentType) { headers.put(HEADER_CONTENT_TYPE, contentType); return contentType; } } String contentType = contentTypes.get(0); headers.put(HEADER_CONTENT_TYPE, contentType); return contentType; } @TestVisible protected virtual void setAcceptHeader(List accepts, Map headers) { for (String accept : accepts) { if (preferredAccept == accept) { headers.put(HEADER_ACCEPT, accept); return; } } if (!accepts.isEmpty()) { headers.put(HEADER_ACCEPT, String.join(accepts, HEADER_ACCEPT_DELIMITER)); } } @TestVisible protected virtual void applyAuthentication(List names, Map headers, List query) { for (Authentication auth : getAuthMethods(names)) { auth.apply(headers, query); } } @TestVisible protected virtual List getAuthMethods(List names) { List authMethods = new List(); for (String name : names) { authMethods.add(authentications.get(name)); } return authMethods; } @TestVisible protected virtual String toPath(String path, Map params) { String formatted = path; for (String key : params.keySet()) { formatted = formatted.replace('{' + key + '}', String.valueOf(params.get(key))); } return formatted; } @TestVisible protected virtual String toEndpoint(String path, Map params, List queryParams) { String query = '?' + paramsToString(queryParams); return basePath + toPath(path, params) + query.removeEnd('?'); } @TestVisible protected virtual String paramsToString(List params) { String s = ''; for (Param p : params) { s += '&' + p; } return s.removeStart('&'); } } }