diff --git a/framework/default/ortoo-core/default/classes/Application.cls b/framework/default/ortoo-core/default/classes/Application.cls index 8d7f4eae9af..43a36a0ac0b 100644 --- a/framework/default/ortoo-core/default/classes/Application.cls +++ b/framework/default/ortoo-core/default/classes/Application.cls @@ -132,9 +132,10 @@ public inherited sharing class Application { } } - /** - * Provides a default mechanism for defining a 'Class Type Configuration', being a mapping between two Classes (or an interface and a class). + * Provides a default mechanism for defining a 'Raw Type Configuration', being a mapping between two String names + * + * Can be used by a factory to map between Class and Sobject, or Class and Class, for example. */ private abstract inherited sharing class RawTypeConfiguration implements TypeConfiguration { @@ -223,7 +224,7 @@ public inherited sharing class Application { /** * Provides a mechanism for configuring and registering the App Logic Configuration */ - private inherited sharing class AppLogicConfiguration extends ClassTypeConfiguration + private inherited sharing class AppLogicConfiguration extends RawTypeConfiguration { public void register() { @@ -234,7 +235,7 @@ public inherited sharing class Application { /** * Provides a mechanism for configuring and registering the Child Record Finders Configuration */ - private inherited sharing class ChildRecordFinderConfiguration extends ClassTypeConfiguration + private inherited sharing class ChildRecordFinderConfiguration extends RawTypeConfiguration { public void register() { @@ -245,7 +246,7 @@ public inherited sharing class Application { /** * Provides a mechanism for configuring and registering the Child Record Finders Configuration */ - private inherited sharing class MessageRendererConfiguration extends ClassTypeConfiguration + private inherited sharing class MessageRendererConfiguration extends RawTypeConfiguration { public void register() { diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SimpleObjectFactory.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SimpleObjectFactory.cls index 6e472a2d96d..2172c8bf3ef 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SimpleObjectFactory.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SimpleObjectFactory.cls @@ -1,6 +1,8 @@ /** * Provides the ability to create object factories that perform a simple Type to Type mapping with no constructor or configuration required. * + * Can be defined using either Type to Type or TypeName to TypeName (Strings) + * * Should not be referenced directly in code, but instead wrapped in another factory that defines the type name and whether the factory * should error when an unmapped type is passed in. * @@ -10,12 +12,27 @@ */ public inherited sharing class ortoo_SimpleObjectFactory // NOPMD: specified a mini-namespace to differentiate from fflib versions { - private Map implementationByType; + private Map implementationByType; + private Map implementationNameByTypeName; private Map mockByType; private String typeName = 'Unknown'; private Boolean errorOnUnmappedType = true; + /** + * Construct an instance using the given type ame to implementation name mapping + * + * @param Map The Type name to Implementation name mapping + */ + public ortoo_SimpleObjectFactory( Map implementationNameByTypeName ) + { + Contract.requires( implementationNameByTypeName != null, 'ortoo_SimpleObjectFactory instantiated with a null implementationNameByTypeName' ); + + this.implementationNameByTypeName = implementationNameByTypeName; + this.implementationByType = new Map(); + this.mockByType = new Map(); + } + /** * Construct an instance using the given type to implementation mapping * @@ -25,6 +42,7 @@ public inherited sharing class ortoo_SimpleObjectFactory // NOPMD: specified { Contract.requires( implementationByType != null, 'ortoo_SimpleObjectFactory instantiated with a null implementationByType' ); + this.implementationNameByTypeName = new Map(); this.implementationByType = implementationByType; this.mockByType = new Map(); } @@ -75,7 +93,9 @@ public inherited sharing class ortoo_SimpleObjectFactory // NOPMD: specified return mockByType.get( requestedType ); } - if ( errorOnUnmappedType && ! implementationByType.containsKey( requestedType ) ) + String requestedTypeName = requestedType.getName(); + + if ( errorOnUnmappedType && ! implementationByType.containsKey( requestedType ) && ! implementationNameByTypeName.containsKey( requestedTypeName ) ) { throw new Exceptions.DeveloperException( 'No implementation registered for ' + typeName + ' ' + requestedType ) .setErrorCode( FrameworkErrorCodes.FACTORY_NO_IMPLEMENTATION_REGISTERED ) @@ -88,6 +108,18 @@ public inherited sharing class ortoo_SimpleObjectFactory // NOPMD: specified { typeToConstruct = implementationByType.get( requestedType ); } + else if ( implementationNameByTypeName.containsKey( requestedTypeName ) ) + { + typeToConstruct = Type.forName( implementationNameByTypeName.get( requestedTypeName ) ); + if ( typeToConstruct == null ) + { + throw new Exceptions.DeveloperException( 'Invalid implementation registered for ' + typeName + ' ' + requestedTypeName + ': The class does not exist, or is not visible' ) + .setErrorCode( FrameworkErrorCodes.FACTORY_INVALID_IMPLEMENTATION_REGISTERED ) + .addContext( 'typeName', typeName ) + .addContext( 'requestedType', requestedType ); + } + implementationByType.put( requestedType, typeToConstruct ); + } Object constructedObject; try diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SimpleObjectFactoryTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SimpleObjectFactoryTest.cls index 432e1ced967..6b884bb69be 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SimpleObjectFactoryTest.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SimpleObjectFactoryTest.cls @@ -1,10 +1,11 @@ @isTest private without sharing class ortoo_SimpleObjectFactoryTest { - private static Map NO_MAPPINGS = new Map(); + private static Map NO_TYPE_MAPPINGS = new Map(); + private static Map NO_STRING_MAPPINGS = new Map(); @isTest - private static void newInstance_whenCalledWithARegisteredType_willReturnTheMappedType() // NOPMD: Test method name format + private static void newInstance_whenCalledWithARegisteredTypeViaType_willReturnTheMappedType() // NOPMD: Test method name format { Map implementationByType = new Map{ Object.class => RegisterableType.class @@ -21,7 +22,24 @@ private without sharing class ortoo_SimpleObjectFactoryTest } @isTest - private static void newInstance_whenCalledWithATypeThatCannotBeConstructed_willThrowAnException() // NOPMD: Test method name format + private static void newInstance_whenCalledWithARegisteredTypeViaString_willReturnTheMappedType() // NOPMD: Test method name format + { + Map implementationNameByTypeName = new Map{ + 'Object' => 'ortoo_SimpleObjectFactoryTest.RegisterableType' + }; + + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( implementationNameByTypeName ); + + Test.startTest(); + Object returnedInstance = factory.newInstance( Object.class ); + Test.stopTest(); + + System.assertNotEquals( null, returnedInstance, 'newInstance, when called with a registered type, will return an instance' ); + System.assert( returnedInstance instanceOf RegisterableType, 'newInstance, when called with a registered type, will return an instance of the mapped type' ); + } + + @isTest + private static void newInstance_whenCalledWithATypeViaTypeThatCannotBeConstructed_throwsException() // NOPMD: Test method name format { Map implementationByType = new Map{ @@ -46,9 +64,73 @@ private without sharing class ortoo_SimpleObjectFactoryTest } @isTest - private static void newInstance_whenSetToAllowUnmappedAndNoMappingsSetUp_willReturnAnInstance() // NOPMD: Test method name format + private static void newInstance_whenCalledWithATypeViaStringThatCannotBeConstructed_throwsException() // NOPMD: Test method name format { - ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_MAPPINGS ); + + Map implementationNameByTypeName = new Map{ + 'Object' => 'ortoo_SimpleObjectFactoryTest.NonConstructableType' + }; + + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( implementationNameByTypeName ); + + Test.startTest(); + String exceptionMessage; + try + { + factory.newInstance( Object.class ); + } + catch ( Exceptions.DeveloperException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + ortoo_Asserts.assertContains( 'NonConstructableType does not have parameterless constructor', exceptionMessage, 'newInstance, when called with a type that cannot be constructed, will throw an exception' ); + } + + @isTest + private static void newInstance_whenCalledWithATypeViaStringThatisInvalid_throwsException() // NOPMD: Test method name format + { + + Map implementationNameByTypeName = new Map{ + 'Object' => 'NotAValidClass' + }; + + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( implementationNameByTypeName ); + + Test.startTest(); + String exceptionMessage; + try + { + factory.newInstance( Object.class ); + } + catch ( Exceptions.DeveloperException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + ortoo_Asserts.assertContains( 'Invalid implementation registered for Unknown Object: The class does not exist, or is not visible', exceptionMessage, 'newInstance, when called with a type that has an invalid type, will throw an exception' ); + } + + @isTest + private static void newInstance_whenSetToAllowUnmappedAndNoMappingsSetUpViaType_willReturnAnInstance() // NOPMD: Test method name format + { + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_TYPE_MAPPINGS ); + + Test.startTest(); + factory.setErrorOnUnmappedType( false ); + Object returnedInstance = factory.newInstance( RegisterableType.class ); + Test.stopTest(); + + System.assertNotEquals( null, returnedInstance, 'newInstance, when set to allow unmapped and no mappings exist, will return an instance' ); + System.assert( returnedInstance instanceOf RegisterableType, 'newInstance, when set to allow unmapped and no mappings exist, will return an instance of the requested type' ); + } + + @isTest + private static void newInstance_whenSetToAllowUnmappedAndNoMappingsSetUpViaString_willReturnAnInstance() // NOPMD: Test method name format + { + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_STRING_MAPPINGS ); Test.startTest(); factory.setErrorOnUnmappedType( false ); @@ -60,7 +142,7 @@ private without sharing class ortoo_SimpleObjectFactoryTest } @isTest - private static void newInstance_whenSetToAllowUnmappedAndCalledWithAnUnmapped_willReturnAnInstance() // NOPMD: Test method name format + private static void newInstance_whenSetToAllowUnmappedAndCalledWithAnUnmappedViaType_willReturnAnInstance() // NOPMD: Test method name format { Map implementationByType = new Map{ Object.class => Object.class @@ -78,9 +160,27 @@ private without sharing class ortoo_SimpleObjectFactoryTest } @isTest - private static void newInstance_whenAMockIsSetOnAnAllowedUnmappedTypeAndNewInstanceCalled_willReturnTheMock() // NOPMD: Test method name format + private static void newInstance_whenSetToAllowUnmappedAndCalledWithAnUnmappedViaString_willReturnAnInstance() // NOPMD: Test method name format + { + Map implementationNameByTypeName = new Map{ + 'Unknown' => 'Unknown' + }; + + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( implementationNameByTypeName ); + + Test.startTest(); + factory.setErrorOnUnmappedType( false ); + Object returnedInstance = factory.newInstance( RegisterableType.class ); + Test.stopTest(); + + System.assertNotEquals( null, returnedInstance, 'newInstance, when set to allow unmapped and then called with an unmapped type, will return an instance' ); + System.assert( returnedInstance instanceOf RegisterableType, 'newInstance, when set to allow unmapped and then called with an unmapped type, will return an instance of the requested type' ); + } + + @isTest + private static void newInstance_whenAMockIsSetOnAnAllowedUnmappedTypeViaTypeAndNewInstanceCalled_willReturnTheMock() // NOPMD: Test method name format { - ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_MAPPINGS ); + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_TYPE_MAPPINGS ); factory.setErrorOnUnmappedType( false ); RegisterableType mockInstance = new RegisterableType( 'mockone' ); @@ -94,9 +194,25 @@ private without sharing class ortoo_SimpleObjectFactoryTest } @isTest - private static void newInstance_whenSetToNotAllowUnmappedAndCalledWithAnUnmapped_willThrowAnException() // NOPMD: Test method name format + private static void newInstance_whenAMockIsSetOnAnAllowedUnmappedTypeViaStringAndNewInstanceCalled_willReturnTheMock() // NOPMD: Test method name format { - ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_MAPPINGS ); + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_STRING_MAPPINGS ); + factory.setErrorOnUnmappedType( false ); + + RegisterableType mockInstance = new RegisterableType( 'mockone' ); + + Test.startTest(); + factory.setMock( RegisterableType.class, mockInstance ); + Object returnedInstance = factory.newInstance( RegisterableType.class ); + Test.stopTest(); + + System.assertEquals( mockInstance, returnedInstance, 'newInstance, when a mock is set on an unmapped type, and newInstance called, will return the Mock' ); + } + + @isTest + private static void newInstance_whenSetToNotAllowUnmappedViaTypeAndCalledWithAnUnmapped_throwsException() // NOPMD: Test method name format + { + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_TYPE_MAPPINGS ); Test.startTest(); @@ -117,9 +233,50 @@ private without sharing class ortoo_SimpleObjectFactoryTest } @isTest - private static void newInstance_whenAMockIsSetOnAnNotAllowedUnmappedTypeAndNewInstanceCalled_willNotThrowAnException() // NOPMD: Test method name format + private static void newInstance_whenSetToNotAllowUnmappedViaStringAndCalledWithAnUnmapped_throwsException() // NOPMD: Test method name format { - ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_MAPPINGS ); + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_STRING_MAPPINGS ); + + Test.startTest(); + + factory.setErrorOnUnmappedType( true ); + + String exceptionMessage; + try + { + factory.newInstance( RegisterableType.class ); + } + catch ( Exceptions.DeveloperException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + ortoo_Asserts.assertContains( 'No implementation registered for', exceptionMessage, 'newInstance, when set to not allow unmapped, and called with an unmapped, will throw an exception' ); + } + + @isTest + private static void newInstance_whenAMockIsSetOnAnNotAllowedUnmappedTypeViaTypeAndNewInstanceCalled_noException() // NOPMD: Test method name format + { + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_TYPE_MAPPINGS ); + RegisterableType mockInstance = new RegisterableType( 'mockone' ); + + Test.startTest(); + + factory.setErrorOnUnmappedType( true ); + factory.setMock( RegisterableType.class, mockInstance ); + + RegisterableType returnedType = (RegisterableType)factory.newInstance( RegisterableType.class ); + + Test.stopTest(); + + System.assertNotEquals( null, returnedType, 'newInstance, when a mock is set on an unmapped type, unmapped types are not allowed, and newInstance called, will not throw an exception' ); + } + + @isTest + private static void newInstance_whenAMockIsSetOnAnNotAllowedUnmappedTypeViaStringAndNewInstanceCalled_noException() // NOPMD: Test method name format + { + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_STRING_MAPPINGS ); RegisterableType mockInstance = new RegisterableType( 'mockone' ); Test.startTest(); @@ -137,7 +294,7 @@ private without sharing class ortoo_SimpleObjectFactoryTest @isTest private static void setTypeName_whenAnErrorOccurs_willSetTheNameOfTheTypeInTheMessage() // NOPMD: Test method name format { - ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_MAPPINGS ); + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( NO_TYPE_MAPPINGS ); Test.startTest(); @@ -159,7 +316,7 @@ private without sharing class ortoo_SimpleObjectFactoryTest } @isTest - private static void setTypeName_whenCalledWithNull_willThrowAnException() // NOPMD: Test method name format + private static void setTypeName_whenCalledWithNull_throwsException() // NOPMD: Test method name format { ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( new Map() ); @@ -179,7 +336,7 @@ private without sharing class ortoo_SimpleObjectFactoryTest } @isTest - private static void newInstance_whenCalledWithNull_willThrowAnException() // NOPMD: Test method name format + private static void newInstance_whenCalledWithNull_throwsException() // NOPMD: Test method name format { ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( new Map() ); @@ -199,7 +356,7 @@ private without sharing class ortoo_SimpleObjectFactoryTest } @isTest - private static void setErrorOnUnmappedType_whenCalledWithNull_willThrowAnException() // NOPMD: Test method name format + private static void setErrorOnUnmappedType_whenCalledWithNull_throwsException() // NOPMD: Test method name format { ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( new Map() ); @@ -219,13 +376,14 @@ private without sharing class ortoo_SimpleObjectFactoryTest } @isTest - private static void constructor_whenCalledWithNull_willThrowAnException() // NOPMD: Test method name format + private static void constructor_Type_whenCalledWithNull_throwsException() // NOPMD: Test method name format { + Map nullMapping = null; Test.startTest(); String exceptionMessage; try { - ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( null ); + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( nullMapping ); } catch ( Contract.RequiresException e ) { @@ -236,7 +394,26 @@ private without sharing class ortoo_SimpleObjectFactoryTest ortoo_Asserts.assertContains( 'ortoo_SimpleObjectFactory instantiated with a null implementationByType', exceptionMessage, 'constructor, when called with null, will throw an exception' ); } - private without sharing class RegisterableType + @isTest + private static void constructor_String_whenCalledWithNull_throwsException() // NOPMD: Test method name format + { + Map nullMapping = null; + Test.startTest(); + String exceptionMessage; + try + { + ortoo_SimpleObjectFactory factory = new ortoo_SimpleObjectFactory( nullMapping ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + ortoo_Asserts.assertContains( 'ortoo_SimpleObjectFactory instantiated with a null implementationNameByTypeName', exceptionMessage, 'constructor with types, when called with null, will throw an exception' ); + } + + public without sharing class RegisterableType { String name; public RegisterableType() @@ -250,7 +427,7 @@ private without sharing class ortoo_SimpleObjectFactoryTest } } - private without sharing class NonConstructableType + public without sharing class NonConstructableType { String name; public NonConstructableType( String name )