Overview
Libshare is a set of reusable modules inspired by years of developing Salesforce apps for enterprises. It is released as a managed package so that you can use without having to worry about managing the code yourself in the org. However, you are free to just use the code as is.
It is licensed under Apache License 2.0.
Install
Install from links provided here
Modules
This package provides following modules.
- Settings: Reusable application settings store and easy to use API to interact with it
- Fluent Asserts: Fluent asserts inspired of Java worlds Assertj
- Stopwatch: Convenient way to log overall time, keep track of time spent in each method, and limits used in each method
- Json Mapper: Higher level abstraction to read and write json content
- Utils: Assorted time-saving utilities
- Logger: Convenience wrapper for System.debug with support for logger name
- App Logs: Save log statements into a well thought out App Log object with support for storing log content up to 56k chars
- App Tasks: Framework to process for your Queueable Jobs, and keep track of what happens to those tasks.
- Http Client: Unirest inspired easy to use and feature rich Http Client with support for attachments (coming soon), OAuth (coming soon), custom headers, default params, URL and body params etc.,
- Flexible Http Callout Mock: Http Callout Mock to simplify your HTTP related testing
- DbChanges with Chunking: Allows you to easily save changes to records with retry and chunking capability.
Namespace
Libshare Salesforce package namespace is lib
.
Settings
We have seen that folks find various clever ways of achieving things to suit their needs. Before Custom Settings/Custom Metadata, many were using Custom Labels to store application settings. It was very inconvenient and to change something you had to deploy new build again, which broke the premise of dynamic configuration.
With custom settings, Salesforce provided a “standard” way of managing application data. While it is powerful, people started abusing Custom Settings. Each developer ends up creating one custom settings object to store one value for their app, which is hard to maintain and manage.
Settings module is built on top of Custom Settings and tries to provide one place to store all application configuration. It provides an easy to use API to interact with settings.
Features
Here are some of the notable features of Settings module.
- Single consistent and reusable place to all application configuration
- Easy to use API to get/set settings
- Support for storing values longer than 255 bytes, up to 25,500 bytes
- Supports
string
,integer
,decimal
,boolean
,date
,datetime
,list
,map
,set
andjson
- Supports specifying the default value for each setting in the application code.
- Supports identifying some settings as Environment specific and provides api to clear those values when Sandbox is refreshed
Storage
It creates a lib__Settings__c
custom settings object with following fields.
Label | API | Type | Description |
---|---|---|---|
Name | Name | String | Standard name of Custom Settings. Settings key will be stored here |
Type | Type__c | String | String indicating what type of value this setting stores. It should be one of
string
,
integer
,
decimal
,
boolean
,
date
,
DateTime
,
list
,
map
,
set
and
json
. Optional and if blank defaults to
string |
Value | Value__c | String | Stores the actual value of the setting. Note all type of values are persisted as Strings and converted to appropriate type during the retrieval |
Length | Length__c | Integer | Indicates maximum length of the value stored in this setting. Optional. If blank defaults to 255. Can be up to 25,000. If the length is more than 255, then multiple keys will be created with
{basekey}__{n}
where base key application uses and n is multi-part key number. For if you set a setting
AppClientId
with value length of 256 bytes, then two keys will be created. One as
AppClientId
and
AppClientId__1 |
Env Specific | Env_Specific__c | Boolean | If checked indicates that this setting is specific this environment and should not be copied over to other environments. For ex., service URLs, user ids or passwords. This helps to identify settings to be cleared when copied over from Prod to Sandbox. There is also an API that one can execute to clear all these types of settings.
lib.sf.settings.clearnEnvSpecificValues() |
API
All settings apis are accessed via lib.sf.settings
reference. This returns instance of Settings.cls.
This the only API that is exposed in the package. This class provides methods to get and set settings.
Usage
Here is the general outline of three methods for each type of supported types.
//Returns the value for given Key. If Key is not defined or if value is empty, then it throws SettingsException
get{Type}({Key});
//Returns the value defined for Key. If Key is not defined or if value is empty, then returns the DefaultValue
get{Type}({Key}, {DefaultValue});
//Sets the Key to given value. If setting doesn't exist, then creates new and sets it.
set{Type}({Key}, {Value});
In above example, Type could be one of String
, Integer
, Decimal
, Date
, DateTime
, Boolean
, Json
, List
, Map
, Set
Values Longer than 255 bytes
Settings modules support setting and getting values longer than 255 bytes by splitting the value into segments of 255 bytes each and storing them in multiple keys. Each of these additional keys is named as {BaseKey}__{nn}
where BaseKey
is key specified by Users and nn
is a number from 1 till 99.
Limitations
Key maximum size is 38 bytes. If Key needs to support value more than 255 bytes, then Key maximum size 34 bytes.
Env Specific Settings
There are always some settings that are specific to each environment.
An example from prior experience: One of the customers we worked with had stored payment system URLs, credentials. When we refreshed sandbox, values copied over but forgot to update them and end up in posting non-production transactions to production payment system.
Set flag each of such settings in Env_Specific__c
and call API lib.sf.settings.clearnEnvSpecificValues()
when refreshed. You could also make this part of your refresh script so that it always gets cleared when refreshed.
Examples
-
Integer maxValue = lib.sf.settings.getInteger('ProcessingMaxValue', 1000);
In this example, we init the maxValue from settings
ProcessingMaxValue
. If setting not defined, then it defaults to 1000.This pattern of “soft-coding” the settings helps avoid the Settings clutter and yet provides a means to override values in production if the need arises.
-
List<String> states = lib.sf.settings.getList('States');
In this example, we get the list of Strings from Setting
States
. The actual value would be stored asCalifornia; Nevada
with;
as the value separators. The settings code will split the values before converting to List.
Fluent Assertions
We feel that Salesforce is missing good support for good assertions. For example., to check something is not null, we need to call System.assertNotEquals(null, some result)
. While this works, but not very intuitive.
Fluent assertions try to bring easy to use API to assertions in Salesforce.
Features
Here are some of the notable features of Fluent Assertions.
- Typed support for asserting primitives, objects, and exceptions
- Chainable assert methods so value being asserted can be tested for various conditions
- Use custom AssertException vs System default
API
All settings apis are accessed via lib.sf.assert
reference. This returns instance of Assert.
This the only API that is exposed in the package and is the starting point to access all other methods.
Apart from Typed assert classes, lib.sf.assert
also has few apis to support general assertions as follows.
Assert fail();
Assert fail(String message);
Assert check(Boolean result);
check(Boolean result, String message);
Assert expectedException();
Assert expectedException(String msg);
expectedException(System.Type cls);
Usage
You typically call reference lib.sf.assert.that(value)
and depending on the type of {value}, method returns one of StringAssert, IntegerAssert, DecimalAssert, BooleanAssert, DateAssert, DateTimeAssert, ObjectAssert, or ExceptionAssert.
Each of these typed assert classes supports various assert methods of two variants, one without custom message and one with the custom message as follows.
StringAssert endsWith(String other);
StringAssert endsWith(String other, String msg);
Example
Here is an example of using String assert.
@IsTest
public class ExampleTest {
static lib.assert assert = lib.sf.assert;
testmethod public static void test_testValue() {
String value = //get value from some method
assert.that(value)
.isNotNull()
.contains('Customer')
.endsWith('.');
}
testmethod public static void test_testValueWithCustomMessage() {
String value = //get value from some method
assert.that(value)
.isNotNull('Cannot be null')
.contains('Customer', 'Should contain Customer')
.endsWith('.', 'Should end with a dot');
}
}
Stopwatch
The Stopwatch is a convenient class used to measure the resources block of code consumes. It calculates the elapsed time, as well as consumed limits within that block.
For example., this block of code:
lib.Stopwatch sw = new lib.Stopwatch();
List<Account> acts = [select id from account];
List<String> strings = new List<String>{'abc', 'abc', 'abc', 'abc'};
System.debug(sw);
Displays:
elapsedTime=7ms, consumedLimits=[CpuTime=5/10000, HeapSize=1095/6000000, Queries=1/100, QueryRows=12/50000]
It doesn’t log the limits which are not consumed to reduce the logging noise.
Saved Measures
Stopwatch also supports saving the measures in various code blocks. This allows to capture measure against a name and then later log all measures for easy analysis. It also calculates the overall measure which is the sum of all other measures.
For ex., this block of code:
lib.Stopwatch sw = new lib.Stopwatch();
List<Account> acts = [select id from account];
List<String> strings = new List<String>{'abc', 'abc', 'abc', 'abc'};
sw.saveAndReset('first block of code');
acts = [select id from account];
acts = [select id from account];
acts = [select id from account];
acts = [select id from account];
acts = [select id from account];
acts = [select id from account];
strings = new List<String>{'abc', 'abc', 'abc', 'abc',
'abc', 'abc', 'abc', 'abc',
'abc', 'abc', 'abc', 'abc',
'abc', 'abc', 'abc', 'abc',
'abc', 'abc', 'abc', 'abc',
'abc', 'abc', 'abc', 'abc',
'abc', 'abc', 'abc', 'abc',
'abc', 'abc', 'abc', 'abc',
'abc', 'abc', 'abc', 'abc',
'abc', 'abc', 'abc', 'abc'
};
sw.saveAndReset('second block of code');
System.debug(sw.getSavedMeasuresString());
Displays:
name=Overall, elapsedTime=33ms, consumedLimits=[CpuTime=21/10000, HeapSize=1335/6000000, Queries=7/100, QueryRows=84/50000], name=first block of code, elapsedTime=6ms, consumedLimits=[CpuTime=4/10000, HeapSize=1039/6000000, Queries=1/100, QueryRows=12/50000], name=second block of code, elapsedTime=27ms, consumedLimits=[CpuTime=17/10000, HeapSize=296/6000000, Queries=6/100, QueryRows=72/50000]
Json Mapper
Salesforce provides a good support for dealing with Json, with support for serializing/deserializing strongly typed classes and as well as parsing/writing using low-level methods.
If you want to deal with json at a level which is in between Typed Classes and low-level parsing, JsonMapper
is for you. It allows you to read keys without having
to create static types and not having to deal with low-level parsing/writing.
Features
Here is the list of things JsonMapper supports.
- Reading List, Date, DateTime, Integer, Decimal, Boolean types
- Writing all above types, including date/datetimes in ISO format
- Excluding nulls during writing
- Reading and writing into the same object
- Easily serialize into Json and Pretty json the written object
- Supports read/writing using
with
concept so the same prefix need not be repeated
Writing Values
Write simple values as set values.
lib.Utils u = new lib.Utils();
lib.JsonMapper mapper = new lib.JsonMapper();
mapper.set('stringValue', 'bar');
mapper.set('dateValue', u.parseIsoDateTime('2017-01-01'));
mapper.set('datetimeValue', u.parseIsoDateTime('2017-01-01T01:02:03Z'));
mapper.set('booleanValue', true);
mapper.set('intValue', 100);
mapper.set('decimalValue', 199.99);
System.debug(mapper.toJson());
Produces
{"decimalValue":199.99,"intValue":100,"booleanValue":true,"datetimeValue":"2017-01-01T01:02:03Z","dateValue":"2017-01-01T00:00:00Z","stringValue":"bar"}
Skipping Null or Blank Values while writing
While writing, you can opt to not write blank or null values using setIfNotNull
or setIfNotBlank
methods.
lib.Utils u = new lib.Utils();
lib.JsonMapper mapper = new lib.JsonMapper();
mapper.setIfNotNull('nonNullValue', 'bar');
mapper.setIfNotNull('nullValue', null);
mapper.setIfNotBlank('nonBlankValue', 'abc');
mapper.setIfNotBlank('blankValue', ' ');
System.debug(mapper.toJson());
Produces
{"nonBlankValue":"abc","nonNullValue":"bar"}
Writing multi-level keys
It is not required that any required parent object values are written before writing child values. Library creates the needed parent levels before writing child objects.
lib.Utils u = new lib.Utils();
lib.JsonMapper mapper = new lib.JsonMapper();
mapper.setIfNotNull('first.second.third', 'value');
System.debug(mapper.toJson());
Produces
{"first":{"second":{"third":"value"}}}
Writing array values
It supports writing list/array keys at any level even skipping elements.
lib.Utils u = new lib.Utils();
lib.JsonMapper mapper = new lib.JsonMapper();
mapper.setIfNotNull('first[0].second[1].third[2].key', 'value');
System.debug(mapper.toJson());
Produces
{"first":[{"second":[null,{"third":[null,null,{"key":"value"}]}]}]}
Reading Values
Values can be read using get*
methods (get
, getString
, getDate
, getDatetime
, getBoolean
, getInteger
, getDecimal
).
Each of these gets methods also support the second argument as the default value. So if the value is not found for the requested key or if it is blank, then the default will be returned.
Reading Array
Reading array values is same as reading simple keys with array delimier and index. For ex., mapper.getString('account.contacts[0].firstName')
.
With Prefix
If you are dealing lots of keys with a common prefix, it can be simplified using with
methods. They allow you to specify a one or more prefixes, which are automatically applied to all other get/set methods.
For ex.,
lib.Utils u = new lib.Utils();
lib.JsonMapper mapper = new lib.JsonMapper();
mapper.with('account.contacts[0]')
.set('firstName', 'John')
.set('lastName', 'Doe');
System.debug(mapper.toJson());
Produces
{"account":{"contacts":[{"lastName":"Doe","firstName":"John"}]}}
New prefixes can be added to the previous list or it can be cleared using clearWith
.
Pretty Print
By default toJson
produces non-pretty json. You can produce pretty json using toPrettyJson
method.
lib.Utils u = new lib.Utils();
lib.JsonMapper mapper = new lib.JsonMapper();
mapper.with('account.contacts[0]')
.set('firstName', 'John')
.set('lastName', 'Doe');
System.debug(mapper.toPrettyJson());
Produces
{
"account" : {
"contacts" : [ {
"lastName" : "Doe",
"firstName" : "John"
}]
}
}
Utils
There are many utilities that save little time but we use them so often that they can be a significant burden to write each time we need it. They also can reduce the significant noise in your functional code and hence increase the readability.
Libshare utils lib.Utils
provides many such utilities. Each method is documented so you can explore the docs as your edit or you can go through the code.
Logger
We have written System.debug() many times. Did you know that method supports specifying the severity of the log message (albeit wrong method name)? This helps varying the level of logging your custom code produces when you enable logging combined with appropriate log levels.
Salesforce supports many log levels including error
, warn
and info
. However, if you want to use one of those levels, you need to write below ugly lines each time you want to use it
System.debug(LoggingLevel.INFO, 'Info level');
Logger
is a simple wrapper around this construct with an ability to set logger name. So you can know where a particular log statement got printed.
Usage as follows.
lib.Logger log = new lib.Logger('ExpiryDaysLogic');
log.debug('This is debug message');
log.info('This is info message');
log.warn('This is warn message');
log.error('This is error message');
Produces
19:08:43.76 (113007849)|USER_DEBUG|[96]|DEBUG|ExpiryDaysLogic: This is debug message
19:08:43.76 (114948670)|USER_DEBUG|[96]|INFO|ExpiryDaysLogic: This is info message
19:08:43.76 (116910526)|USER_DEBUG|[96]|WARN|ExpiryDaysLogic: This is warn message
19:08:43.76 (118190399)|USER_DEBUG|[96]|ERROR|ExpiryDaysLogic: This is error message
Notice the logging level and also logger name.
Flexible Http Callout Mock
The CalloutMock feature released in Winter 13 has really risen the bar and helps to test the Http calls related functionality.
However, creating an implementation of HttpCalloutMock
is not an easy task. Sometimes it takes more time to write that than writing the actual code.
Libshare FlexHttpCalloutMock
provides a flexible way to configure mock. It allows you to configure a list of HttpRequestMatcher
and HttpResponseProvider
and flex mock will figure out which one to return based on the matching.
Concepts
Flexible callout mock is designed around things.
- HttpRequestMatcher: Provides the logic to determine if a particular incoming request is a match or not
- HttpResponseProvider: Provides the logic to construct the appropriate response
- DefaultResponse: Is default response that needs to be returned if no match is found or if no config is configured.
- FlexHttpCalloutMock: Main class which integrates concepts to provide the callout mock functionality
HttpRequestMatcher
It is an interface with one method Boolean isMatches(HttpRequestMatcherRequest req)
that implementation needs to implement. It should check if given http request is a match or not. It is up to the implementation to use any request fields to check the match.
Library provides an implementation of this interface via FlexHttpRequestMatcher
that can be used to pretty much match any combination of request attributes.
HttpResponseProvider
It is an interface with one method HttpResponse getResponse(HttpResponseProviderRequest req)
that implementation needs to implement. Implementation should construct the appropriate response and return.
Library provides an implementation of this interface via FlexHttpResponseProvider
.
FlexHttpCalloutMock
This is the main workhorse of the Flex callout framework. This is the class that you would interact with most often. This class provides lots of convenience methods that automatically configures other needed classes. So you will deal with other classes only if you need to override or provide functionality which is not possible to pre-built methods.
Default Response
Default Response feature is used to return same response to all requests or to return a response when there are no other matches.
Here is an example showing most basic usage.
new FlexHttpCalloutMock()
.withDefaultResponse('Some Response')
.setMock();
This will set the mock with default response such that it will return status code 200 (default if not specified) and Some Response
as the body with no headers.
You can set status code for default response as below
new FlexHttpCalloutMock()
.withDefaultResponse(403, 'Some Response')
.setMock();
You can also set HttpResponse
instance (so you can set other attributes) as the default response.
HttpResponse httpResp = new HttpResponse();
httpResp.setHeader('Content-Type', 'application/json');
httpResp.setStatusCode(200);
new FlexHttpCalloutMock()
.withDefaultResponse(httpResp)
.setMock();
Pre-configured Conditional matching Methods
FlexHttpCalloutMock
provides a bunch of if{attribute}{operator}Return
methods which allows you to specify conditional responses.
new FlexHttpCalloutMock()
.ifUrlEqualsReturn('https://www.datasert.com', 'Datasert Response')
.ifUrlEqualsReturn('https://www.google.com', 'Google Response')
.setMock();
new FlexHttpCalloutMock()
.ifUrlContainsReturn('datasert', 'Datasert Response')
.ifUrlContainsReturn('google', 'Google Response')
.setMock();
new FlexHttpCalloutMock()
.ifBodyContainsReturn('datasert', 403, 'Datasert Response')
.ifBodyContainsReturn('google', 'Google Response')
.setMock();
Each of these methods provides two variants to set responses. With just body or with status code and body. If you think it is worthwhile to add new convenience methods with other conditions, please create an issue.
Here is the list of convenience methods at this time.
- ifUrlEqualsReturn
- ifUrlEqualsIgnoreCaseReturn
- ifUrlContainsReturn
- ifUrlContainsIgnoreCaseReturn
- ifUrlEndsWithReturn
- ifUrlEndsWithIgnoreCaseReturn
- ifUrlStartsWithReturn
- ifUrlStartsWithIgnoreCaseReturn
- ifUrlEqualsCountReturn
- ifBodyContainsReturn
- ifBodyEqualsReturn
- ifBodyStartsWithReturn
- ifBodyEndsWithReturn
Extended Config Conditional Responses
If any of the pre-defined conditional methods are not enough, then you can configure FlexHttpRequestMatcher
to suit your needs.
This class provides a mechanism to add one or more match config consisting of {request attribute}
, {operator}
and {value}
. If you specify more than one match config, then all of the conditions need to be satisfied for a request to match.
For ex., if you are looking to match a request if url ends with Controller
, contains header Content-Type=text/plain
and method POST
, here is that config.
FlexHttpRequestMatcher matcher = new FlexHttpRequestMatcher()
.match('url', 'endsWith', 'Controller')
.match('header:Content-Type', 'equals', 'text/plain')
.match('method', 'equals', 'POST');
new FlexHttpCalloutMock()
.withConfig(matcher, new FlexHttpResponseProvider(200, 'Response body'))
.setMock();
Custom Matcher and Provider
If for whatever reason, FlexHttpRequestMatcher
doesn’t satisfy your needs, you can completely implement a custom class which implements HttpRequestMatcher
interface and set that new config.
new FlexHttpCalloutMock()
.withConfig(new CustomMatcher(), new FlexHttpResponseProvider(200, 'Response body'))
.setMock();
Simulating Error Retries
Sometimes we want to test the error retry logic in the code. Error retry is when code checks for specific error conditions and retry the logic. In such cases, just matching on request attributes would not suffice as all requests are expected to have same attributes.
FlexHttpRequestMatcher provides a mechanism of Minimum Requests and Max Requests boundary within which request is matched, otherwise not.
In below example, first two requests will return 400 error and thrid call would return success response.
FlexHttpRequestMatcher matcher = new FlexHttpRequestMatcher()
.match('url', 'endsWith', 'Controller', 3, null);
new FlexHttpCalloutMock()
.withConfig(matcher, new FlexHttpResponseProvider(200, 'Response body'))
.withDefaultResponse(400, 'Some error')
.setMock();
Clear Config
Once you create an instance of FlexHttpCalloutMock
, you can reuse that instance using clear()
method which will remove all previous configs including default response.
If you just want to remove configs except default response, then you can call clearConfigs()
.
Verification of calls
Returning the expected response is one aspect but making sure that service made an appropriate number of calls and content of the call is important.
Mock facilitates this by providing access all calls using method getCalloutCalls()
which returns a list of CalloutCall
objects.
App Logs
In most of the places I have worked, we have had an object which stores the application logs. Even though object to store the logs can be easily created, storing them and adding additional enhancements to it, is not an easy matter.
Libshare provides a mechanism to store such logs, and class to conveniently log the information, including support for storing logs much more than what is allowed by Salesforce.
Object
It creates an object App_Log__c
to store all information. It has following fields.
Label | API | Type | Description |
---|---|---|---|
App Log# | Name | AutoNumber (APL-{0000000000}) | Auto number to generate an incremental number for each record. It configured such that it can be incremented upto 10 billion records before reverting to 1 |
Modue | lib__Module__c |
Text (100) | Indicates the module which generated this log |
Action | lib__Action__c |
Text (100) | Indicates the action which generated this log |
App Task | lib__App_Task_Id__c |
Lookup (App Task) | Indicates the application task whose processing generated this log |
Details1 | lib__Details1__c |
Long Text (131072) | Details1-5 are used to store the application logs. Since salesforce allows maximum 131072 bytes, multiple fields are created so additional log can be split. |
Details2 | lib__Details2__c |
Long Text (131072) | |
Details3 | lib__Details3__c |
Long Text (131072) | |
Details4 | lib__Details4__c |
Long Text (131072) | |
Details5 | lib__Details5__c |
Long Text (131072) | |
External Id | lib__External_Id__c |
Text (255) | Any external id as per the processing logic |
Message | lib__Message__c |
Text (255) | Any short message that business logic can add to log |
Record Id | lib__Record_Id__c |
Text (18) | If this app log is generated during processing of a particular record, save that record id here |
Sobject | lib__Sobject__c |
Text (100) | Sobject of the record id. This should be populated if record id is populated |
Type | lib__Type__c |
Picklist (Error, Debug) | Indicates the type of this log. Defaults to Debug. Set this to Error if processing failed at the end. |
Value1 | lib__Value1__c |
Text(255) | Value1-5 are provided to store any application specific data elements |
Value2 | lib__Value2__c |
Text(255) | |
Value3 | lib__Value3__c |
Text(255) | |
Value4 | lib__Value4__c |
Text(255) | |
Value5 | lib__Value5__c |
Text(255) |
App Log Builder
Application logic can directly insert into this object without having to use any other library classes. However, using the builder helps populate some debug information, populate the sobject based on record id, extract the stacktrace etc.,
Here is an example of using app log builder.
lib.AppLogBuilder appLog = lib.sf.newAppLog()
.module('SyncProcess')
.action('SyncAccount');
try {
appLog.log('Processing account1');
appLog.log('Updating contacts');
throw new LibshareException('Cannot connect to service');
} catch (Exception e) {
appLog.log('Exception while processing the request', e);
}
appLog.save();
Creates a App_Log__c record with following details
Id,Name,lib__Action__c,lib__Details1__c,lib__Message__c,lib__Module__c,lib__Type__c,SystemModstamp
a011N00000Yi6ojQAB,APL-0000000030,SyncAccount,"2018-01-25T18:54:48Z Processing account1
2018-01-25T18:54:48Z Updating contacts
2018-01-25T18:54:48Z Exception: Exception while processing the request
Stacktrace: lib.LibshareException: Cannot connect to service
(lib)",Exception while processing the request,SyncProcess,Error,2018-01-25T18:54:48Z
Log size
The appLog.log()
can be used to continuously log all the details you want to log. While saving, the log details will be split into chunks of 130000 bytes and stored in each of Details1-5
fields. If log size exceeds before those 5 fields, will be ignored.
Stashing Logs
In Salesforce you cannot make a call out call (Rest api or Soap api) once you have made any database change like insert/update/delete a record. When you call appLog.save()
, the record will be inserted which would cause issues if you were to make calls after calling that method.
To facilitate such scenarios, the library provides stashing via method appLog.stash()
. When you call this, a log will be kept in memory.
Once you did all your processing, you can save stashed logs using lib.sf.appLogger.saveStashed()
, which will insert all stashed logs and then clear the memory.
Short cut way to log error
While the example above is easy to populate and insert the logs, it is still many lines. If you are looking to just create a log when an exception happens without having to log any other information, you can use shorthand versions of app log function as below.
try {
throw new LibshareException('Cannot connect to service');
} catch (Exception e) {
lib.sf.appLogger.save('Module Name', 'Action Name', 'Exception while processing the request', e);
}
Setting your Custom Fields
Since App_Log__c
is a custom object, you can add your own fields to capture your application-specific values. In such cases, you can set those values before saving the app log as follows.
lib.sf.newAppLog()
.module('SyncProcess')
.action('SyncAccount');
.set('Your_Custom_Field__c', 'some value')
.save();
If you prefer to use typed fields to ensure validation, you can get app log record and access its fields as below.
lib.AppLogBuilder appLog = lib.sf.newAppLog()
.module('SyncProcess')
.action('SyncAccount');
appLog.getLogRecord().Your_Custom_Field__c = 'some value';
appLog.save();
App Tasks
We all do background jobs to process some work. Many times, we don’t know what happened to those tasks. Even if we know something failed by looking into AsyncApexJob
object, it doesn’t give enough information about which task failed.
App Task is designed to handle this scenario. It is an object and set of framework classes which facilitates identifying individual background job.
Http Client
Http client module helps to deal with http interaction in a fluent and convenient way. It provides a mechanism for configuring all aspects of request, and read the response into various formats.
Http module is implemeted using classes HttpClient
, HttpClientRequest
, HttpClientResponse
and HttpClientExecutor
Simplified Usage
Utils
class provides simplified access to http module without much configuration or setup. This way of interacting is as easy it can get to interact with http service. However keep in mind that if you want to configure other aspects like header or query parameters, then you will have to use another approach as highlighted in below sections.
lib.Utils u = lib.sf.utils;
u.httpGet('https://test.com');
u.httpGet('https://test.com', ResponseBean.class);
u.httpPost('https://test.com', 'Body');
u.httpPost('https://test.com', 'Body', ResponseBean.class);
u.httpPatch('https://test.com', 'Body');
u.httpPatch('https://test.com', 'Body', ResponseBean.class);
u.httpPut('https://test.com', 'Body');
u.httpPut('https://test.com', 'Body', ResponseBean.class);
u.httpDelete('https://test.com');
u.httpDelete('https://test.com', ResponseBean.class);
HttpClient Class
If you look into those utility methods, they merely expose few of methods in HttpClient
class for convenience at the cost of additional configuration.
HttpClient class provides complete access to interact with your http resources including setting defaults, customize requests, configure timeouts etc.,
To use HttpClient
, you create an instance of it and invoke one of the appropriate http method
methods. In general, it provides the type of methods for each of http method
.
Setting defaults
By instantiating an instance of HttpClient
, you get the advantage of setting some defaults which will be applied to all http alls that made. How default kicks in depends on each of the http request attributes.
lib.HttpClient client = new lib.HttpClient();
client.defaults()
.url('https://www.datasert.com')
.header('X-App-Name', 'TestApp')
.authzBearer('Token');
client.get('/path'); //request will be sent to https://www.datasert.com/path with authorization token and X-App-Name header
You can use any of the variables in the HttpClientRequest
to set defaults including body
. How defaults work with each request values depends on attributes.
Request Att | Description |
---|---|
url |
Default url will be prefixed with actual request url, if request url doesn’t start with http or https or callout:. |
All others | Will be applied to actual request only if such value is not specified in the request and they set as whole |
Calling http methods
Once you have initialized HttpClient
, you can use it to call various http methods as below. Each http method provides four variants.
{httpMethod}Req()
: Returns instance ofHttpClientRequest
which can be later used to configure additional request parameters before sending the request.{httpMethod}Req(most used params)
: Returns an instance ofHttpClientRequest
with most used parameters set, which can be later used to configure additional request parameters before sending the request.{httpMethod}(most used params)
: Invokes the request http method call and returns the response body as String.{httpMethod}(most used params, System.Type)
: Invokes the request http method call and returns the response body json deserialized to specified class type.
Examples:
//returns html text from datasert.com webpage
client.get('https://www.datasert.com');
//Gets the response, json deserializes into specified bean and returns the bean
client.get('https://api.test.com', ResponseBean.class);
//Gets the Get Request, configures additional parameters, sends and returns HttpClientResponse object
client.getReq('https://api.test.com')
.compressed(false)
.readTimeout(1000)
.send();
//Instead of send(), you can use
//sendToString() => to get string
//sendToBlob() => to get blob
//sendToObject() => to deserialize into json object
//sendToJsonMapper() => to return JsonMapper instance of response body
Reading Response Attributes
If you are looking for reading additional response attributes than just getting the body, you can use {httpMethod}Req
and send()
method which returns an instance of HttpClientResposne
. It gives access all aspects of http response.
//Gets the Get Request, configures additional parameters, sends and returns HttpClientResponse object
lib.HttpClientResponse resp = client.getReq('https://api.test.com')
.compressed(false)
.readTimeout(1000)
.send();
//Returns status code
resp.statusCode();
//Returns the content-type header value
resp.header('Content-Type');
//Returns all headers as map
resp.headers();
//Methods to get body as various values
resp.body();
resp.bodyAsBlob();
resp.bodyAsDocument();
resp.bodyAsJsonMapper();
//Methods to check status
resp.isStatus(204);
resp.isStatus2xx();
resp.isStatus3xx();
resp.isStatus4xx();
resp.isStatus5xx();
Handling Exceptions
If http request results in non-successful response (status code >= 300), then HttpException
will be thrown. That exception object will have access to both HttpClientRequest
and HttpClientResponse
so you can use response attributes to handle the needed logic.
try {
client.httpDelete('/sobjects/sobjectid');
} catch (HttpException e) {
if (e.isStatus(404)) {
//ignore
} else {
throw e;
}
}
Request Retrys
You can enable (either by default or per request level) to retry the request and set a number of retries (defaults to 3). Then, the request will be retried if the library receives CalloutException
.
client.deleteReq('/sobjects/sobjectid')
.retry(true)
.retryAttempts(5)
.send();
Extending HttpClient
If you want to build custom logic before and after the execution to alter the request/responses, you need to extend HttpClientExecutor
and override one of the methods. Look into the code to know more about what each method does.
Release Notes
Version 1.2
- Added Stopwatch
- Added FlexHttpCalloutMock
- Added HttpClient
- Added Logger
- Added AppLogger
- Added App Task
- Added Utils
- Added JsonMapper
Version 1.1
- Optimize settings for single value as that is most often used
Version 1.0
- Initial version of package with Settings and Fluent Assertions