All Projects → SCWells72 → Sirono Common

SCWells72 / Sirono Common

Licence: apache-2.0
Common Apex utility classes and frameworks used by Sirono products

Labels

Projects that are alternatives of or similar to Sirono Common

Streams
Durable event pipelines.
Stars: ✭ 51 (-41.38%)
Mutual labels:  apex
Rflib
Salesforce open source library with logging framework, trigger framework, feature switches, and advanced monitoring capabilities
Stars: ✭ 69 (-20.69%)
Mutual labels:  apex
Processbuilderblocks
apex invocable methods for use in Process Builder
Stars: ✭ 77 (-11.49%)
Mutual labels:  apex
Wsdl2apex
Stars: ✭ 54 (-37.93%)
Mutual labels:  apex
Lwc Recipes
A collection of easy-to-digest code examples for Lightning Web Components on Salesforce Platform
Stars: ✭ 1,147 (+1218.39%)
Mutual labels:  apex
Apex Dml Manager
Enforces CRUD/FLS in the least disruptive way possible
Stars: ✭ 70 (-19.54%)
Mutual labels:  apex
Dg Net
Joint Discriminative and Generative Learning for Person Re-identification. CVPR'19 (Oral)
Stars: ✭ 1,042 (+1097.7%)
Mutual labels:  apex
Batch Entry For Salesforce.com
jQuery-based quick entry screen that works with any object. Highly configurable and flexible.
Stars: ✭ 82 (-5.75%)
Mutual labels:  apex
Apexunit
ApexUnit is a powerful continuous integration tool for the Force.com platform
Stars: ✭ 69 (-20.69%)
Mutual labels:  apex
Visualforce Typeahead
A flexible typeahead component for use on Visualforce pages. Uses the typeahead.js library from Twitter.
Stars: ✭ 73 (-16.09%)
Mutual labels:  apex
Sobject Remote
JavaScript library to simplify CRUD DML operations with JavaScript Remoting on the force.com platform.
Stars: ✭ 57 (-34.48%)
Mutual labels:  apex
Purealoe
Salesforce Sample App part of the sample gallery. Agriculture and retail use case. Get inspired and learn best practices.
Stars: ✭ 65 (-25.29%)
Mutual labels:  apex
Sfdc Convert Attachments To Chatter Files
📎 Easily migrate your Attachments to Salesforce Files.
Stars: ✭ 72 (-17.24%)
Mutual labels:  apex
Apex Legends Internal
Simple Apex Legends esp source
Stars: ✭ 53 (-39.08%)
Mutual labels:  apex
Query.apex
A dynamic SOQL and SOSL query builder on Salesforce.com platform
Stars: ✭ 78 (-10.34%)
Mutual labels:  apex
Stripeforce
Stripe API Client Library for Force.com
Stars: ✭ 50 (-42.53%)
Mutual labels:  apex
Cinnamon
Cinnamon is a Force.com app that enables you to build and run Selenium tests to validate custom UI pages with Visualforce/Javascript in your Salesforce org.
Stars: ✭ 70 (-19.54%)
Mutual labels:  apex
Dialogue Generation
Generating responses with pretrained XLNet and GPT-2 in PyTorch.
Stars: ✭ 86 (-1.15%)
Mutual labels:  apex
Apextestkit
A way to simplify your Salesforce data creation.
Stars: ✭ 80 (-8.05%)
Mutual labels:  apex
Connectapihelper
Helper class that makes it easier to post Chatter @-mentions, rich text, and inline images with Apex code.
Stars: ✭ 72 (-17.24%)
Mutual labels:  apex

sirono-common

Common Apex utility classes and frameworks used by Sirono products including:

Trigger handler framework

The class library includes a simple, lightweight framework for handling DML events in accordance with documented best practices. The primary interfaces and classes in the framework are:

  • TriggerHandler - the common interface for all trigger handlers
  • AbstractTriggerHandler - an abstract base implementation of TriggerHandler that provides default no-op implementations of all trigger event handler methods and common utility methods for processing trigger data.
  • TriggerHandlerFactory - the factory interface for the creation of TriggerHandler implementations in response to DML events.
  • TriggerHandlerDispatcher - the "glue" between the trigger and the trigger handler that instantiates the trigger handler using the specified factory and invokes the appropriate method based on the originating DML event.

Example

A typical trigger handler implementation would follow the pattern:

Trigger

trigger ContactTrigger on Contact (before insert, before update, ...) {
    TriggerHandlerDispatcher.dispatch(ContactTriggerHandler.Factory.class);
}

Trigger handler

public with sharing class ContactTriggerHandler extends AbstractTriggerHandler {
    // Trigger handler factory as inner class to avoid top-level namespace pollution
    public class Factory implements TriggerHandlerFactory {
        public TriggerHandler create(List<SObject> objects, Map<Id, SObject> oldObjectsById) {
            return new ContactTriggerHandler(objects, oldObjectsById);
        }
    }

    // Strongly-typed collections of new and old objects initialized by the trigger handler 
    // constructor when invoked by the factory
    private final List<Contact> contacts;
    private final Map<Id, Contact> oldContactsById;

    // Private constructor that is only ever invoked by the factory in response to a DML event
    private ContactTriggerHandler(List<SObject> contacts, Map<Id, SObject> oldContactsById) {
        this.contacts = contacts;
        this.oldContactsById = (Map<Id, Contact>) oldContactsById;
    }

    // Event/timing handler implementations corresponding to the trigger from which the handler was invoked

    public override void beforeInsert() {
        for (Contact newContact : contacts) {
            // Do something interesting
        }
    }

    public override void beforeUpdate() {
        for (Contact newContact : contacts) {
            Contact oldContact = oldContactsById.get(newContact.Id);
            if (fieldValueChanged(oldContact, newContact, Contact.Birthdate)) {
                // Do something interesting
            }
        }
    }

    ...
}

MultiMap Collection and Collection Utilities

MultiMap

The three standard Apex collection types--List, Set, and Map--are exceptionally useful. However, sometimes more complex in-memory data storage mechanisms are required. One of the most useful non-standard collections is a map with multiple values per-key. This can be easily modeled using a standard map with a List or Set of some other type as its value type, e.g.:

// Populate the multi-valued map
Map<Id, List<Contact>> contactsByAccountId = new Map<Id, List<Contact>>();
for (Contact contact : [SELECT Id, AccountId FROM Contact]) {
    Id accountId = contact.AccountId;
    
    // See if there's already a list of contacts for this account ID
    List<Contact> contacts = contactsByAccountId.get(accountId);

    // If not, create one and add it to the map
    if (contacts == null) {
        contacts = new List<Contact>();
        contactsByAccountId.put(accountId, contacts);
    }

    // And finally add the contact to the correct bucket
    contacts.add(contact);
}

// And now work with the map's contents
for (Id accountId : contactsByAccountId.keySet()) {
    for (Contact contact : contactsByAccountId.get(accountId)) {
        // Do something
    }
}

However, this is quite a bit of boilerplate code that's required every time this pattern is needed. The MultiMap collection type exists for exactly this reason. It is by definition a multi-valued map data structure.

The class library includes an Apex implementation of MultiMap. Unlike the standard collection types, it does not support type parameterization. However, it is still quite simple to extract both keys and values from the map and work with them in a strongly-typed manner. Here is the same example displayed above but using a MultiMap:

// Populate the multi-valued map
MultiMap contactsByAccountId = MultiMap.newSetInstance();
for (Contact contact : [SELECT Id, AccountId FROM Contact]) {
    contactsByAccountId.putValue(contact.AccountId, contact);
}

// And now work with the map's contents
for (Object key : contactsByAccountId.keySet()) {
    Id accountId = (Id) key;
    for (Object value : contactsByAccountId.getValues(accountId)) {
        Contact contact = (Contact) value;
        // Do something
    }
}

As you can see, a small amount of explicit casting is required, but overall the code is more compact, especially during population. It's always safe to iterate the results of MultiMap key and value extractors (keySet(), getValues(), etc.) because they will always return a non-null collection.

Apex MultiMap collections can be created with either List- or Set-based backing storage, and values in the MultiMap share behavioral characteristics with the underlying collection type. Use List-based storage via MultiMap.newListInstance() when you need duplicate values for the same key and/or preservation of value insertion order is important. Use Set-based storage via MultiMap.newSetInstance() when you need distinct values and/or value order is unimportant.

Collection Utilities

We often find ourselves doing the same repeated actions when working with collections of data. This includes checking whether a provided collection is non-null and actually has contents, extracting first or last elements from lists (which may be null or empty, so best to check first!), converting a list of SObjects to a list of Ids for use in a SOQL query IN clause, and many other common idioms and patterns.

The class library includes CollectionUtil a set of common utility methods for many of these frequently required collection operations, for example:

  • isEmpty(list) - Checks whether a collection is non-null and contains any elements.
  • isNotEmpty(list) Checks whether a collection is null or empty (technically a negation of isEmpty(), but makes for much more readable code).
  • getFirstItem(list) - Returns the first item from a list if the list is non-null and contains at least one item; otherwise safely returns null.
  • getLastItem(list) - Returns the last item from a list if the list is non-null and contains at least one item; otherwise safely returns null.
  • addIfNotNull(toList, value) - Adds a value to the list only if it's non-null.
  • addAllNotNull(toList, fromList) - Adds all non-null values from one (potentially null) list to another list.
  • toIds(rawIds) - Converts an untyped collection of IDs to a typed collection of IDs. This is particularly useful when used with MultiMap because of the untyped nature of its keys and values, e.g. SELECT Id FROM Account WHERE Id IN :CollectionUtil.toIds(contactsByAccountId.keySet()).
  • toStrings(values) - Converts a collection of arbtrarily typed data into string values. This is particularly useful when used with String.format().
  • toTypedList(sourceValues, targetValues) - Converts an untyped list of values into a typed list of values. This is particularly useful when used with MultiMap because of the untyped nature of its keys and values, e.g. List<Contact> contacts = CollectionUtil.toTypedList(contactsByAccountId.values(), new List<Contact>());
  • getIds(sobjects) - Extracts IDs from a list of SObjects.
  • getIdSet(sobjects) - Extracts distinct IDs from a list of SObjects.
  • mapByIdField(sobjects, field) - Slices the provided list of SObjects by the specified Id field.
  • mapByField(sobjects, field) - Slices the provided list of SObjects by the specified field of any type.
  • multiMapByField(sobjects, field) - Slices the provided list of SObjects by the specified field of any type allowing for duplicate values.
  • sort(objects, comparator) - Uses the specified comparator to sort the provided list of objects. See comparator-based sorting for more details.

Refer to the ApexDoc for more comprehensive and up-to-date documentation.

NOTE: Due to current Apex bugs, many of these utility methods are only available for List collections. This is because Apex currently does not properly support polymorphic assignment of Set or Map collections based on type parameters. We have reported this to Salesforce and hope that it will be addressed in a relatively near-term release. Once it has been addressed the class library will be updated to support all three collection types.

Comparator-based sorting

While Apex includes a Comparable interface that can be implemented by custom classes, it does not include the more decoupled notion of a standalone Comparator that you'd find in other languages/libraries. Comparators are useful for sorting of types which cannot implement Comparable such as SObjects and system Apex classes as well as for performing usage-specific ordering that is different from the standard Comparable implementation for a type.

The class library provides a Comparator interface which can be implemented and supplied to CollectionUtil.sort() along with a list of values to order those values according to the comparator's logic. It also includes a set of standard comparators via the Comparators factory class for ordering lists of primitive types and lists of SObjects by a particular field value. These standard comparators can be configured for the direction of sorting (ascending vs. descending), how null values are handled (nulls first vs. nulls last), and as appropriate, case-sensitivity of string comparisons. The standard comparators can also be used as building blocks for more complex comparators via composition and delegation.

Comparator-based sorting of SObjects is particularly useful when required ordering of a result set cannot be accomplished as part of a SOQL query's ORDER BY clause, for example because the ordering logic is more complex than ORDER BY allows or because the data cannot be ordered as part of the query due to features such as Salesforce Shield Platform Encryption.

Example

Standard SObject field value comparator

// Query contacts from an org where birth date is encrypted and cannot be used in an ORDER BY clause
List<Contact> contacts = [SELECT Id, Birthdate FROM Contact];
// Sort the contacts by birth date descending with null birth dates at the end
CollectionUtil.sort(contacts, Comparators.sobjectFieldValueComparator(Contact.Birthdate).ascending(false).nullsFirst(false));

Standard string comparator

List<Contact> contacts = [SELECT Name FROM Contact];
List<String> contactNames = new List<String>();
for (Contact c : contacts) {
    CollectionUtil.addIfNotNull(contactNames, c.Name);
}
// Sort the contact names ascending case-insensitive
CollectionUtil.sort(contactNames, Comparators.stringComparator().caseSensitive(false));

Custom comparator

public with sharing class OpportunityDateComparator implements Comparator {
    // Initialize a delegate that can be used to sort by the respective date values in descending order
    private static final Comparator DATE_COMPARATOR = Comparators.dateComparator().ascending(false);

    public Integer compare(Object value1, Object value2) {
        Opportunity opportunity1 = (Opportunity) value1;
        Opportunity opportunity2 = (Opportunity) value2;
        
        // NOTE: Null checks omitted here for brevity, but generally you'd want to check any values that are not
        // guaranteed to be non-null for null values. You could also extend Comparators.ConfigurableComparator
        // and use its native ability to perform null checks.
        
        // Primarily order by close date if one or both opportunities have been closed
        Date closeDate1 = opportunity1.CloseDate;
        Date closeDate2 = opportunity2.CloseDate;
        if ((closeDate1 != null) && (closeDate2 == null)) {
            return -1;
        } else if ((closeDate1 == null) && (closeDate2 != null)) {
            return 1;
        } else if ((closeDate1 != null) && (closeDate2 != null)) {
            // Both opportunities have been closed, so sort by close date descending
            return DATE_COMPARATOR.compare(closeDate1, closeDate2);
        }
        
        // Neither is closed, so compare by last activity date if possible
        Date lastActivityDate1 = opportunity1.LastActivityDate;
        Date lastActivityDate2 = opportunity2.LastActivityDate;
        if ((lastActivityDate1 != null) && (lastActivityDate2 == null)) {
            return -1;
        } else if ((lastActivityDate1 == null) && (lastActivityDate2 != null)) {
            return 1;
        } else if ((lastActivityDate1 != null) && (lastActivityDate2 != null)) {
            return DATE_COMPARATOR.compare(lastActivityDate1, lastActivityDate2);
        }
        
        // Worst-case scenario sort by created date
        return DATE_COMPARATOR.compare(opportunity1.CreatedDate, opportunity2.CreatedDate);
    }
}

// Using the custom comparator
List<Opportunity> opportunities = [SELECT Id, CloseDate, LastActivityDate, CreatedDate FROM Opportunity];
CollectionUtil.sort(opportunities, new OpportunityDateComparator());

Authorization utilities

Apex does not automatically enforce authorization. The onus is placed upon the developer to ensure that access to data is properly authorized for the current user. There are three general types of authorization checks which may be performed:

  • CRUD - Whether the user has Create/Read/Update/Delete access to an entire object type.
  • FLS - Whether the user has Field-Level access(/Security) for the distinct fields on an object type.
  • Sharing - Whether the user has access to specific rows for an object type.

Failure to verify authorization properly can lead to security gaps and is a major consideration during the security review process. Salesforce provides documentation on how to address these types of issues, but again, doing so is the responsibility of the developer.

The class library includes a simple authorization utility class, AuthorizationUtil, that currently helps to address the CRUD aspects. It provides both check and assertion methods of verifying the various types of object-level operations that are allowed for the current user. The check methods should be used when an alternative execution path is available if the user is not authorized; the assertion methods should be used when a lack of sufficient authorization should terminate the operation immediately, though recovery is possible through exception handling.

Example

Check methods

// Use an empty list by default
List<Contact> contacts = new List<Contact>();

// If the user is authorized to read contacts and accounts, query the appropriate contacts and their accounts
if (AuthorizationUtil.isAccessible(Contact.SObjectType) && AuthorizationUtil.isAccessible(Account.SObjectType)) {
    contacts = [SELECT Id, Account.Id FROM Contact];

    // If the user is authorized to update contacts, update them
    if (AuthorizationUtil.isUpdateable(Contact.SObjectType)) {
        for (Contact contact : contacts) {
            // Do something interesting to each contact
        }
        update contacts;
    } else {
        // Report the lack of access
    }
} else {
    // Report the lack of access
}

Assert methods

// Exception handling would generally occur in the service or presentation tier and queries/DML in the data access tier,
// but showing both together here to demonstrate how assertion handling might work
try {
    // If the user is authorized to read contacts and accounts, query the appropriate contacts and their accounts
    AuthorizationUtil.assertAccessible(Contact.SObjectType);
    AuthorizationUtil.assertAccessible(Account.SObjectType);
    List<Contact> contacts = [SELECT Id, Account.Id FROM Contact];

    // If the user is authorized to update contacts, update them
    AuthorizationUtil.assertUpdateable(Contact.SObjectType);
    for (Contact contact : contacts) {
        // Do something interesting to each contact
    }
    update contacts;
} catch (AuthorizationException e) {
    // Report the failure
}

Test Assertions

Apex includes three methods for verification of test expectations: System.assert(condition), System.assertEquals(expected, actual), and System.assertNotEquals(expected, actual). You can express pretty much any test expectation using these three methods (in fact, you really only need the first one). For example, if you want to verify that a value is non-null, you can use either assert(value != null) or assertNotEquals(null, value). Similarly, if you want to force a test failure when an expected exception has not been raised, you can use assert(false, 'Expected some exception'). These work, but they're not as expressive as they could be. Many other test frameworks include more extensive sets of assertions. The previous two examples could be expressed as assertNotNull(value) and fail('Expected some exception') respectively. While these are functionally identical, the latter are more explicit in their intent.

The class library includes a test assertion facade, Assert, with methods for the most common types of test assertions (all of the methods also accept an optional message):

  • isTrue(condition) / isFalse(condition)
  • equals(expected, actual) / notEquals(expected, actual)
  • isNull(actual) / isNotNull(actual)
  • fail(message)

Example

In practice test code looks like:

Contact contact = [SELECT Id, FirstName FROM Contact WHERE Name = :expectedName];
Assert.isNotNull(contact.FirstName);
Assert.equals('Me', contact.FirstName);
Assert.isTrue(contact.Name.startsWith('Me'));

Assert also includes higher-level assertions for verifying expected messages (up to the first embedded formatting specifier) from raised exceptions including special handling for object- and field-level errors as a result of failed validation rules or errors added in trigger logic:

  • hasExceptionMessage(expectedMessage, actualException)
  • hasDmlExceptionMessage(expectedField, expectedMessage, actualDmlException) - field-level errors
  • hasDmlExceptionMessage(expectedMessage, actualDmlException) - object-level errors
  • hasPageMessage(expectedField, expectedMessage) - field-level errors added to page messages; required when multiple levels of triggers have fired even if not using Visualforce

Again, in practice this looks like:

try {
    update contact;
    Assert.fail('Expected DmlException here');
} catch (DmlException e) {
    Assert.hasDmlExceptionMessage(Contact.FirstName, Label.Invalid_First_Name, e);
}

Apex Picklist Enums

We often need to refer to known values of picklist fields from Apex. These values are not modeled as symbolic constants, though, so this can lead to a proliferation of hard-coded strings or, at least a bit better, one-off string constants. The class library includes a framework for modeling enum-like data types as wrappers for picklist field values, at least those for whom the candidate values are known at compile-time.

Implementation

In order to create a new picklist enum for a picklist field's known values, create a new subclass of PicklistEnum with the following pattern (using Opportunity.Type as an example):

public with sharing class OpportunityTypeEnum extends PicklistEnum {
    // Model the enum as a private singleton for convenient access from class methods below
    private static final OpportunityTypeEnum INSTANCE = new OpportunityTypeEnum();

    // Initialize the enum values by using valueOf() on each picklist field value (values, not labels)
    public static final Entry EXISTING_CUSTOMER_UPGRADE = valueOf('Existing Customer - Upgrade');
    public static final Entry EXISTING_CUSTOMER_REPLACEMENT = valueOf('Existing Customer - Replacement');
    public static final Entry EXISTING_CUSTOMER_DOWNGRADE = valueOf('Existing Customer - Downgrade');
    public static final Entry NEW_CUSTOMER = valueOf('New Customer');

    // Private constructor for singleton initialized off of the picklist field's SObjectField
    private OpportunityTypeEnum() {
        super(Opportunity.Type);
    }
    
    // The following must be provided in each implementation to provide class-level access to entries based on
    // the type-specific singleton

    public static Entry valueOf(String value) {
        return INSTANCE.getEntry(value);
    }

    public static Entry[] values() {
        return INSTANCE.getEntries();
    }
}

Example

Picklist enum values can then be referenced from Apex as:

Entry[] opportunityTypes = OpportunityTypeEnum.values();
for (Entry opportunityType : opportunityTypes) {
    System.debug('Value = ' + opportunityType.value() + 
                 ', label = ' + opportunityType.label() + 
                 ', active = ' + opportunityType.isActive() +
                 ', defaultValue = ' + opportunityType.isDefaultValue());
}

// You can also perform direct comparisons with string values from SObject instances
Opportunity opp = [SELECT Type FROM Opportunity LIMIT 1];
if (OpportunityTypeEnum.NEW_CUSTOMER.equalTo(opp.Type)) {
    // Do something new customer-ish
}

Best Practices for Picklist Enums

In order to get the best results from picklist enums and the underlying picklist fields, we recommend the following guidelines:

  • Picklist field value naming - Picklist field values should be named the same as the symbolic constant that will be used in Apex. Traditionally it was difficult to provide distinct values and labels for picklist field values, but now it's quite easy. As a result, we would have recommended that the entries for Opportunity.Type above have values like EXISTING_CUSTOMER_UPGRADE, EXISTING_CUSTOMER_REPLACEMENT, EXISTING_CUSTOMER_DOWNGRADE, and NEW_CUSTOMER and labels as shown above. Ultimately what you see as the value in the database should be the exact same as the name of the enum constant used to reference that value from Apex. This won't be possible for existing picklist fields which have already been deployed into production, but it's a good practice for new picklist fields going forward.
  • Picklist enum type naming - The picklist enum type should reflect the SObject type and field as closely as possible within the constraints of Apex type naming (maximum 40 characters). In the example above, the two names are concatenated into a type name with an Enum suffix. Sometimes the type and field names will have overlap, for example, Account.AccountSource. Picklist enum types for such fields should merge overlapping name portions, e.g., AccountSourceEnum.

Apex Type-Safe Enums

Apex supports first-class enum types. However, unlike in other languages, Apex enums are very simple and cannot include information other than the enum constants themselves. There are times when it's desirable to have additional information stored with each enum constant, or to have enum constant values be distinct from enum constant names.

The class library includes a framework for modeling extensible type-safe enums. These should be considered distinct from Apex enums and also from the picklist enums described above. They are more sophisticated than the former and do not represent the known values for a picklist field like the latter. They are particularly useful to provide Apex symbolic constants for the values of string formula fields.

Ordinals are computed automatically for each enum constant based on the order of declaration within the containing type. Additional information can be captured in the concrete type-safe enum implementation as appropriate, e.g., the name of an image to represent the process status, a severity index, etc., and behavior can be extended because the enum is just an Apex class.

Implementation

In order to create a new type-safe enum, create a new subclass of TypeSafeEnum with the following pattern:

public with sharing class ComputedScoreEnum extends TypeSafeEnum {
    // Initialize the enum constants with the distinct string value for each
    public static final ComputedScoreEnum HIGH = new ComputedScoreEnum('HIGH', '/images/stoplight-green.png');
    public static final ComputedScoreEnum MEDIUM = new ComputedScoreEnum('MEDIUM', '/images/stoplight-yellow.png');
    public static final ComputedScoreEnum LOW = new ComputedScoreEnum('LOW', '/images/stoplight-red.png');
    
    // Optionally extend the enum interface with custom properties and behavior
    public final String imagePath { get; private set; }
    
    // Private constructor for singleton initialized using the concrete sub-type and distinct value for that type.
    // Note that the signature minimally needs the value to delegate to the base constructor, but additional state
    // can be gathered for each enum constant as required.
    private ComputedScoreEnum(String value, String imagePath) {
        super(ComputedScoreEnum.class, value);
        this.imagePath = imagePath;
    }

    // The following must be provided in each implementation to provide strongly-typed versions
    // of class method class-level for the enum

    public static ComputedScoreEnum valueOf(String value) {
        return (ComputedScoreEnum) TypeSafeEnum.valueOf(ComputedScoreEnum.class, value);
    }

    public static List<ComputedScoreEnum> values() {
        return (List<ComputedScoreEnum>) TypeSafeEnum.values(ComputedScoreEnum.class, new List<ComputedScoreEnum>());
    }

    public static Boolean matchesAny(ComputedScoreEnum[] values, String testValues) {
        return TypeSafeEnum.matchesAny(values, testValues);
    }

    public static Boolean matchesNone(ComputedScoreEnum[] values, String testValue) {
        return TypeSafeEnum.matchesNone(values, testValue);
    }
}

Again, the inclusion of imagePath is to demonstrate the extensible nature of these enums. If no additional state is required, all references to that property would be removed and the concrete TypeSafeEnum subclass would simply include its enum constants, the private constructor, and the strongly-typed class methods.

Example

Type-safe enum values can then be referenced from Apex as:

List<ComputedScoreEnum> computedScores = ComputedScoreEnum.values();
for (ComputedScoreEnum computedScore : computedScores) {
    System.debug('Value = ' + computedScore.value() + ', ordinal = ' + computedScore.ordinal() + ', image path = ' computedScore.imagePath);
}

// You can also perform direct comparisons with string values
String computedScore = getComputedScore();
if (ProcessStatusEnum.LOW.equalTo(computedScore)) {
    // Handle low scores
}

Logging Wrapper

The primary Apex logging facility is System.debug(). While this is a useful diagnostic tool, the calling interface certainly has its limitations. If you'd like to log at different level than the default (DEBUG), you must include a value for the LoggingLevel enum as the first argument which can lead to unnecessarily verbose logging statements. Additionally, if you'd like to log messages of any complexity, you must either use string concatenation or String.format() to build a more sophisticated message, again muddying the actual message that is being constructed.

For these reasons the class library includes a simple logging wrapper that more closely mimics loggers in other environments. The logging wrapper supports level-specific logging and direct construction of more complex logged messages via String.format() embedded formatting specifiers as call arguments.

Example

public with sharing class ExternalServiceInvoker {

    // Create the logger as a class constant, providing the type name of the containing class
    private static final Logger LOG = Logger.getInstance(ExternalServiceInvoker.class);
    
    public void invokeExternalService(String host, Integer port, String path, String externalId) {
        LOG.info('Invoking service at https://{0}:{1}/{2}?id={3}.', host, port, path, externalId);
        
        try {
            HttpResponse response = // actually invoke the service
            if (response.getStatusCode() != 200) {
                LOG.warn('Failed to invoke the service: status code = {0}, status = {1}', 
                    response.getStatusCode(), response.getStatus());
            }
        } catch (Exception e) {
            LOG.error('Exception while invoking the service: {0}', e);
        }
    }
}

There are some gotchas when using loggers from inner classes. Consult the ApexDoc for Logger for more details and how to work around those issues.

Future Thoughts

The logging wrapper was also designed to have a pluggable back end with System.debug() as the default implementation. In the future we'd like to investigate the use of alternative back ends, in particular some type of remote/federated logging facility. Unfortunately this is complicated by various limitations of the Salesforce platform. It's possible that platform events may provide a good option for remote logging and, if so, once implemented no client should need to change if written against the logging wrapper instead of directly against System.debug().

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].