Skip to content

Commit

Permalink
Merge pull request #2 from OrtooApps/feature/adding-sobject-fabricator
Browse files Browse the repository at this point in the history
Feature/adding sobject fabricator
  • Loading branch information
rob-baillie-ortoo committed Dec 6, 2021
2 parents c7c3ec6 + 287e8c2 commit 0797902
Show file tree
Hide file tree
Showing 19 changed files with 3,795 additions and 2 deletions.
20 changes: 18 additions & 2 deletions TODO.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ Licenses that are needed with the source code and binary:
* fflib - https://github.com/apex-enterprise-patterns/fflib-apex-common/blob/master/LICENSE
* fflib apex extensions - https://github.com/wimvelzeboer/fflib-apex-extensions/blob/main/LICENSE
* Amoss - https://github.com/bobalicious/amoss/blob/main/LICENSE

TODO:
* SObject Fabricator - https://github.com/bobalicious/SObjectFabricator/blob/master/LICENSE

Look at the use of 'MockDatabase' in fflib

Expand Down Expand Up @@ -31,6 +30,23 @@ Add to documentation
* Using the Mock Registarar
* Describe the Application Factories

From Utilities, things that may be useful:
* getReferenceObjectAPIName
* getObjName - get the object name from an Id
* getLabel / getObjectLabel - get the label for an sobject
* getFieldLabel
* delimitedStringToSet and reverse
* escaping single quotes - in both directions?
* unitsBetweenDateTime
* emailAddressIsValid / emailAddressListIsValid
* sObjectIsCustom / sObjectIsCustomfromAPIName
* IsfieldFilterable
* isFieldCustom
* idIsValid
* getCrossObjectAPIName
* objectFieldExist
* sortSelectOptions - complete re-write

Write tests for the SOQL generation in the criteria library

Amoss_Asserts.assertContains improvement into the OS lib
Expand Down
146 changes: 146 additions & 0 deletions framework/default/ortoo-core/default/classes/utils/DirectedGraph.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Directed Graph algorithm ased on the DirectedGraph implemented by Robert Sösemann: https://github.com/rsoesemann/apex-domainbuilder
public inherited sharing class DirectedGraph
{
public inherited sharing class GraphContainsCircularReferenceException extends ortoo_Exception {}
//
// TODO: detect and block circular references. We can deal with them independently
//
private Map<Object,Set<Object>> parentsByChildren = new Map<Object, Set<Object>>();

/**
* Adds a node to the graph
*
* @param Object The node to add
* @return DirectedGraph Itself, allowing for a fluent interface
*/
public DirectedGraph addNode( Object node )
{
if( ! parentsByChildren.containsKey( node ) )
{
parentsByChildren.put( node, new Set<Object>() );
}
return this;
}

/**
* Adds a relationship between two nodes
*
* @param Object The child node of the relationship
* @param Object The parent node of the relationship
* @return DirectedGraph Itself, allowing for a fluent interface
*/
public DirectedGraph addRelationship( Object child, Object parent )
{
Contract.requires( parentsByChildren.containsKey( child ), 'addRelationship called with a child that has not been added as a node (' + child + ')' );
Contract.requires( parentsByChildren.containsKey( parent ), 'addRelationship called with a parent that has not been added as a node (' + parent + ')' );
parentsByChildren.get( child ).add( parent );
return this;
}

/**
* Generates a list of nodes, sorted by their depdencies.
*
* That is, the children first, resolving upwards to the parents.
* No parent appears in the list prior to any of their children.
*
* Algorithm:
* A leaf node is added
* All references to that as a child are removed
* If any parent no longer has any children registered, it is regarded as a leaf node
* Move onto the next leaf node.
*
* Assuming that there are no circular references,
* Eventually, every node will be regarded as a leaf node, and therefore every node will be added
*
* @param Object The child node of the relationship
* @param Object The parent node of the relationship
* @return DirectedGraph Itself, allowing for a fluent interface
*/
public List<Object> generateSorted()
{
List<Object> sortedObjects = new List<Object>();

while( ! leafNodes.isEmpty() )
{
Object currentLeaf = (Object)leafNodes.iterator().next();
leafNodes.remove( currentLeaf );

sortedObjects.add( currentLeaf );

for( Object thisParent : parentsByChildren.get( currentLeaf ) )
{
if ( childCountsByParents.containsKey( thisParent ) )
{
Integer remainingChildrenCount = childCountsByParents.get( thisParent ) - 1;
childCountsByParents.put( thisParent, remainingChildrenCount );

if ( remainingChildrenCount == 0 )
{
leafNodes.add( thisParent );
}
}
}
}

leafNodes = null; // reset the leaf nodes so they will be re-calculated on a subsequent call
childCountsByParents = null; // similar to above

if ( sortedObjects.size() != allNodes.size() )
{
throw new GraphContainsCircularReferenceException( 'The graph contains a circular reference and therefore cannot be resolved.' );
}
return sortedObjects;
}

/**
* A reference to the full list of nodes registered on this graph.
*/
private Set<Object> allNodes
{
get
{
return parentsByChildren.keySet();
}
}

private Set<Object> leafNodes
{
get
{
if ( leafNodes == null )
{
leafNodes = new Set<Object>();
leafNodes.addAll( allNodes );
leafNodes.removeAll( childCountsByParents.keySet() );
}
return leafNodes;
}
set;
}

private Map<Object,Integer> childCountsByParents
{
get
{
if ( childCountsByParents == null )
{
childCountsByParents = new Map<Object,Integer>();

for ( Object thisChild : allNodes )
{
for ( Object parent : parentsByChildren.get( thisChild ) )
{
if ( ! childCountsByParents.containsKey( parent ) )
{
childCountsByParents.put( parent, 0 );
}
childCountsByParents.put( parent, childCountsByParents.get( parent ) + 1 );
}
}

}
return childCountsByParents;
}
set;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexClass>
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
@isTest
private without sharing class DirectedGraphTest
{
@isTest
private static void generateSorted_whenASimpleGraphIsSpecified_willReturnTheNodesInOrder() // NOPMD: Test method name format
{
DirectedGraph graph = new DirectedGraph()
.addNode( 'Great grandparent' )
.addNode( 'Grandparent' )
.addNode( 'Parent' )
.addNode( 'Child' )
.addRelationship( 'Child', 'Parent' )
.addRelationship( 'Parent', 'Grandparent')
.addRelationship( 'Grandparent', 'Great grandparent' );

List<Object> expectedNodes = new List<Object>
{
'Child',
'Parent',
'Grandparent',
'Great grandparent'
};

List<Object> returnedNodes = graph.generateSorted();

System.assertEquals( expectedNodes, returnedNodes, 'generateSorted, when a simple graph has been built, will return the nodes in child to parent order' );
}

@isTest
private static void generateSorted_whenAComplexGraphIsSpecified_willReturnTheNodesInOrder() // NOPMD: Test method name format
{
DirectedGraph graph = new DirectedGraph()
.addNode( 'Great grandparent' )
.addNode( 'Grandparent of both parents' )
.addNode( 'Parent 1' )
.addNode( 'Parent 2' )
.addNode( 'Child 1 of Parent 1' )
.addNode( 'Child 2 of Parent 1' )
.addNode( 'Child 1 of Parent 2' )
.addNode( 'Child 2 of Parent 2' )
.addNode( 'Child of Parents 1 and 2' )

.addRelationship( 'Grandparent of both parents', 'Great grandparent' )
.addRelationship( 'Parent 1', 'Grandparent of both parents' )
.addRelationship( 'Parent 2', 'Grandparent of both parents' )
.addRelationship( 'Child 1 of Parent 1', 'Parent 1' )
.addRelationship( 'Child 2 of Parent 1', 'Parent 1' )
.addRelationship( 'Child 1 of Parent 2', 'Parent 2' )
.addRelationship( 'Child 2 of Parent 2', 'Parent 2' )
.addRelationship( 'Child of Parents 1 and 2', 'Parent 1')
.addRelationship( 'Child of Parents 1 and 2', 'Parent 2' );

List<Object> expectedNodes = new List<Object>
{
'Child 1 of Parent 1',
'Child 2 of Parent 1',
'Child 1 of Parent 2',
'Child 2 of Parent 2',
'Child of Parents 1 and 2',
'Parent 1',
'Parent 2',
'Grandparent of both parents',
'Great grandparent'
};

List<Object> returnedNodes = graph.generateSorted();

System.assertEquals( expectedNodes, returnedNodes, 'generateSorted, when a complex graph has been built, will return the nodes in child to parent order' );
}

@isTest
private static void generateSorted_whenNoGraphIsSpecified_willReturnAnEmptyList() // NOPMD: Test method name format
{
DirectedGraph graph = new DirectedGraph();
List<Object> expectedNodes = new List<Object>();

List<Object> returnedNodes = graph.generateSorted();

System.assertEquals( expectedNodes, returnedNodes, 'generateSorted, when no graph has been built, will return the an empty list' );
}

@isTest
private static void generateSorted_whenADuplicatedNodesAreSpecified_willReturnTheUniqueNodesInOrder() // NOPMD: Test method name format
{
DirectedGraph graph = new DirectedGraph()
.addNode( 'Great grandparent' )
.addNode( 'Great grandparent' )
.addNode( 'Great grandparent' )
.addNode( 'Grandparent' )
.addNode( 'Grandparent' )
.addNode( 'Grandparent' )
.addNode( 'Parent' )
.addNode( 'Parent' )
.addNode( 'Child' )
.addNode( 'Child' )
.addNode( 'Child' )
.addNode( 'Child' )
.addNode( 'Child' )
.addRelationship( 'Child', 'Parent' )
.addRelationship( 'Child', 'Parent' )
.addRelationship( 'Child', 'Parent' )
.addRelationship( 'Child', 'Parent' )
.addRelationship( 'Parent', 'Grandparent')
.addRelationship( 'Parent', 'Grandparent')
.addRelationship( 'Grandparent', 'Great grandparent' );

List<Object> expectedNodes = new List<Object>
{
'Child',
'Parent',
'Grandparent',
'Great grandparent'
};

List<Object> returnedNodes = graph.generateSorted();

System.assertEquals( expectedNodes, returnedNodes, 'generateSorted, when duplicate nodes and relationships are specified, will return the unique nodes in child to parent order' );
}

@isTest
private static void generateSorted_whenACircularReference_willThrowAnException() // NOPMD: Test method name format
{
DirectedGraph graph = new DirectedGraph()
.addNode( 1 )
.addNode( 2 )
.addNode( 3 )
.addRelationship( 1, 2 )
.addRelationship( 2, 3 )
.addRelationship( 3, 1 );
Test.startTest();
String exceptionMessage;
try
{
graph.generateSorted();
}
catch ( Exception e )
{
exceptionMessage = e.getMessage();
}
Test.stopTest();

Amoss_Asserts.assertContains( 'The graph contains a circular reference and therefore cannot be resolved', exceptionMessage, 'generateSorted, when a circular reference has been defined, will throw an exception' );
}

@isTest
private static void addRelationship_whenGivenAnInvalidChild_willThrowAnException() // NOPMD: Test method name format
{
DirectedGraph graph = new DirectedGraph()
.addNode( 'Parent' );
Test.startTest();
String exceptionMessage;
try
{
graph.addRelationship( 'UnregisteredChild', 'Parent' );
}
catch ( Exception e )
{
exceptionMessage = e.getMessage();
}
Test.stopTest();

Amoss_Asserts.assertContains( 'addRelationship called with a child that has not been added as a node (UnregisteredChild)', exceptionMessage, 'addRelationship, when given a child that has not previously been added, will throw an exception' );
}

@isTest
private static void addRelationship_whenGivenAnInvalidParent_willThrowAnException() // NOPMD: Test method name format
{
DirectedGraph graph = new DirectedGraph()
.addNode( 'Child' );
Test.startTest();
String exceptionMessage;
try
{
graph.addRelationship( 'Child', 'UnregisteredParent' );
}
catch ( Exception e )
{
exceptionMessage = e.getMessage();
}
Test.stopTest();

Amoss_Asserts.assertContains( 'addRelationship called with a parent that has not been added as a node (UnregisteredParent)', exceptionMessage, 'addRelationship, when given a parent that has not previously been added, will throw an exception' );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading

0 comments on commit 0797902

Please sign in to comment.