From 581459a91266535f7c366e2c25409c54ab58cdd5 Mon Sep 17 00:00:00 2001 From: Nikolai <35693870+nkskaare@users.noreply.github.com> Date: Thu, 2 Jan 2025 11:42:25 +0100 Subject: [PATCH] feat: HttpClient, String&Date Utility classes, Transformer + updates to QueryBuilder and SObjectSelector (#70) * add generic httpclient * update QueryBuilder with filter list * Add utility classes for strings and dates * add httpclient test class * add transformer class and interface * update sobjectselector --- src/apex/utils/classes/DateFormatUtility.cls | 55 +++ .../classes/DateFormatUtility.cls-meta.xml | 5 + src/apex/utils/classes/HttpClient.cls | 400 ++++++++++++++++++ .../utils/classes/HttpClient.cls-meta.xml | 5 + src/apex/utils/classes/QueryBuilder.cls | 111 +++-- src/apex/utils/classes/SObjectSelector.cls | 43 +- .../utils/classes/StringFormatUtility.cls | 99 +++++ .../classes/StringFormatUtility.cls-meta.xml | 5 + src/apex/utils/classes/Transformer.cls | 27 ++ .../utils/classes/Transformer.cls-meta.xml | 4 + .../classes/interfaces/ITransformable.cls | 3 + .../interfaces/ITransformable.cls-meta.xml | 5 + .../classes/tests/DateFormatUtilityTest.cls | 57 +++ .../tests/DateFormatUtilityTest.cls-meta.xml | 5 + .../utils/classes/tests/HttpClientTest.cls | 307 ++++++++++++++ .../classes/tests/HttpClientTest.cls-meta.xml | 5 + .../utils/classes/tests/QueryBuilderTest.cls | 23 + .../classes/tests/SObjectSelectorTest.cls | 72 ++++ .../tests/SObjectSelectorTest.cls-meta.xml | 5 + .../classes/tests/StringFormatUtilityTest.cls | 128 ++++++ .../StringFormatUtilityTest.cls-meta.xml | 5 + .../utils/classes/tests/TransformerTest.cls | 81 ++++ .../tests/TransformerTest.cls-meta.xml | 5 + 23 files changed, 1422 insertions(+), 33 deletions(-) create mode 100644 src/apex/utils/classes/DateFormatUtility.cls create mode 100644 src/apex/utils/classes/DateFormatUtility.cls-meta.xml create mode 100644 src/apex/utils/classes/HttpClient.cls create mode 100644 src/apex/utils/classes/HttpClient.cls-meta.xml create mode 100644 src/apex/utils/classes/StringFormatUtility.cls create mode 100644 src/apex/utils/classes/StringFormatUtility.cls-meta.xml create mode 100644 src/apex/utils/classes/Transformer.cls create mode 100644 src/apex/utils/classes/Transformer.cls-meta.xml create mode 100644 src/apex/utils/classes/interfaces/ITransformable.cls create mode 100644 src/apex/utils/classes/interfaces/ITransformable.cls-meta.xml create mode 100644 src/apex/utils/classes/tests/DateFormatUtilityTest.cls create mode 100644 src/apex/utils/classes/tests/DateFormatUtilityTest.cls-meta.xml create mode 100644 src/apex/utils/classes/tests/HttpClientTest.cls create mode 100644 src/apex/utils/classes/tests/HttpClientTest.cls-meta.xml create mode 100644 src/apex/utils/classes/tests/SObjectSelectorTest.cls create mode 100644 src/apex/utils/classes/tests/SObjectSelectorTest.cls-meta.xml create mode 100644 src/apex/utils/classes/tests/StringFormatUtilityTest.cls create mode 100644 src/apex/utils/classes/tests/StringFormatUtilityTest.cls-meta.xml create mode 100644 src/apex/utils/classes/tests/TransformerTest.cls create mode 100644 src/apex/utils/classes/tests/TransformerTest.cls-meta.xml diff --git a/src/apex/utils/classes/DateFormatUtility.cls b/src/apex/utils/classes/DateFormatUtility.cls new file mode 100644 index 0000000..3bee6ca --- /dev/null +++ b/src/apex/utils/classes/DateFormatUtility.cls @@ -0,0 +1,55 @@ +public class DateFormatUtility { + public class FormattingException extends Exception { + } + + public static String formatDateTimeToString(DateTime inputDate) { + String formattedDate = DateTime.ValueOf(inputDate) + .format('dd MMM yyyy HH:mm'); + return formattedDate; + } + + public static String formatDateToString(Date inputDate, String dateFormat) { + return DateTime.newInstance( + inputDate.year(), + inputDate.month(), + inputDate.day() + ) + .format(dateFormat); + } + + public static DateTime parseDateTimeFromString(String dateString) { + if (dateString != null) { + return (DateTime) JSON.deserialize( + '"' + dateString + '"', + DateTime.class + ); + } + return null; + } + + public static Date parseDateFromString(String dateString) { + if (dateString != null) { + return (Date) JSON.deserialize('"' + dateString + '"', Date.class); + } + return null; + } + + /** + * @description Takes in a list of strings and returns a date object. The list should contain the day, month and year in that order. + * + * @param dateList - A list of strings containing the day, month and year in that order. + */ + public static Date parseDateFromString(String dateString, String format) { + if (format == 'dd.MM.yyyy') { + List dateList = dateString.split('\\.'); + + return Date.newInstance( + Integer.valueOf(dateList[2]), + Integer.valueOf(dateList[1]), + Integer.valueOf(dateList[0]) + ); + } + + return parseDateFromString(dateString); + } +} diff --git a/src/apex/utils/classes/DateFormatUtility.cls-meta.xml b/src/apex/utils/classes/DateFormatUtility.cls-meta.xml new file mode 100644 index 0000000..f5e18fd --- /dev/null +++ b/src/apex/utils/classes/DateFormatUtility.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/src/apex/utils/classes/HttpClient.cls b/src/apex/utils/classes/HttpClient.cls new file mode 100644 index 0000000..cb7ffd0 --- /dev/null +++ b/src/apex/utils/classes/HttpClient.cls @@ -0,0 +1,400 @@ +public virtual class HttpClient implements IHttpClient.Callout, IHttpClient.Callback { + List statusCodes = new List{ 200 }; + + public class InvalidUrlException extends Exception { + } + + public interface IUrl { + String toString(); + } + + /** + * @description URL class + */ + public virtual class Url implements IUrl { + String baseUrl; + String path; + Map params = new Map(); + + public Url(String baseUrl) { + this.baseUrl = baseUrl; + } + + public override String toString() { + String url = this.baseUrl; + + url += this.getPath(); + url += this.getQueryString(); + + return url; + } + + /** + * @description Set the path + * ! validatePath commented out, will throw error on paths from Nova_Proxy_Path__mdt + */ + public void setPath(String path) { + // this.validatePath(path); + this.path = path; + } + + /** + * @description Get the path + * ! Contains fixes for paths from Nova_Proxy_Path__mdt. In the future, + * this should be removed and instead validated in the validatePath method + * + * @return String + */ + public String getPath() { + if (this.path == null) { + return ''; + } + + // Fix paths from Nova_Proxy_Path__mdt + if (path.substring(0, 1) != '/') { + this.path = '/' + this.path; + } + + if (path.substring(path.length() - 1) == '/') { + this.path = path.substring(0, path.length() - 1); + } + + return this.path; + } + + private String getQueryString() { + if (this.params.isEmpty()) { + return ''; + } + + String queryString = '?'; + List paramList = new List(); + for (String key : this.params.keySet()) { + paramList.add(key + '=' + this.params.get(key)); + } + queryString += String.join(paramList, '&'); + + return queryString; + } + + public Map getParams() { + return this.params; + } + + public void setParam(String key, String value) { + this.params.put(key, value); + } + + public void setParams(Map params) { + this.params.putAll(params); + } + + /** + * @description Recieves a list of paths, concats them all and makes sure they make a correct path + * @param paths + * @return String + * @example `['api','method/','/ex/'] => /api/method/ex` + */ + public String cleanPath(List paths) { + String clean = ''; + for (String path : paths) { + clean += fixPath(path); + } + + if (clean.substring(clean.length() - 1) == '/') { + clean = clean.substring(0, clean.length() - 1); + } + + clean = '/' + clean; + + return clean; + } + + /** + * @description Fixes a path, by making sure it doesnt start with '/'' and ends with '/' + * @param path + * @return String + */ + public String fixPath(String path) { + String ret = path; + + if (ret.substring(0, 1) == '/') { + ret = ret.substring(1); + } + if (ret.substring(ret.length() - 1) != '/') { + ret += '/'; + } + return ret; + } + + /** + * @description Validate the path + * ! To be implemented in the future, current implementation will break paths from Nova_Proxy_Path__mdt + * @param path + */ + protected void validatePath(String path) { + if (path.substring(0, 1) != '/') { + throw new InvalidUrlException( + 'Path must start with a forward slash' + ); + } + + if (path.substring(path.length() - 1) == '/') { + throw new InvalidUrlException( + 'Path must not end with a forward slash' + ); + } + } + } + + public virtual class RequestBuilder { + public HttpRequest request; + private List registeredHeaders = new List(); + + public RequestBuilder(HttpRequest request) { + this.request = request; + } + + public RequestBuilder() { + this.request = new HttpRequest(); + } + + public virtual RequestBuilder fromConfig(CalloutConfig params) { + if (params.method != null) { + this.request.setMethod(params.method); + } + if (params.endpoint != null) { + this.request.setEndpoint(params.endpoint); + } + if (params.timeout != null) { + this.request.setTimeout(params.timeout); + } + if (params.body != null) { + this.request.setBody(params.body); + } + if (params.headers != null) { + this.setHeaders(params.headers); + } + return this; + } + + public RequestBuilder setEndpoint(IUrl url) { + this.request.setEndpoint(url.toString()); + return this; + } + + public RequestBuilder setEndpoint(String url) { + this.request.setEndpoint(url); + return this; + } + + public RequestBuilder setHeaders(Map headers) { + for (String key : headers.keySet()) { + String headerValue = headers.get(key); + if (headerValue == null) { + continue; + } + + this.request.setHeader(key, headerValue); + this.registeredHeaders.add(key); + } + + return this; + } + + /** + * @description Serialize an object to JSON and set it as the request body + * also sets the content type to application/json + * + * @param bodyObj + */ + public RequestBuilder setJSONBody(Object bodyObj) { + this.setContentType('json'); + this.request.setBody(JSON.serializePretty(bodyObj, true)); + + return this; + } + + /** + * @description Short-hand support for setting the content type + * + * @param contentType + */ + public RequestBuilder setContentType(String contentType) { + String contentHeader; + if (contentType.toLowerCase() == 'json') { + contentHeader = 'application/json'; + } else if (contentType.toLowerCase() == 'xml') { + contentHeader = 'application/xml'; + } else if (contentType.toLowerCase() == 'form') { + contentHeader = 'application/x-www-form-urlencoded'; + } else { + contentHeader = contentType; + } + + if (contentHeader == null) { + throw new InvalidUrlException('Unsupported content type'); + } + + this.request.setHeader('Content-Type', contentHeader); + this.registeredHeaders.add('Content-Type'); + return this; + } + + public HttpRequest getRequest() { + return this.request; + } + + public virtual String toJson() { + CalloutConfig config = new CalloutConfig(); + config.method = this.request.getMethod(); + config.endpoint = this.request.getEndpoint(); + config.body = this.request.getBody(); + for (String key : this.registeredHeaders) { + config.headers.put(key, this.request.getHeader(key)); + } + + return new JSONConfigParser().toJson(config); + } + + public virtual RequestBuilder fromJson(String configJson) { + return this.fromConfig( + new JSONConfigParser() + .fromJson(configJson, HttpClient.CalloutConfig.class) + ); + } + } + + public HttpResponse get(HttpRequest request) { + return this.callout('GET', request); + } + + public HttpResponse post(HttpRequest request) { + return this.callout('POST', request); + } + + public HttpResponse put(HttpRequest request) { + return this.callout('PUT', request); + } + + public HttpResponse callout(String method, HttpRequest request) { + request.setMethod(method); + return this.callout(request); + } + + public virtual HttpResponse callout(HttpRequest request) { + Logger.info('Making callout ' + request.toString()); + return new Http().send(request); + } + + public void setStatusCodes(List statusCodes) { + this.statusCodes = statusCodes; + } + + public void callback(HttpResponse response) { + this.handleResponse(response); + } + + public virtual void handleResponse(HttpResponse response) { + if (!statusCodes.contains(response.getStatusCode())) { + throw new CalloutException( + 'Callout failed with status code ' + response.getStatusCode() + ); + } + } + + public class JSONConfigParser { + public CalloutConfig fromJson(String configJson, Type configType) { + CalloutConfig config = (CalloutConfig) JSON.deserialize( + configJson, + configType + ); + config.headers = this.getHeadersFromJson(configJson); + return config; + } + + private Map getHeadersFromJson(String configJson) { + Map sourceHeaders = (Map) ((Map) JSON.deserializeUntyped( + configJson + )) + .get('headers'); + + Map headers = new Map(); + if (sourceHeaders == null) { + return headers; + } + + for (String key : sourceHeaders.keySet()) { + headers.put(key, String.valueOf(sourceHeaders.get(key))); + } + + return headers; + } + + public String toJson(CalloutConfig config) { + return JSON.serialize(config); + } + } + + public virtual class CalloutConfig { + public String method; + public String endpoint; + public Map headers = new Map(); + public String body; + public Integer timeout = 120000; + } + + public class AsyncCalloutConfig extends CalloutConfig { + public String callbackClass = 'HttpClient'; + } + + @Future(callout=true) + public static void asyncCallout(String asyncConfigJson) { + Logger.info('Making async callout ' + asyncConfigJson); + try { + ((HttpClient) TypeFactory.newInstance('HttpClient')) + .doAsyncCallout(asyncConfigJson); + } catch (Exception e) { + Logger.error(e.getMessage()); + } + } + + /** + * @description Make an async callout + * + * @param config + */ + @TestVisible + private void doAsyncCallout(String configJson) { + AsyncCalloutConfig config = (AsyncCalloutConfig) new JSONConfigParser() + .fromJson(configJson, HttpClient.AsyncCalloutConfig.class); + + System.debug(config); + + HttpRequest request = new HttpClient.RequestBuilder(new HttpRequest()) + .fromConfig(config) + .getRequest(); + + HttpResponse response = new HttpClient().callout(request); + + if (config.callbackClass == null) { + return; + } + + IHttpClient.Callback callback = (IHttpClient.Callback) TypeFactory.newInstance( + config.callbackClass + ); + callback.callback(response); + } + + /** + * @description Retry a callout + * + * @param request + */ + public void retry(HttpRequest request) { + String asyncConfigJson = new RequestBuilder(request).toJson(); + Logger.info('Retrying callout ' + asyncConfigJson); + HttpClient.asyncCallout(asyncConfigJson); + } +} diff --git a/src/apex/utils/classes/HttpClient.cls-meta.xml b/src/apex/utils/classes/HttpClient.cls-meta.xml new file mode 100644 index 0000000..800ee42 --- /dev/null +++ b/src/apex/utils/classes/HttpClient.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/src/apex/utils/classes/QueryBuilder.cls b/src/apex/utils/classes/QueryBuilder.cls index c253d26..c5cdf8f 100644 --- a/src/apex/utils/classes/QueryBuilder.cls +++ b/src/apex/utils/classes/QueryBuilder.cls @@ -1,8 +1,16 @@ public class QueryBuilder { private Query query; + public Config config = new Config(); + + Map bindVariables = new Map(); + + public class Config { + public String defaultChain = 'AND'; + } public QueryBuilder(Schema.SObjectType sObjectType) { this.query = new Query(sObjectType.getDescribe().getName()); + this.query.filterChain = this.config.defaultChain; } public QueryBuilder(String sObjectType) { @@ -13,20 +21,41 @@ public class QueryBuilder { public String sObjectType; public Set fields; public List subQueries; - public String filter; + public List filters; public String sortBy; public String limitClause; + public String filterChain = 'AND'; + public Query(String sObjectType) { this.sObjectType = sObjectType; this.fields = new Set(); this.subQueries = new List(); - this.filter = null; + this.filters = new List(); this.sortBy = null; this.limitClause = null; } + private String resolveFilter() { + if (filters.size() == 1) { + return this.filters[0].toString(); + } + + String filter = ''; + + for (ICondition condition : filters) { + filter += + '(' + + condition.toString() + + ') ' + + this.filterChain + + ' '; + } + + return filter.removeEnd(' ' + this.filterChain + ' '); + } + public override String toString() { String queryString = ''; queryString += 'SELECT ' + String.join(fields, ', '); @@ -39,8 +68,8 @@ public class QueryBuilder { queryString += ' FROM ' + sObjectType; - if (filter != null) { - queryString += ' WHERE ' + filter; + if (!this.filters.isEmpty()) { + queryString += ' WHERE ' + this.resolveFilter(); } if (sortBy != null) { @@ -60,59 +89,68 @@ public class QueryBuilder { } public virtual class Condition implements ICondition { + public String expression; + public String field; public String operator; - public String value; - public String raw; + public Object value; - public Condition(String field) { - this.field = field; - } - - public Condition(String raw, Boolean isRaw) { - this.raw = raw; + public Condition(String expression) { + if (expression.containsWhitespace()) { + this.expression = expression; + } else { + this.field = expression; + } } - public ICondition equals(Object value) { + public String equals(Object value) { this.operator = '='; return this.setValue(value); } - public ICondition notEquals(Object value) { + public String notEquals(Object value) { this.operator = '!='; return this.setValue(value); } - public ICondition greaterThan(Object value) { + public String greaterThan(Object value) { this.operator = '>'; return this.setValue(value); } - public ICondition lessThan(Object value) { + public String lessThan(Object value) { this.operator = '<'; return this.setValue(value); } - public ICondition isIn(Object value) { + public String isIn(Object value) { this.operator = 'IN'; return this.setValue(value); } - public ICondition matches(Object value) { + public String matches(Object value) { this.operator = 'LIKE'; return this.setValue(value); } - private ICondition setValue(Object value) { + public String inSubQuery(IQuery subQuery) { + this.operator = 'IN'; + return this.setValue('( ' + subQuery.toString() + ' )'); + } + + private String setValue(Object value) { this.value = String.valueOf(value); - return this; + this.expression = + this.field + + ' ' + + this.operator + + ' ' + + this.value; + return this.expression; } public virtual override String toString() { - if (this.raw != null) { - return this.raw; - } - return this.field + ' ' + this.operator + ' ' + this.value; + return this.expression; } } @@ -127,6 +165,10 @@ public class QueryBuilder { this.condition = condition; } + public ConditionNode(String expression) { + this.condition = new Condition(expression); + } + public ConditionNode( String logical, ConditionNode left, @@ -178,7 +220,7 @@ public class QueryBuilder { public ConditionBuilder andWith(String condition) { return this.addNode( 'AND', - new ConditionNode(new Condition(condition, true)) + new ConditionNode(new Condition(condition)) ); } @@ -189,7 +231,7 @@ public class QueryBuilder { public ConditionBuilder orWith(String condition) { return this.addNode( 'OR', - new ConditionNode(new Condition(condition, true)) + new ConditionNode(new Condition(condition)) ); } @@ -209,15 +251,13 @@ public class QueryBuilder { } public QueryBuilder addFilter(String condition) { - if (this.query.filter == null) { - this.query.filter = ''; - } - this.query.filter += condition; + this.query.filters.add(new Condition(condition)); return this; } public QueryBuilder addFilter(ICondition condition) { - return this.addFilter(condition.toString()); + this.query.filters.add(condition); + return this; } public QueryBuilder addSubQuery(IQuery query) { @@ -239,6 +279,15 @@ public class QueryBuilder { return this.query; } + public QueryBuilder setVariable(String variable, Object value) { + this.bindVariables.put(variable, value); + return this; + } + + public Map getVariables() { + return this.bindVariables; + } + public override String toString() { return String.escapeSingleQuotes(this.query.toString()); } diff --git a/src/apex/utils/classes/SObjectSelector.cls b/src/apex/utils/classes/SObjectSelector.cls index 37eb92f..a05e1c6 100644 --- a/src/apex/utils/classes/SObjectSelector.cls +++ b/src/apex/utils/classes/SObjectSelector.cls @@ -2,8 +2,10 @@ public abstract class SObjectSelector { public String sObjectType; public List defaultFields; - public SObjectSelector() { - } + System.AccessLevel accessLevel = System.AccessLevel.SYSTEM_MODE; + + @TestVisible + protected QueryBuilder builder; public SObjectSelector(String sObjectType) { this.sObjectType = sObjectType; @@ -15,6 +17,28 @@ public abstract class SObjectSelector { this.defaultFields = fields; } + public SObjectSelector(ISchemaSObject schema) { + this.sObjectType = schema.getQuerySObject(); + this.defaultFields = schema.getQueryFields(); + } + + public class QueryException extends Exception { + String query; + + public QueryException(String message, String query) { + this.setMessage(message); + this.query = query; + } + } + + public System.AccessLevel getAccessLevel() { + return this.accessLevel; + } + + public String getQuery() { + return this.builder.getQuery().toString(); + } + public virtual List getWhere(String field, Object value) { QueryBuilder builder = new QueryBuilder(this.sObjectType) .selectFields(this.defaultFields) @@ -38,4 +62,19 @@ public abstract class SObjectSelector { return Database.query(builder.toString()); } + + public virtual List query() { + return Database.queryWithBinds( + this.builder.getQuery().toString(), + this.builder.getVariables(), + this.getAccessLevel() + ); + } + + /** + * Future implementation + */ + // public virtual List> search() { + // return Search.find(this.getQuery(), this.getAccessLevel()) + // } } diff --git a/src/apex/utils/classes/StringFormatUtility.cls b/src/apex/utils/classes/StringFormatUtility.cls new file mode 100644 index 0000000..8663e36 --- /dev/null +++ b/src/apex/utils/classes/StringFormatUtility.cls @@ -0,0 +1,99 @@ +public class StringFormatUtility { + public static String stripHtmlTags(String inputString) { + String formattedString = inputString; + formattedString = formattedString.stripHtmlTags(); + + return formattedString; + } + + public static String convertNickNameToUserName(String inputNickName) { + List userName = [ + SELECT Id, Name + FROM User + WHERE CommunityNickname = :inputNickName + LIMIT 1 + ]; + String convertedUserName; + + if (userName.isEmpty()) { + return null; + } + + return userName[0].Name; + } + + public static String getInitials(String inputName) { + if (inputName != null) { + String[] names = inputName.split(' '); + if (names.size() >= 2) { + String firstNameInitial = names[0].substring(0, 1); + String lastNameInitial = names[1].substring(0, 1); + + return firstNameInitial + lastNameInitial; + } + } + return ''; + } + + public static String nameConcat(String firstName, String lastName) { + if (firstName == null && lastName == null) { + return ''; + } + + if (firstName == null) { + return lastName; + } + + if (lastName == null) { + return firstName; + } + + return firstName + ' ' + lastName; + } + + public static String ifNullReplaceWithEmpty(String input) { + return ifNullReplaceWith(input, ''); + } + + public static String ifNullReplaceWith(String input, String replace) { + if (String.isBlank(input)) { + return replace; + } + return input; + } + + /** + * @description Method that camelcasializes every word in a string even if it is only one word. + * Words must be separated by separator input. + * + **/ + public static String camelCaseAllWords(String input, String separator) { + if (String.isBlank(input)) { + return input; + } + + String output = ''; + input = input.toLowerCase(); + + List words = input.split(separator); + + Integer count = 0; + for (String w : words) { + if (w.contains('-') && w.length() > 1) { + w = camelCaseAllWords(w, '-'); + } + + String first = w.substring(0, 1); + String rest = w.substring(1); + + output += first.toUpperCase() + rest; + + if ((count + 1) < words.size()) { + output += separator; + } + count++; + } + + return output; + } +} diff --git a/src/apex/utils/classes/StringFormatUtility.cls-meta.xml b/src/apex/utils/classes/StringFormatUtility.cls-meta.xml new file mode 100644 index 0000000..f5e18fd --- /dev/null +++ b/src/apex/utils/classes/StringFormatUtility.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/src/apex/utils/classes/Transformer.cls b/src/apex/utils/classes/Transformer.cls new file mode 100644 index 0000000..a668c7a --- /dev/null +++ b/src/apex/utils/classes/Transformer.cls @@ -0,0 +1,27 @@ +public class Transformer { + public static List transform( + List source, + Type target + ) { + try { + List result = (List) Type.forName( + 'List<' + target.getName() + '>' + ) + .newInstance(); + for (ITransformable item : source) { + result.add(item.transformTo(target)); + } + return result; + } catch (Exception e) { + throw TransformExceptions.errorTransforming(e.getMessage()); + } + } + + public static Object transform(ITransformable source, Type target) { + try { + return source.transformTo(target); + } catch (Exception e) { + throw TransformExceptions.errorTransforming(e.getMessage()); + } + } +} diff --git a/src/apex/utils/classes/Transformer.cls-meta.xml b/src/apex/utils/classes/Transformer.cls-meta.xml new file mode 100644 index 0000000..7640834 --- /dev/null +++ b/src/apex/utils/classes/Transformer.cls-meta.xml @@ -0,0 +1,4 @@ + + 62.0 + Active + diff --git a/src/apex/utils/classes/interfaces/ITransformable.cls b/src/apex/utils/classes/interfaces/ITransformable.cls new file mode 100644 index 0000000..374b4a4 --- /dev/null +++ b/src/apex/utils/classes/interfaces/ITransformable.cls @@ -0,0 +1,3 @@ +public interface ITransformable { + Object transformTo(Type target); +} diff --git a/src/apex/utils/classes/interfaces/ITransformable.cls-meta.xml b/src/apex/utils/classes/interfaces/ITransformable.cls-meta.xml new file mode 100644 index 0000000..800ee42 --- /dev/null +++ b/src/apex/utils/classes/interfaces/ITransformable.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/src/apex/utils/classes/tests/DateFormatUtilityTest.cls b/src/apex/utils/classes/tests/DateFormatUtilityTest.cls new file mode 100644 index 0000000..fb1b3e1 --- /dev/null +++ b/src/apex/utils/classes/tests/DateFormatUtilityTest.cls @@ -0,0 +1,57 @@ +@isTest +public class DateFormatUtilityTest { + @isTest + static void testFormatDateTime() { + DateTime testDateTime = DateTime.newInstance(2022, 3, 15, 10, 30, 0); + + Test.startTest(); + String result = DateFormatUtility.formatDateTimetoString(testDateTime); + Test.stopTest(); + + String expected = '15 Mar 2022 10:30'; + Assert.areEqual(expected, result); + } + + @isTest + static void testParseDateFromString() { + String testDateString = '2022-04-20T16:20:00.000Z'; + + Test.startTest(); + DateTime result = DateFormatUtility.parseDateTimeFromString( + testDateString + ); + Test.stopTest(); + + DateTime expected = DateTime.newInstanceGmt(2022, 4, 20, 16, 20, 0); + Assert.areEqual(expected, result, 'Date should be parsed correctly'); + } + + @IsTest + static void testFormatDate() { + Date testDate = Date.newInstance(2022, 3, 15); + + Test.startTest(); + String result = DateFormatUtility.formatDateToString( + testDate, + 'yyyy-MM-dd' + ); + Test.stopTest(); + + Assert.areEqual('2022-03-15', result); + } + + @IsTest + static void testParseDateFromStringFormat() { + String testDateString = '20.04.2022'; + + Test.startTest(); + Date result = DateFormatUtility.parseDateFromString( + testDateString, + 'dd.MM.yyyy' + ); + Test.stopTest(); + + Date expected = Date.newInstance(2022, 4, 20); + Assert.areEqual(expected, result); + } +} diff --git a/src/apex/utils/classes/tests/DateFormatUtilityTest.cls-meta.xml b/src/apex/utils/classes/tests/DateFormatUtilityTest.cls-meta.xml new file mode 100644 index 0000000..f5e18fd --- /dev/null +++ b/src/apex/utils/classes/tests/DateFormatUtilityTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/src/apex/utils/classes/tests/HttpClientTest.cls b/src/apex/utils/classes/tests/HttpClientTest.cls new file mode 100644 index 0000000..53108da --- /dev/null +++ b/src/apex/utils/classes/tests/HttpClientTest.cls @@ -0,0 +1,307 @@ +@IsTest +public class HttpClientTest { + public static final String CALLOUT_JSON = + '{"timeout":120000,' + + '"method":"GET",' + + '"headers":{"Content-Type":"application/json"},' + + '"endpoint":"https://www.example.com",' + + '"body":"{\\"test\\": \\"hello\\"}"}'; + + public static final String ASYNC_CALLOUT_JSON = + '{"timeout":120000,' + + '"method":"GET",' + + '"headers":{"Content-Type":"application/json"},' + + '"endpoint":"https://www.example.com",' + + '"body":"{\\"test\\": \\"hello\\"}",' + + '"callbackClass":"HttpClient"}'; + + @IsTest + static void testCreateUrl() { + HttpClient.Url url = new HttpClient.Url('https://www.example.com'); + url.setPath('/path'); + url.setParam('test', '5'); + url.setParams(new Map{ 'test2' => 'hello' }); + + Assert.areEqual( + 'https://www.example.com/path?test=5&test2=hello', + url.toString() + ); + } + + public class TestJsonBody { + public String test = 'hello'; + public Integer num = 5; + public String empty; + } + + @IsTest + static void testCreateRequest() { + HttpRequest request = new HttpClient.RequestBuilder() + .setEndpoint(new HttpClient.Url('https://www.example.com')) + .setJSONBody(new TestJsonBody()) + .getRequest(); + + Assert.areEqual('https://www.example.com', request.getEndpoint()); + Assert.areEqual( + JSON.serializePretty(new TestJsonBody(), true), + request.getBody() + ); + Assert.areEqual('application/json', request.getHeader('Content-Type')); + } + + @IsTest + static void testRequestToJson() { + HttpRequest request = new HttpRequest(); + request.setMethod('GET'); + request.setEndpoint('https://www.example.com'); + request.setBody('{"test": "hello"}'); + + HttpClient.RequestBuilder builder = new HttpClient.RequestBuilder( + request + ); + String requestJson = builder.setContentType('json').toJson(); + + Assert.areEqual(CALLOUT_JSON, requestJson); + } + + @IsTest + static void testRequestFromJson() { + HttpRequest request = new HttpClient.RequestBuilder() + .fromJson(CALLOUT_JSON) + .getRequest(); + + Assert.areEqual('GET', request.getMethod()); + Assert.areEqual('https://www.example.com', request.getEndpoint()); + Assert.areEqual('{"test": "hello"}', request.getBody()); + Assert.areEqual('application/json', request.getHeader('Content-Type')); + } + + @IsTest + static void testConfigFromJson() { + HttpClient.CalloutConfig config = (HttpClient.CalloutConfig) new HttpClient.JSONConfigParser() + .fromJson(CALLOUT_JSON, HttpClient.CalloutConfig.class); + + Assert.areEqual('GET', config.method); + Assert.areEqual('https://www.example.com', config.endpoint); + Assert.areEqual('{"test": "hello"}', config.body); + Assert.areEqual('application/json', config.headers.get('Content-Type')); + Assert.areEqual(120000, config.timeout); + + HttpClient.AsyncCalloutConfig asyncConfig = (HttpClient.AsyncCalloutConfig) new HttpClient.JSONConfigParser() + .fromJson(ASYNC_CALLOUT_JSON, HttpClient.AsyncCalloutConfig.class); + + Assert.areEqual('GET', asyncConfig.method); + Assert.areEqual('https://www.example.com', asyncConfig.endpoint); + Assert.areEqual('{"test": "hello"}', asyncConfig.body); + Assert.areEqual( + 'application/json', + asyncConfig.headers.get('Content-Type') + ); + Assert.areEqual(120000, asyncConfig.timeout); + Assert.areEqual('HttpClient', asyncConfig.callbackClass); + } + + @IsTest + static void testConfigToJson() { + HttpClient.CalloutConfig config = new HttpClient.CalloutConfig(); + + config.method = 'GET'; + config.endpoint = 'https://www.example.com'; + config.body = '{"test": "hello"}'; + config.headers.put('Content-Type', 'application/json'); + config.timeout = 120000; + + Assert.areEqual( + CALLOUT_JSON, + new HttpClient.JsonConfigParser().toJson(config) + ); + + HttpClient.AsyncCalloutConfig asyncConfig = new HttpClient.AsyncCalloutConfig(); + + asyncConfig.method = 'GET'; + asyncConfig.endpoint = 'https://www.example.com'; + asyncConfig.body = '{"test": "hello"}'; + asyncConfig.headers.put('Content-Type', 'application/json'); + asyncConfig.timeout = 120000; + asyncConfig.callbackClass = 'HttpClient'; + + Assert.areEqual( + ASYNC_CALLOUT_JSON, + new HttpClient.JsonConfigParser().toJson(asyncConfig) + ); + } + + public class MockHttpResponse implements HttpCalloutMock { + Boolean hasResponded = false; + HttpRequest inputRequest; + + public HttpResponse respond(HttpRequest request) { + this.inputRequest = request; + + HttpResponse response = new HttpResponse(); + response.setBody('{"test": "hello"}'); + response.setStatusCode(200); + this.hasResponded = true; + + return response; + } + } + + @IsTest + static void testGet() { + HttpRequest request = new HttpRequest(); + + Test.setMock(HttpCalloutMock.class, new MockHttpResponse()); + + Test.startTest(); + HttpClient client = new HttpClient(); + HttpResponse response = client.get(request); + Test.stopTest(); + + Assert.areEqual('GET', request.getMethod()); + Assert.areEqual('{"test": "hello"}', response.getBody()); + Assert.areEqual(200, response.getStatusCode()); + } + + @IsTest + static void testPost() { + HttpRequest request = new HttpRequest(); + + Test.setMock(HttpCalloutMock.class, new MockHttpResponse()); + + Test.startTest(); + HttpClient client = new HttpClient(); + HttpResponse response = client.post(request); + Test.stopTest(); + + Assert.areEqual('POST', request.getMethod()); + Assert.areEqual('{"test": "hello"}', response.getBody()); + Assert.areEqual(200, response.getStatusCode()); + } + + @IsTest + static void testPut() { + HttpRequest request = new HttpRequest(); + + Test.setMock(HttpCalloutMock.class, new MockHttpResponse()); + + Test.startTest(); + HttpClient client = new HttpClient(); + HttpResponse response = client.put(request); + Test.stopTest(); + + Assert.areEqual('PUT', request.getMethod()); + Assert.areEqual('{"test": "hello"}', response.getBody()); + Assert.areEqual(200, response.getStatusCode()); + } + + @IsTest + static void testHandleResponseException() { + HttpResponse response = new HttpResponse(); + response.setStatusCode(400); + + HttpClient client = new HttpClient(); + client.setStatusCodes(new List{ 200, 201 }); + + Test.startTest(); + try { + client.callback(response); + Assert.isTrue(false); + } catch (CalloutException e) { + Assert.areEqual( + 'Callout failed with status code 400', + e.getMessage() + ); + } + Test.stopTest(); + } + + @IsTest + static void testHandleResponseSuccess() { + HttpResponse response = new HttpResponse(); + response.setStatusCode(200); + + HttpClient client = new HttpClient(); + client.setStatusCodes(new List{ 200, 201 }); + + Test.startTest(); + client.callback(response); + Test.stopTest(); + } + + @IsTest + static void testAsyncCalloutFuture() { + MockHttpResponse responder = new MockHttpResponse(); + Test.setMock(HttpCalloutMock.class, responder); + + String asyncConfigJson = '{"method":"GET","endpoint":"https://www.example.com"}'; + Test.startTest(); + HttpClient.asyncCallout(asyncConfigJson); + Test.stopTest(); + + Assert.isTrue(responder.hasResponded); + Assert.areEqual( + responder.inputRequest.getEndpoint(), + 'https://www.example.com' + ); + Assert.areEqual(responder.inputRequest.getMethod(), 'GET'); + } + + public class MockCallback implements IHttpClient.Callback { + Boolean called = false; + public void callback(HttpResponse response) { + this.called = true; + } + } + + @IsTest + static void testAsyncCallout() { + MockCallback mockCallback = new MockCallback(); + TypeFactory.setMock('HttpClient', mockCallback); + + HttpClient.AsyncCalloutConfig config = new HttpClient.AsyncCalloutConfig(); + config.method = 'GET'; + config.endpoint = 'https://www.example.com'; + config.headers = new Map{ + 'Content-Type' => 'application/json', + 'Accept' => '*/*' + }; + config.body = '{"test": "hello"}'; + config.callbackClass = 'HttpClient'; + + MockHttpResponse responder = new MockHttpResponse(); + Test.setMock(HttpCalloutMock.class, responder); + + Test.startTest(); + HttpClient client = new HttpClient(); + client.doAsyncCallout(new HttpClient.JSONConfigParser().toJson(config)); + Test.stopTest(); + + Assert.isTrue(responder.hasResponded); + Assert.isTrue(mockCallback.called); + } + + @IsTest + static void testRetry() { + HttpRequest request = new HttpRequest(); + request.setMethod('GET'); + request.setEndpoint('https://www.example.com'); + + MockHttpResponse responder = new MockHttpResponse(); + Test.setMock(HttpCalloutMock.class, responder); + + HttpClient client = new HttpClient(); + Test.startTest(); + client.retry(request); + Test.stopTest(); + + Assert.areEqual( + responder.inputRequest.getMethod(), + request.getMethod() + ); + Assert.areEqual( + responder.inputRequest.getEndpoint(), + request.getEndpoint() + ); + } +} diff --git a/src/apex/utils/classes/tests/HttpClientTest.cls-meta.xml b/src/apex/utils/classes/tests/HttpClientTest.cls-meta.xml new file mode 100644 index 0000000..800ee42 --- /dev/null +++ b/src/apex/utils/classes/tests/HttpClientTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/src/apex/utils/classes/tests/QueryBuilderTest.cls b/src/apex/utils/classes/tests/QueryBuilderTest.cls index 790cf46..54ad142 100644 --- a/src/apex/utils/classes/tests/QueryBuilderTest.cls +++ b/src/apex/utils/classes/tests/QueryBuilderTest.cls @@ -65,4 +65,27 @@ public class QueryBuilderTest { qb.toString() ); } + + @IsTest + static void testMultipleFilters() { + List cities = new List{ 'Scranton', 'New York' }; + String name = 'Dunder Mifflin'; + + QueryBuilder qb = new QueryBuilder('Account') + .selectFields(new List{ 'Id', 'Name' }) + .addFilter('Name = :name') + .addFilter(new QueryBuilder.Condition('BillingCity').isIn(':cities')) + .sortBy('Name ASC') + .setLimit(10); + + qb.setVariable(':name', name); + qb.setVariable(':cities', cities); + + Assert.areEqual( + 'SELECT Id, Name FROM Account WHERE (Name = :name) AND (BillingCity IN :cities) ORDER BY Name ASC LIMIT 10', + qb.toString() + ); + Assert.areEqual(cities, qb.getVariables().get(':cities')); + Assert.areEqual(name, qb.getVariables().get(':name')); + } } diff --git a/src/apex/utils/classes/tests/SObjectSelectorTest.cls b/src/apex/utils/classes/tests/SObjectSelectorTest.cls new file mode 100644 index 0000000..c526cae --- /dev/null +++ b/src/apex/utils/classes/tests/SObjectSelectorTest.cls @@ -0,0 +1,72 @@ +@IsTest +public class SObjectSelectorTest { + public class TestAccountSelector extends SObjectSelector { + public TestAccountSelector() { + super('Account', new List{ 'Name', 'Id' }); + } + } + + @IsTest + static void testGetWhere() { + List testAccounts = (List) new TestFactory('Account') + .createRecords(3) + .setField( + 'Name', + new TestFactory.StringTemplate('Test Account {{i}}') + ) + .getRecords(); + + insert testAccounts; + + Test.startTest(); + TestAccountSelector selector = new TestAccountSelector(); + List accounts = selector.getWhere('Name', 'Test Account 1'); + Test.stopTest(); + + Assert.areEqual(1, accounts.size()); + } + + @IsTest + static void testGetWhereIdIn() { + TestFactory accountFactory = new TestFactory('Account') + .createRecords(3) + .setField( + 'Name', + new TestFactory.StringTemplate('Test Account {{i}}') + ); + + insert accountFactory.getRecords(); + + Test.startTest(); + TestAccountSelector selector = new TestAccountSelector(); + List accounts = selector.getWhereIdIn( + new Set(accountFactory.getIds()) + ); + Test.stopTest(); + + Assert.areEqual(3, accounts.size()); + } + + @IsTest + static void testGetWhereIn() { + List testAccounts = (List) new TestFactory('Account') + .createRecords(3) + .setField( + 'Name', + new TestFactory.StringTemplate('Test Account {{i}}') + ) + .getRecords(); + + insert testAccounts; + + Test.startTest(); + TestAccountSelector selector = new TestAccountSelector(); + List accounts = selector.getWhereIn( + 'Name', + new List{ 'Test Account 1', 'Test Account 2' } + ); + Test.stopTest(); + + Assert.areEqual(2, accounts.size()); + } +} diff --git a/src/apex/utils/classes/tests/SObjectSelectorTest.cls-meta.xml b/src/apex/utils/classes/tests/SObjectSelectorTest.cls-meta.xml new file mode 100644 index 0000000..800ee42 --- /dev/null +++ b/src/apex/utils/classes/tests/SObjectSelectorTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/src/apex/utils/classes/tests/StringFormatUtilityTest.cls b/src/apex/utils/classes/tests/StringFormatUtilityTest.cls new file mode 100644 index 0000000..b1387b0 --- /dev/null +++ b/src/apex/utils/classes/tests/StringFormatUtilityTest.cls @@ -0,0 +1,128 @@ +@isTest +public with sharing class StringFormatUtilityTest { + @isTest + static void testFormatString() { + String testString = '

This is a formatted string.

'; + + Test.startTest(); + String result = StringFormatUtility.stripHtmlTags(testString); + Test.stopTest(); + + String expected = 'This is a formatted string.'; + System.assertEquals(expected, result); + } + + @isTest + static void testConvertUserName() { + Profile admin = [ + SELECT Id + FROM Profile + WHERE Name = 'System Administrator' + ]; + + User testUser = new User( + IsActive = true, + FirstName = 'Wholesale', + LastName = 'User1', + Username = 'wholesaleUser1@telenor.no', + Email = 'wholesaleUser1@telenor.no', + CommunityNickname = 'wUser1', + Alias = 'wUser1', + TimeZoneSidKey = 'Europe/Paris', + LocaleSidKey = 'no_NO', + EmailEncodingKey = 'UTF-8', + LanguageLocaleKey = 'en_US', + ProfileId = admin.Id, + UserRoleId = null + ); + + insert testUser; + + String nickName = 'wUser1'; + String expected = 'Wholesale User1'; + + Test.startTest(); + String result = StringFormatUtility.convertNickNameToUserName(nickName); + Test.stopTest(); + + System.assertEquals(expected, result); + } + + @isTest + static void testGetInitials() { + String testName = 'John Doe'; + + Test.startTest(); + String result = StringFormatUtility.getInitials(testName); + Test.stopTest(); + + String expected = 'JD'; + System.assertEquals(expected, result); + } + + @isTest + static void testBlankGetInitials() { + String testName = 'John'; + + Test.startTest(); + String result = StringFormatUtility.getInitials(testName); + Test.stopTest(); + + String expected = ''; + System.assertEquals(expected, result); + } + + @IsTest + static void testNameConcat() { + String firstName = 'Michael'; + String lastName = 'Scott'; + + Assert.areEqual( + 'Michael Scott', + StringFormatUtility.nameConcat(firstName, lastName) + ); + Assert.areEqual( + 'Michael', + StringFormatUtility.nameConcat(firstName, null) + ); + Assert.areEqual( + 'Scott', + StringFormatUtility.nameConcat(null, lastName) + ); + } + + @isTest + static void testCamelCaseAllWords() { + Assert.areEqual( + 'Lol_Ol_Q', + StringFormatUtility.camelCaseAllWords('lol_ol_q', '_') + ); + } + + @isTest + static void ifNullReplaceWithEmpty() { + String inputNotNull = '13'; + Assert.areEqual( + inputNotNull, + StringFormatUtility.ifNullReplaceWithEmpty(inputNotNull) + ); + + String inputNull = null; + Assert.areEqual( + '', + StringFormatUtility.ifNullReplaceWithEmpty(inputNull) + ); + } + + @isTest + static void ifNullReplaceWith() { + Assert.areEqual( + 'init', + StringFormatUtility.ifNullReplaceWith(null, 'init') + ); + Assert.areEqual( + 'tested', + StringFormatUtility.ifNullReplaceWith('tested', 'replace') + ); + } +} diff --git a/src/apex/utils/classes/tests/StringFormatUtilityTest.cls-meta.xml b/src/apex/utils/classes/tests/StringFormatUtilityTest.cls-meta.xml new file mode 100644 index 0000000..f5e18fd --- /dev/null +++ b/src/apex/utils/classes/tests/StringFormatUtilityTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/src/apex/utils/classes/tests/TransformerTest.cls b/src/apex/utils/classes/tests/TransformerTest.cls new file mode 100644 index 0000000..e8828b9 --- /dev/null +++ b/src/apex/utils/classes/tests/TransformerTest.cls @@ -0,0 +1,81 @@ +@IsTest +public class TransformerTest { + public class TestTransformable implements ITransformable { + public String name = 'Test'; + public String website = 'http://test.com'; + + public Object transformTo(Type target) { + if (target == Account.class) { + return this.transformToAccount(); + } + + throw new TransformException('Unsupported target type'); + } + + public Account transformToAccount() { + Account acc = new Account(); + acc.Name = this.name; + acc.Website = this.website; + return acc; + } + } + + @IsTest + static void testTransformSimple() { + TestTransformable source = new TestTransformable(); + + Account result = (Account) Transformer.transform(source, Account.class); + + Assert.areEqual('Test', result.Name); + Assert.areEqual('http://test.com', result.Website); + } + + @IsTest + static void testTransformList() { + List source = new List{ + new TestTransformable(), + new TestTransformable() + }; + + List result = (List) Transformer.transform( + source, + Account.class + ); + + Assert.areEqual(2, result.size()); + Assert.areEqual('Test', result[0].Name); + Assert.areEqual('http://test.com', result[0].Website); + } + + @IsTest + static void testPerformance() { + List source = new List(); + for (Integer i = 0; i < 1000; i++) { + source.add(new TransformerTest.TestTransformable()); + } + + DateTime tic = System.now(); + List result = (List) Transformer.transform( + source, + Account.class + ); + DateTime toc = System.now(); + + System.debug( + 'Elapsed time: ' + (toc.getTime() - tic.getTime()) + ' ms' + ); // 454 ms + + DateTime tic2 = System.now(); + List result2 = new List(); + for (TransformerTest.TestTransformable item : source) { + result2.add(item.transformToAccount()); + } + DateTime toc2 = System.now(); + + System.debug( + 'Elapsed time: ' + (toc2.getTime() - tic2.getTime()) + ' ms' + ); // 161 ms + + Assert.areEqual(1000, result.size()); + } +} diff --git a/src/apex/utils/classes/tests/TransformerTest.cls-meta.xml b/src/apex/utils/classes/tests/TransformerTest.cls-meta.xml new file mode 100644 index 0000000..800ee42 --- /dev/null +++ b/src/apex/utils/classes/tests/TransformerTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active +