Libshare Docs

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 and json
  • 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 as California; 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 of HttpClientRequest which can be later used to configure additional request parameters before sending the request.
  • {httpMethod}Req(most used params): Returns an instance of HttpClientRequest 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