diff --git a/README.md b/README.md index a1abe69..85c8946 100644 --- a/README.md +++ b/README.md @@ -1,171 +1,157 @@ -:information_source: _Utility classes from this repository have been moved to [apex-utils](https://github.com/ipavlic/apex-utils) repository._ - # Lambda -## Functionality - -- [`Filter`](#filter) -- [`GroupBy`](#group-by) -- [`Pick`](#pick) -- [`Pluck`](#pluck) -- [Important notes on the type system in Apex](#type-system) +Lambda allows functional constructs to be used with `SObject` collections through providing a class named `Collection`. +A `Collection` is a view of the backing `SObject` collection, and is built from standard Apex collection classes: -### `Filter` - +```java +List accounts = new List{ + new Account(Name = 'Foo', AnnualRevenue = 1000), + new Account(Name = 'Bar', AnnualRevenue = 5000) +} +Collection accountCollection = Collection.of(accounts); +``` -`Filter` enables filtering lists of sObject records by declaring *criteria* that records have to match through a fluent interface. +`Collection` instance then offers functional methods like `filter` or `remove`: -| Modifier and type | Method | Description | -|-------------------|--------|-------------| -| `static RecordMatchingFilterQuery` | `match(SObject prototype)` | Constructs and returns an object matching query against the `prototype` | -| `static PartialFieldFilterQuery` | `field(Schema.SObjectField field)` | Constructs and returns a field matching filter starting with `field` | -| `static PartialFieldFilterQuery` | `field(String fieldRelation)` | Constructs and returns a field matching filter starting with `fieldRelation` | +```java +Collection filtered = accountCollection.filter(Match.field(Account.AnnualRevenue).greaterThan(1000)); +Collection remaining = accountCollection.remove(Match.field(Account.Name).equals('Foo')); +``` -#### Record matching filter +Methods which deal with collections return `Collection` views again. Standard Apex collection instances obtained from views +through `asList()` and `asSet()` methods. ```java -Account prototype = new Account( - Name = 'Test', - AnnualRevenue = 50000000 -); -// Accounts named 'Test' with an AnnualRevenue of **exactly** 50,000,000 are matched -List filtered = Filter.match(prototype).apply(accounts); +List rawList = accountCollection.asList(); +Set rawSet = accountCollection.asSet(); ``` -Matches list records against a “prototype” record. A list record is a match if all the fields which are defined on the prototype record are equal to those on the list record. +## `Collection` functions -`Filter.match(SObject prototype)` returns a `RecordMatchingFilterQuery` which provides methods to match the filter against a list. +- [`filter`](#filter) +- [`remove`](#remove) +- [`groupBy`](#group-by) +- [`pick`](#pick) +- [`pluck`](#pluck) +- [`mapAll`](#map-all) +- [`mapSome`](#map-some) + +### `filter` + | Modifier and type | Method | Description | |-------------------|--------|-------------| -| `List` | `apply(Iterable records)` | Matches elements in `records` and returns them as a new list | -| `List` | `apply(Iterable records, Type listType)` | Matches elements in `records` and returns them as a new list of `listType` type | -| `FilterResult` | `applyLazy(Iterable records)` | Returns `FilterResult` iterable which can be used for lazy matching to allow extraction of partial results from large sources | -| `List` | `extract(Iterable records)` | Matches elements in `records`, removes them from the original list and returns them in a new list | -| `List` | `extract(Iterable records)` | Matches elements in `records`, removes them from the original list and returns them in a new list of `listType` type | +| `Collection` | `filter(SObjectPredicate predicate)` | Returns a `Collection` view of records that satisfied predicate | + +Two predicates are provided out of the box, `FieldsMatch` and `RecordMatch`. They are instantiated through factory methods on `Match`: -#### Field matching filter +```java +Collection accountCollection = Collection.of(accounts); -Matches against field criteria. +Account prototype = new Account(Name = 'Foo'); +Collection recordMatched = accountCollection.filter(Match.record(prototype)); + +Collection fieldMatched = accountCollection.filter(Match.field(Account.Name).equals('Foo')); +``` + +#### `FieldsMatch` + +`FieldsMatch` returns `true` if a record satisfies all field matching conditions. + +`FieldsMatch` is constructed with a fluent interface. `Match` factory method `field` returns an `IncompleteFieldsMatch`. +`FieldsMatch` is obtained from the `IncompleteFieldsMatch` by providing a matching condition on the field. `FieldsMatch` +can be expanded with a new matching condition to get another `IncompleteFieldsMatch`. The process is continued until all +desired matching conditions are defined. ```java -// Accounts named 'Test' are matched -List testAccounts = Filter.field(Account.Name).equals('Test').apply(accounts); +FieldsMatch m = Match.field(Account.Name).equals('Foo').also(Account.AnnualRevenue).greaterThan(100000); ``` -Multiple criteria can be stringed together with `also` (alias `field`) to form the full matching query. Records have to match *all* criteria. +`FieldsMatch` can be provided directly to `filter` method: ```java -// Accounts named 'Test' with annual revenue under 100,000 are matched -List filtered = Filter.field(Account.Name).equals('Test') - .also(Account.AnnualRevenue).lessThanOrEquals(100000) - .apply(accounts); +Collection filtered = Collection.of(accounts).filter(Match.field(Account.Name).equals('Foo').also(Account.AnnualRevenue).greaterThan(100000)); ``` -`Filter.field(Schema.SObjectField field)` returns a `PartialFieldFilterQuery` which is used to define criteria: +##### `IncompleteFieldsMatch` | Modifier and type | Method | Alias | Description | |-------------------|--------|-------|-------------| -| `FieldFilterQuery` | `equals(Object value)` | `eq` | Defines an equality comparison criterium for the current field | -| `FieldFilterQuery` | `notEquals(Object value)` | `neq` | Defines an inequality comparison criterium for the current field | -| `FieldFilterQuery` | `lessThan(Object value)` | `lt` | Defines a less than comparison criterium for the current field | -| `FieldFilterQuery` | `lessThanOrEquals(Object value)` | `leq` | Defines a less than or equals criterium for the current field | -| `FieldFilterQuery` | `greaterThan(Object value)` | `gt` | Defines a greater than criterium for the current field | -| `FieldFilterQuery` | `greaterThanOrEquals(Object value)` | `geq` | Defines a greaterThanOrEquals criterium for the current field | -| `FieldFilterQuery` | `isIn(Object value)` | | Defines a set membership criterium for the current field | -| `FieldFilterQuery` | `isNotIn(Object value)` | `notIn` | Defines a set non-membership criterium for the current field | -| `FieldFilterQuery` | `hasValue()` | `notNull` | Defines a non-null criterium for the current field | - -`FieldFilterQuery` can then be *applied* to a list, or further criteria can be chained with `also` (alias `field`): +| `FieldsMatch` | `equals(Object value)` | `eq` | Defines an equality comparison condition for the current field | +| `FieldsMatch` | `notEquals(Object value)` | `neq` | Defines an inequality comparison condition for the current field | +| `FieldsMatch` | `lessThan(Object value)` | `lt` | Defines a less than comparison condition for the current field | +| `FieldsMatch` | `lessThanOrEquals(Object value)` | `leq` | Defines a less than or equals condition for the current field | +| `FieldsMatch` | `greaterThan(Object value)` | `gt` | Defines a greater than condition for the current field | +| `FieldsMatch` | `greaterThanOrEquals(Object value)` | `geq` | Defines a greaterThanOrEquals condition for the current field | +| `FieldsMatch` | `isIn(Object value)` | | Defines a set membership condition for the current field | +| `FieldsMatch` | `isNotIn(Object value)` | `notIn` | Defines a set non-membership condition for the current field | +| `FieldsMatch` | `hasValue()` | `notNull` | Defines a non-null condition for the current field | + +##### `FieldsMatch` + +Additional conditions can be defined with `also`, or its alias, `field`: | Modifier and type | Method | Alias | Description | |-------------------|--------|-------|-------------| -| `PartialFieldFilterQuery` | `also(Schema.SObjectField field)` | `field` | Chains another criterium to the filtering query | -| `PartialFieldFilterQuery` | `also(String fieldRelationfield)` | `field` | Chains another criterium to the filtering query | -| `List` | `apply(Iterable records)` | | Matches elements in `records` and returns them as a new list | -| `List` | `apply(Iterable records, Type listType)` | | Matches elements in `records` and returns them as a new list of `listType` type | -| `FilterResult` | `applyLazy(Iterable records)` | | Returns `FilterResult` iterable which can be used for lazy matching to allow extraction of partial results from large sources | -| `List` | `extract(Iterable records)` | | Matches elements in `records`, removes them from the original list and returns them in a new list | -| `List` | `extract(Iterable records)` | | Matches elements in `records`, removes them from the original list and returns them in a new list of `listType` type | +| `IncompleteFieldsMatch` | `also(Schema.SObjectField field)` | `field` | Defines another condition to match | +| `IncompleteFieldsMatch` | `also(String fieldPath)` | `field` | Defines another condition to match | -#### Warning :warning: +##### Warning :warning: -Most criteria expect a primitive value to compare against. `isIn` and `isNotIn` instead expect a `Set` of one of the following types: `Boolean`, `Date`, `Decimal`, `Double`, `Id`, `Integer` or `String`. **Other types are not supported and will throw an exception**. +`isIn` and `isNotIn` support a `Set` of one of the following types: -Fields used in field criteria must be available on the list which is filtered, otherwise a `System.SObjectException: SObject row was retrieved via SOQL without querying the requested field` exception can be thrown. +- `Boolean` +- `Date` +- `Datetime` +- `Decimal` +- `Double` +- `Id` +- `Integer` +- `Long` +- `String` -Fields that are present on the *prototype* object must also be available on the list which is filtered, otherwise a `System.SObjectException: SObject row was retrieved via SOQL without querying the requested field` exception will be thrown. +**Other types are not supported and will throw an exception**. -Filtering query is dynamic and cannot be type-checked at compile-time. +Fields used in field conditions must be available on the collection which is filtered, otherwise a `System.SObjectException: SObject row was retrieved via SOQL without querying the requested field` exception can be thrown. -### `GroupBy` - +#### `RecordMatch` -Groups objects by values on a specified field. +`RecordMatch` returns `true` if record fields are equal to those defined on a “prototype” record. Fields that are not +defined on a prototype record do not have to match. ```java -Map> opportunitiesByCloseDate = GroupBy.dates(Opportunity.CloseDate, opportunities); +Account prototype = new Account( + Name = 'Test', + AnnualRevenue = 50000000 +); +// Accounts named 'Test' with an AnnualRevenue of **exactly** 50,000,000 are matched +Collection filtered = accountCollection.filter(Match.record(prototype)); ``` -| Modifier and type | Method | Description | -|-------------------|--------|-------------| -| `Map>` | `booleans(Schema.SObjectField field, List records)` | Groups `records` by value on boolean `field` | -| `Map>` | `booleans(Schema.SObjectField field, List records, Type listType)` | Groups `records` by value on boolean `field` as `listType` | -| `Map>` | `dates(Schema.SObjectField field, List records)` | Groups `records` by value on date `field` | -| `Map>` | `dates(Schema.SObjectField field, List records, Type listType)` | Groups `records` by value on date `field` as `listType` | -| `Map>` | `decimals(Schema.SObjectField field, List records)` | Groups `records` by value on number `field` | -| `Map>` | `decimals(Schema.SObjectField field, List records, Type listType)` | Groups `records` by value on number `field` as `listType` | -| `Map>` | `ids(Schema.SObjectField field, List records)` | Groups `records` by value on id `field` | -| `Map>` | `ids(Schema.SObjectField field, List records, Type listType)` | Groups `records` by value on id `field` as `listType` | -| `Map>` | `strings(Schema.SObjectField field, List records)` | Groups `records` by value on string `field` | -| `Map>` | `strings(Schema.SObjectField field, List records, Type listType)` | Groups `records` by value on string `field` as `listType` | - -#### Warning :warning: +##### Warning :warning: -**The type system will NOT warn you if you use the wrong subtype of `sObject`!** [Important notes on the type system in Apex](#type-system) section explains why. - -```java -// this compiles -Map> accountsByName = GroupBy.strings(Account.Name, accounts); -// this compiles as well!!!??? -Map> accountsByName = GroupBy.strings(Account.Name, accounts); -``` +Fields that are present on the *prototype* object must also be available on the collection which is filtered, otherwise a `System.SObjectException: SObject row was retrieved via SOQL without querying the requested field` exception will be thrown. -### `Pick` - +### `remove` + -Picks fields from a list of sObjects to build a new list with just those fields. Helps reduce overwriting potential for concurrent updates when locking is not an option. +`remove` works just like `filter`, but records which match a predicate are removed from the `Collection` view instead of kept. -```java -List opportunities = new List{ - new Opportunity(Name = 'Foo', Amount = 10000, Description = 'Bar') -} -// picked contains just Name and Amount fields, Description is not present -List picked = Pick.fields(new Set{'Name', 'Amount'}, opportunities); -``` - -| Modifier and type | Method | Description | -|-------------------|--------|-------------| -| `List` | `fields(List fields, Iterable records)` | Picks fields into a new `SObject` list | -| `List` | `fields(Set fields, Iterable records)` | Picks fields into a new `SObject` list | -| `List` | `fields(List apiFieldNames, Iterable records)` | Picks fields into a new `SObject` list | -| `List` | `fields(Set apiFieldNames, Iterable records)` | Picks fields into a new `SObject` list | - -### `Pluck` +### `pluck` -Plucks field values from a list of sObjects into a new list. +Plucks field values from a `Collection` view of records into a `List` of appropriate type. ```java List accounts = new List{ new Account(Name = 'Foo'), new Account(Name = 'Bar') } -// Names are plucked into a new list ['Foo', 'Bar'] -List names = Pluck.strings(accounts, Account.Name); +// Names are plucked into a new list, ['Foo', 'Bar'] +List names = Collection.of(accounts).pluckStrings(Account.Name); ``` -Pluck can also be used for relations with `String` parameters. +Pluck can also be used for deeper relations by using `String` field paths instead of `Schema.SObjectField` parameters. ```java List opportunities = new List{ @@ -174,24 +160,88 @@ List opportunities = new List{ }; // Names are plucked into a new list ['Foo', 'Bar'] -List accountNames = Pluck.strings(opportunities, 'Account.Name'); +List accountNames = Collection.of(opportunities).pluckStrings('Account.Name'); +``` + +| Modifier and type | Method | Description | +|-------------------|--------|-------------| +| `List` | `pluckBooleans(Schema.SObjectField)` | Plucks Boolean `field` values | +| `List` | `pluckBooleans(String relation)` | Plucks Boolean `relation` values | +| `List` | `pluckDates(Schema.SObjectField field)` | Plucks Date `field` values | +| `List` | `pluckDates(String relation)` | Plucks Date `relation` values | +| `List` | `pluckDatetimes(Schema.SObjectField field)` | Plucks Datetime `field` values | +| `List` | `pluckDatetimes(String relation)` | Plucks Datetime `relation` values | +| `List` | `pluckDecimals(Schema.SObjectField field)` | Plucks numerical `field` values | +| `List` | `pluckDecimals(Schema.SObjectField field)` | Plucks numerical `relation` values | +| `List` | `pluckIds(Schema.SObjectField field)` | Plucks Id `field` values | +| `List` | `pluckIds(String relation)` | Plucks Id `relation` values | +| `List` | `pluckIds()` | Plucks values of `Id` field | +| `List` | `strings(Schema.SObjectField field)` | Plucks String or Id `field` values | +| `List` | `strings(Schema.SObjectField relation)` | Plucks String or Ids `relation` values | + +### `groupBy` + + +Groups records by values of a specified field. + +```java +Map> opportunitiesByCloseDate = Collection.of(opportunities).groupByDates(Opportunity.CloseDate, opportunities); +``` + +| Modifier and type | Method | Description | +|-------------------|--------|-------------| +| `Map>` | `groupByBooleans(Schema.SObjectField field)` | Groups records by Boolean `field` values | +| `Map>` | `groupByBooleans(Schema.SObjectField field, Type listType)` | Groups records by Boolean `field` values, with specified list type | +| `Map>` | `groupByDates(Schema.SObjectField field)` | Groups records by Date `field` values | +| `Map>` | `groupByDates(Schema.SObjectField field, Type listType)` | Groups records by Date `field` values, with specified list type | +| `Map>` | `groupByDatetimes(Schema.SObjectField field)` | Groups records by Datetime `field` values | +| `Map>` | `groupByDatetimes(Schema.SObjectField field, Type listType)` | Groups records by Datetime `field` values, with specified list type | +| `Map>` | `groupByDecimals(Schema.SObjectField field)` | Groups records by numeric `field` values | +| `Map>` | `groupByDecimals(Schema.SObjectField field, Type listType)` | Groups records by numeric `field` values, with specified list type | +| `Map>` | `groupByIds(Schema.SObjectField field)` | Groups records by Id `field` values | +| `Map>` | `groupByIds(Schema.SObjectField field, Type listType)` | Groups records by Id `field` values, with specified list type | +| `Map>` | `groupByStrings(Schema.SObjectField field)` | Groups records by String `field` values | +| `Map>` | `groupByStrings(Schema.SObjectField field, Type listType)` | Groups records by String `field` values, with specified list type | + +### `pick` + + +Returns a new `Collection` view of the collection which keeps just the specified fields, discarding others. Helps reduce overwriting potential for concurrent updates when locking is not an option. + +```java +List opportunities = new List{ + new Opportunity(Name = 'Foo', Amount = 10000, Description = 'Bar') +} +// Picked contains just Name and Amount fields. Description is not present. +Collection picked = Collection.of(opportunities).pick(new Set{'Name', 'Amount'}); ``` | Modifier and type | Method | Description | |-------------------|--------|-------------| -| `List` | `booleans(Schema.SObjectField field, List records)` | Plucks booleans of `field` into a new list | -| `List` | `booleans(String relation)` | , List recordsPlucks booleans of `relation` into a new list | -| `List` | `dates(Schema.SObjectField field, List records)` | Plucks dates of `field` into a new list | -| `List` | `dates(String relation)` | , List recordsPlucks dates of `relation` into a new list | -| `List` | `decimals(Schema.SObjectField field, List records)` | Plucks numbers of `field` into a new list | -| `List` | `decimals(Schema.SObjectField field, List records)` | Plucks numbers of `relation` into a new list | -| `Set` | `ids(Schema.SObjectField field, List records)` | Plucks ids of `field` into a new set | -| `Set` | `ids(String relation)` | , List recordsPlucks ids of `relation` into a new set | -| `Set` | `ids( | Plucks `, List recordsId` field values into a new set | -| `List` | `strings(Schema.SObjectField field, List records)` | Plucks strings or ids of `field` into a new list | -| `List` | `strings(Schema.SObjectField relation, List records)` | Plucks strings or ids of `relation` into a new list | - -### Important notes on the type system in Apex +| `Collection` | `pick(List fields)` | Picks fields into a new `Collection` view | +| `Collection` | `pick(Set fields)` | Picks fields into a new `Collection` view | +| `Collection` | `pick(List apiFieldNames)` | Picks fields into a new `Collection` view | +| `Collection` | `pick(Set apiFieldNames)` | Picks fields into a new `Collection` view | + +### `mapAll` + + +Maps all elements of `Collection` view into another `Collection` view with the provided `SObjectToSObjectFunction` mapping function. + +| Modifier and type | Method | Description | +|-------------------|--------|-------------| +| `Collection` | `mapAll(SObjectToSObjectFunction fn)` | Returns a new `Collection` view formed by mapping all current view elements with `fn` | + +### `mapSome` + + +Returns a new `Collection` view formed by mapping those view elements that satisfy `predicate`, and keeping those that do not unchanged. + +| Modifier and type | Method | Description | +|-------------------|--------|-------------| +| `Collection` | `mapAll(SObjectToSObjectFunction fn)` | Returns a new `Collection` view formed by mapping current view elements that satisfy `predicate` with `fn`, and keeping those that do not satisfy `predicate` unchanged. | + +## Important notes on the type system in Apex Apex allows assignment of `SObject` collection to its “subclass”, and the other way around: @@ -212,22 +262,13 @@ System.debug(objects instanceof List); // true System.debug(objects instanceof List); // true ``` -Lambda classes usually return an `SObject` list, which can be then assigned to a specific `SObject` “subclass” list, like `Account`. This is more convenient, but `instanceof` can provide unexpected results: +Collection’s `asList()` and `asSet()` return a raw `List` and `Set`. This is more convenient, but `instanceof` can provide unexpected results. +A concrete type of the list can be passed in as well. When this is done, the returned `List` or `Set` are of the correct concrete type instead of generic `SObject` collection type: ```java -List accounts = Filter... -// accounts points to a List returned from Filter +List filteredAccounts = accountCollection.asList(); +// List returned! -Boolean isOpportunities = accounts instanceof List; -// isOpportunities is true!!!??? -``` - -`Filter` and `GroupBy` therefore provide overloaded methods in which the concrete type of the list can be passed in as well. When this is done, the returned `List` or `Map` are of the correct concrete type instead of generic `SObject` collection type: - -```java -List filteredAccounts = Filter.field(...).apply(allAccounts, List.class); +List filteredAccounts = accountCollection.asList(List.class); // List returned! - -Map> accountsByName = GroupBy.strings(Account.Name, allAccounts, List.class); -// Map> returned! ``` diff --git a/src/classes/Collection.cls b/src/classes/Collection.cls new file mode 100644 index 0000000..4ae0f1d --- /dev/null +++ b/src/classes/Collection.cls @@ -0,0 +1,318 @@ +public with sharing class Collection { + + private List records; + + public static Collection of(Iterable records) { + return new Collection(records); + } + + public static Collection of(Set records) { + return new Collection(new List(records)); + } + + private Collection(Iterable records) { + this.records = new List(); + Iterator iter = records.iterator(); + while (iter.hasNext()) { + this.records.add(iter.next()); + } + } + + public List asList() { + return new List(records); + } + + public List asList(Type listType) { + List typedList = (List) listType.newInstance(); + typedList.addAll(records); + return typedList; + } + + public Set asSet() { + return new Set(records); + } + + public Set asSet(Type setType) { + Set typedSet = (Set) setType.newInstance(); + typedSet.addAll(records); + return typedSet; + } + + public Collection filter(SObjectPredicate predicate) { + List filtered = new List(); + for (SObject record : records) { + if (predicate.apply(record)) { + filtered.add(record); + } + } + return Collection.of(filtered); + } + + public Collection remove(SObjectPredicate predicate) { + List remaining = new List(); + for (SObject record : records) { + if (!predicate.apply(record)) { + remaining.add(record); + } + } + return Collection.of(remaining); + } + + public List pluckBooleans(Schema.SObjectField field) { + List results = new List(); + for (SObject rec : records) { + results.add((Boolean)rec.get(field)); + } + return results; + } + + public List pluckBooleans(String relation) { + List results = new List(); + SObjectFieldReader reader = new SObjectFieldReader(); + for (SObject rec : records) { + results.add((Boolean)reader.read(rec, relation)); + } + return results; + } + + public List pluckDates(Schema.SObjectField field) { + List results = new List(); + for (SObject rec : records) { + results.add((Date)rec.get(field)); + } + return results; + } + + public List pluckDates(String relation) { + List results = new List(); + SObjectFieldReader reader = new SObjectFieldReader(); + for (SObject rec : records) { + results.add((Date)reader.read(rec, relation)); + } + return results; + } + + public List pluckDatetimes(Schema.SObjectField field) { + List results = new List(); + for (SObject rec : records) { + results.add((Datetime)rec.get(field)); + } + return results; + } + + public List pluckDatetimes(String relation) { + List results = new List(); + SObjectFieldReader reader = new SObjectFieldReader(); + for (SObject rec : records) { + results.add((Datetime)reader.read(rec, relation)); + } + return results; + } + + public List pluckDecimals(Schema.SObjectField field) { + List results = new List(); + for (SObject rec : records) { + results.add((Decimal)rec.get(field)); + } + return results; + } + + public List pluckDecimals(String relation) { + List results = new List(); + SObjectFieldReader reader = new SObjectFieldReader(); + for (SObject rec : records) { + results.add((Decimal)reader.read(rec, relation)); + } + return results; + } + + public List pluckIds() { + List results = new List(); + for (SObject rec : records) { + results.add(rec.Id); + } + return results; + } + + public List pluckIds(Schema.SObjectField field) { + List results = new List(); + for (SObject rec : records) { + results.add((Id)rec.get(field)); + } + return results; + } + + public List pluckIds(String relation) { + List results = new List(); + SObjectFieldReader reader = new SObjectFieldReader(); + for (SObject rec : records) { + results.add((Id)reader.read(rec, relation)); + } + return results; + } + + public List pluckStrings(Schema.SObjectField field) { + List results = new List(); + for (SObject rec : records) { + results.add((String)rec.get(field)); + } + return results; + } + + public List pluckStrings(String relation) { + List results = new List(); + SObjectFieldReader reader = new SObjectFieldReader(); + for (SObject rec : records) { + results.add((String) reader.read(rec, relation)); + } + return results; + } + + public Map> groupByBooleans(Schema.SObjectField field, Type listType) { + Map> grouped = new Map>(); + for (SObject rec : records) { + Boolean key = (Boolean) rec.get(field); + if (!grouped.containsKey(key)) { + grouped.put(key, (List) listType.newInstance()); + } + grouped.get(key).add(rec); + } + return grouped; + } + + public Map> groupByBooleans(Schema.SObjectField field) { + return groupByBooleans(field, List.class); + } + + public Map> groupByDates(Schema.SObjectField field, Type listType) { + Map> grouped = new Map>(); + for (SObject rec : records) { + Date key = (Date) rec.get(field); + if (!grouped.containsKey(key)) { + grouped.put(key, (List) listType.newInstance()); + } + grouped.get(key).add(rec); + } + return grouped; + } + + public Map> groupByDatetimes(Schema.SObjectField field) { + return groupByDatetimes(field, List.class); + } + + public Map> groupByDatetimes(Schema.SObjectField field, Type listType) { + Map> grouped = new Map>(); + for (SObject rec : records) { + Datetime key = (Datetime) rec.get(field); + if (!grouped.containsKey(key)) { + grouped.put(key, (List) listType.newInstance()); + } + grouped.get(key).add(rec); + } + return grouped; + } + + public Map> groupByDates(Schema.SObjectField field) { + return groupByDates(field, List.class); + } + + public Map> groupByDecimals(Schema.SObjectField field, Type listType) { + Map> grouped = new Map>(); + for (SObject rec : records) { + Decimal key = (Decimal) rec.get(field); + if (!grouped.containsKey(key)) { + grouped.put(key, (List) listType.newInstance()); + } + grouped.get(key).add(rec); + } + return grouped; + } + + public Map> groupByDecimals(Schema.SObjectField field) { + return groupByDecimals(field, List.class); + } + + public Map> groupByIds(Schema.SObjectField field, Type listType) { + Map> grouped = new Map>(); + for (SObject rec : records) { + Id key = (Id) rec.get(field); + if (!grouped.containsKey(key)) { + grouped.put(key, (List) listType.newInstance()); + } + grouped.get(key).add(rec); + } + return grouped; + } + + public Map> groupByIds(Schema.SObjectField field) { + return groupByIds(field, List.class); + } + + public Map> groupByStrings(Schema.SObjectField field, Type listType) { + Map> grouped = new Map>(); + for (SObject rec : records) { + String key = (String) rec.get(field); + if (!grouped.containsKey(key)) { + grouped.put(key, (List) listType.newInstance()); + } + grouped.get(key).add(rec); + } + return grouped; + } + + public Map> groupByStrings(Schema.SObjectField field) { + return groupByStrings(field, List.class); + } + + public Collection pick(List fields) { + return pick(new Set(fields)); + } + + public Collection pick(Set fields) { + Set fieldNames = new Set(); + for (Schema.SObjectField field : fields) { + Schema.DescribeFieldResult describe = field.getDescribe(); + fieldNames.add(describe.getName()); + } + return pick(fieldNames); + } + + public Collection pick(Set apiFieldNames) { + List picked = new List(); + for (SObject record : records) { + SObject result = record.getSObjectType().newSObject(); + Map fieldMap = record.getPopulatedFieldsAsMap(); + for (String fieldName : apiFieldNames) { + if (fieldMap.containsKey(fieldName)) { + result.put(fieldName, record.get(fieldName)); + } + } + picked.add(result); + } + return Collection.of(picked); + } + + public Collection pick(List apiFieldNames) { + return pick(new Set(apiFieldNames)); + } + + public Collection mapAll(SObjectToSObjectFunction fn) { + List mapped = new List(); + for (SObject record : records) { + mapped.add(fn.apply(record)); + } + return Collection.of(mapped); + } + + public Collection mapSome(SObjectPredicate predicate, SObjectToSObjectFunction fn) { + List transformed = new List(); + for (SObject record : records) { + if (predicate.apply(record)) { + transformed.add(fn.apply(record)); + } else { + transformed.add(record); + } + } + return Collection.of(transformed); + } +} diff --git a/src/classes/Filter.cls-meta.xml b/src/classes/Collection.cls-meta.xml similarity index 80% rename from src/classes/Filter.cls-meta.xml rename to src/classes/Collection.cls-meta.xml index b211a09..45aa0a0 100644 --- a/src/classes/Filter.cls-meta.xml +++ b/src/classes/Collection.cls-meta.xml @@ -1,5 +1,5 @@ - 28.0 + 44.0 Active diff --git a/src/classes/CollectionTest.cls b/src/classes/CollectionTest.cls new file mode 100644 index 0000000..6f3ae11 --- /dev/null +++ b/src/classes/CollectionTest.cls @@ -0,0 +1,432 @@ +@IsTest +public class CollectionTest { + + static Id firstUserId = TestUtility.getTestId(User.SObjectType); + static Id secondUserId = TestUtility.getTestId(User.SObjectType); + + static List testAccounts() { + return new List{ + new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = firstUserId, Name = 'Foo', AnnualRevenue = 100), + new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = firstUserId, Name = 'Bar', AnnualRevenue = 60), + new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = secondUserId, Name = 'Foo', AnnualRevenue = 150), + new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = secondUserId, Name = 'Bar', AnnualRevenue = 150) + }; + } + + @IsTest + public static void testPluckDecimals() { + List revenues = Collection.of(testAccounts()).pluckDecimals(Account.AnnualRevenue); + System.assertEquals(4, revenues.size()); + System.assertEquals(100.0, revenues[0]); + System.assertEquals(60.0, revenues[1]); + System.assertEquals(150.0, revenues[2]); + System.assertEquals(150.0, revenues[3]); + } + + @IsTest + public static void testPluckStrings() { + List names = Collection.of(testAccounts()).pluckStrings(Account.Name); + System.assertEquals(4, names.size()); + System.assertEquals('Foo', names[0]); + System.assertEquals('Bar', names[1]); + System.assertEquals('Foo', names[2]); + System.assertEquals('Bar', names[3]); + } + + @IsTest + public static void testPluckIdsAsStrings() { + List ownerIds = Collection.of(testAccounts()).pluckStrings(Account.OwnerId); + System.assertEquals(4, ownerIds.size()); + System.assertEquals(firstUserId, ownerIds[0]); + System.assertEquals(firstUserId, ownerIds[1]); + System.assertEquals(secondUserId, ownerIds[2]); + System.assertEquals(secondUserId, ownerIds[3]); + } + + @IsTest + public static void testPluckIds() { + List ownerIds = Collection.of(testAccounts()).pluckIds(Account.OwnerId); + // workaround for List.contains bug + Set idSet = new Set(ownerIds); + System.assertEquals(2, idSet.size()); + System.assert(idSet.contains(firstUserId)); + System.assert(idSet.contains(secondUserId)); + } + + @IsTest + public static void testPluckRecordIds() { + List accounts = testAccounts(); + List recordIds = Collection.of(accounts).pluckIds(); + System.assertEquals(4, recordIds.size()); + // workaround for List.contains bug + Set idSet = new Set(recordIds); + System.assert(idSet.contains(accounts[0].Id)); + System.assert(idSet.contains(accounts[1].Id)); + System.assert(idSet.contains(accounts[2].Id)); + System.assert(idSet.contains(accounts[3].Id)); + } + + @IsTest + public static void testPluckBooleans() { + List users = new List{ + new User(Title = 'Foo', IsActive = true), + new User(Title = 'Bar', IsActive = true), + new User(Title = 'Baz', IsActive = false) + }; + List active = Collection.of(users).pluckBooleans(User.IsActive); + System.assertEquals(3, active.size()); + System.assertEquals(true, active[0]); + System.assertEquals(true, active[1]); + System.assertEquals(false, active[2]); + } + + @IsTest + public static void testFieldsMatchFilter() { + Collection l = Collection.of(new List{ + new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = firstUserId, Name = 'Foo', AnnualRevenue = 100), + new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = firstUserId, Name = 'Bar', AnnualRevenue = 60), + new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = secondUserId, Name = 'Foo', AnnualRevenue = 150), + new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = secondUserId, Name = 'Bar', AnnualRevenue = 150) + }); + + List filtered = l.filter(Match.field(Account.AnnualRevenue).eq(150)).asList(); + System.assertEquals(2, filtered.size()); + + for (Account a : filtered) { + System.assertEquals(150, a.AnnualRevenue); + } + } + + @IsTest + public static void testGroupByStrings() { + Collection l = Collection.of(testAccounts()); + Map> accountsByName = l.groupByStrings(Account.Name); + System.assertEquals(2, accountsByName.size()); + System.assert(accountsByName.keySet().contains('Foo')); + System.assert(accountsByName.keySet().contains('Bar')); + System.assertEquals(2, accountsByName.get('Foo').size()); + System.assertEquals(2, accountsByName.get('Bar').size()); + for (Account a : accountsByName.get('Foo')) { + System.assertEquals('Foo', a.Name); + } + for (Account a : accountsByName.get('Bar')) { + System.assertEquals('Bar', a.Name); + } + } + + @IsTest + public static void testGroupByStringTyping() { + Collection l = Collection.of(testAccounts()); + Map> accountsByName = l.groupByStrings(Account.Name); + List fooAccounts = accountsByName.get('Foo'); + List objects = fooAccounts; + // since fooAccounts points to a returned list of SObjects, it can be anything! + System.assert(objects instanceof List); + + accountsByName = l.groupBystrings(Account.Name, List.class); + fooAccounts = accountsByName.get('Foo'); + objects = fooAccounts; + // this time around, it works fine! + System.assert(!(objects instanceof List)); + System.assert(objects instanceof List); + } + + @IsTest + public static void testGroupByDecimals() { + Collection l = Collection.of(testAccounts()); + Map> accountsByRevenue = l.groupByDecimals(Account.AnnualRevenue); + System.assertEquals(3, accountsByRevenue.size()); + System.assert(accountsByRevenue.keySet().contains(60)); + System.assert(accountsByRevenue.keySet().contains(100)); + System.assert(accountsByRevenue.keySet().contains(150)); + System.assertEquals(1, accountsByRevenue.get(60).size()); + System.assertEquals(1, accountsByRevenue.get(100).size()); + System.assertEquals(2, accountsByRevenue.get(150).size()); + for (Account a : accountsByRevenue.get(150)) { + System.assertEquals(150.0, a.AnnualRevenue); + } + } + + @IsTest + public static void testGroupByIds() { + Collection l = Collection.of(testAccounts()); + Map> accountsByOwners = l.groupByIds(Account.OwnerId); + System.assertEquals(2, accountsByOwners.size()); + System.assert(accountsByOwners.keySet().contains(firstUserId)); + System.assert(accountsByOwners.keySet().contains(secondUserId)); + System.assertEquals(2, accountsByOwners.get(firstUserId).size()); + System.assertEquals(2, accountsByOwners.get(secondUserId).size()); + for (Account a : accountsByOwners.get(firstUserId)) { + System.assertEquals(firstUserId, a.OwnerId); + } + } + + @IsTest + public static void testGroupByBooleans() { + Collection l = Collection.of(new List{ + new User(Title = 'Foo', IsActive = true), + new User(Title = 'Bar', IsActive = true), + new User(Title = 'Baz', IsActive = false) + }); + Map> usersByActive = l.groupByBooleans(User.IsActive); + System.assertEquals(2, usersByActive.size()); + System.assert(usersByActive.keySet().contains(true)); + System.assert(usersByActive.keySet().contains(false)); + System.assertEquals(2, usersByActive.get(true).size()); + System.assertEquals(1, usersByActive.get(false).size()); + for (User u : usersByActive.get(true)) { + System.assertEquals(true, u.IsActive); + } + } + + @IsTest + public static void pickShouldPickFields() { + Collection l = Collection.of(new List{ + new Account(Name = 'Test1', AnnualRevenue = 100), + new Account(Name = 'Test2', AnnualRevenue = 200) + }); + verifyNamePick(l.pick(new List{Account.Name})); + verifyNamePick(l.pick(new Set{Account.Name})); + verifyNamePick(l.pick(new List{'Name'})); + verifyNamePick(l.pick(new Set{'Name'})); + } + + @IsTest + public static void pickedFieldsShouldHaveValues() { + Collection l = Collection.of(new List{ + new Opportunity(Name = 'Test', Amount = 100, Description = 'Test description') + }); + List picked = l.pick(new List{'Name', 'Amount'}).asList(); + System.assertEquals(1, picked.size()); + for (Opportunity opp : picked) { + System.assertEquals('Test', opp.Name); + System.assertEquals(100, opp.Amount); + } + } + + @IsTest + public static void pickShouldPickHeterogenousRecords() { + Collection l = Collection.of(new List{ + new Account(Name = 'Test1', AnnualRevenue = 100), + new Opportunity(Name = 'Test1', Description = 'Test description') + }); + verifyNamePick(l.pick(new List{'Name'})); + verifyNamePick(l.pick(new Set{'Name'})); + } + + @IsTest + public static void pickShouldHaveMatchingObjectTypes() { + Collection l = Collection.of(new List{ + new Account(Name = 'Test1', AnnualRevenue = 100), + new Opportunity(Name = 'Test1', Description = 'Test description') + }); + List picked = l.pick(new List{'Name'}).asList(); + System.assertEquals(Account.sObjectType, picked[0].getSObjectType(), 'First picked element should be an Account.'); + System.assertEquals(Opportunity.sObjectType, picked[1].getSObjectType(), 'Second picked element should be an Opportunity.'); + } + + private static void verifyNamePick(Collection picked) { + for (SObject obj : picked.asList()) { + Map fields = obj.getPopulatedFieldsAsMap(); + System.assertEquals(1, fields.size()); + System.assert(fields.containsKey('Name')); + } + } + + static List testFilterAccounts() { + List accounts = new List{ + new Account(Name = 'Ok', AnnualRevenue = 100), + new Account(Name = 'Wrong', AnnualRevenue = 60), + new Account(Name = 'Ok', AnnualRevenue = 150), + new Account(Name = 'Wrong', AnnualRevenue = 150) + }; + return accounts; + } + + @IsTest + static void testRelationalFiltering() { + List accounts = new List{ + new Account(Name = 'Ok', AnnualRevenue = 100), + new Account(Name = 'Wrong', AnnualRevenue = 60) + }; + List opps = new List{ + new Opportunity( + Name = 'First', + CloseDate = Date.today().addDays(3), + Account = accounts[0] + ), + new Opportunity( + Name = 'Second', + CloseDate = Date.today().addDays(6), + Account = accounts[1] + ) + }; + Collection l = Collection.of(opps); + List filtered = (List) l.filter(Match.field('Account.AnnualRevenue').greaterThan(70)).asList(); + System.assertEquals(1, filtered.size()); + System.assertEquals('First', filtered[0].Name); + } + + @IsTest + static void testHasValue() { + Collection l = Collection.of(testFilterAccounts()); + List filtered = (List) l.filter(Match.field(Account.Industry).hasValue()).asList(); + System.assertEquals(0, filtered.size()); + + filtered = (List) l.filter(Match.field(Account.Name).hasValue()).asList(); + System.assertEquals(4, filtered.size()); + + } + + @IsTest + static void testIsIn() { + Collection l = Collection.of(testFilterAccounts()); + List filtered = (List) l.filter(Match.field(Account.AnnualRevenue).isIn(new Set{60, 150})).asList(); + System.assertEquals(3, filtered.size()); + for (Account acc : filtered) { + System.assert(acc.AnnualRevenue == 60 || acc.AnnualRevenue == 150); + } + } + + @IsTest + static void testIsNotIn() { + Collection l = Collection.of(testFilterAccounts()); + List filtered = (List) l.filter(Match.field(Account.AnnualRevenue).notIn(new Set{60})).asList(); + System.assertEquals(3, filtered.size()); + for (Account acc : filtered) { + System.assert(acc.AnnualRevenue == 100 || acc.AnnualRevenue == 150); + } + } + + @IsTest + static void testFieldEqualsOkFilter() { + Collection l = Collection.of(testFilterAccounts()); + + List filtered = (List) l.filter(Match.field(Account.Name).equals('Ok')).asList(); + + System.assertEquals(2, filtered.size()); + for (Account acc : filtered) { + System.assertEquals('Ok', acc.Name); + } + + List remaining = (List) l.remove(Match.field(Account.Name).equals('Ok')).asList(); + + System.assertEquals(2, remaining.size()); + for (Account acc : remaining) { + System.assertEquals('Wrong', acc.Name); + } + } + + @IsTest + static void testMultipleFieldFilter() { + Collection l = Collection.of(testFilterAccounts()); + List filtered = (List) l.filter(Match.field(Account.Name).equals('Ok').also(Account.AnnualRevenue).gt(100)).asList(); + + System.assertEquals(1, filtered.size()); + for (Account acc : filtered) { + System.assertEquals('Ok', acc.Name); + System.assert(acc.AnnualRevenue > 100); + } + + List remaining = (List) l.remove(Match.field(Account.Name).equals('Ok').also(Account.AnnualRevenue).gt(100)).asList(); + + System.assertEquals(3, remaining.size()); + for (Account acc : remaining) { + System.assert(acc.AnnualRevenue <= 100 || acc.Name != 'Ok'); + } + } + + @IsTest + static void testSameFieldTokenExclusionCriteria() { + Collection l = Collection.of(testFilterAccounts()); + + List filtered = (List) l.filter(Match.field(Account.Name).equals('Ok').also(Account.Name).neq('Ok')).asList(); + System.assertEquals(0, filtered.size()); + + List remaining = (List) l.remove(Match.field(Account.Name).equals('Ok').also(Account.Name).neq('Ok')).asList(); + System.assertEquals(4, remaining.size()); + } + + @IsTest + static void testSameFieldExclusionCriteria() { + Collection l = Collection.of(testFilterAccounts()); + + List filtered = (List) l.filter(Match.field(Account.Name).equals('Ok').also(Account.Name).neq('Ok')).asList(); + System.assertEquals(0, filtered.size()); + + List remaining = (List) l.remove(Match.field(Account.Name).equals('Ok').also(Account.Name).neq('Ok')).asList(); + System.assertEquals(4, remaining.size()); + } + + @IsTest + static void testLongChaining() { + Collection l = Collection.of(testFilterAccounts()); + + List filtered = (List) l.filter(Match.field(Account.Name).equals('Ok').also(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).geq(100)).asList(); + System.assertEquals(1, filtered.size()); + + List remaining = (List) l.remove(Match.field(Account.Name).equals('Ok').also(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).geq(100)).asList(); + System.assertEquals(3, remaining.size()); + } + + @IsTest + static void testSameFieldSandwichCriteria() { + Collection l = Collection.of(testFilterAccounts()); + + List filtered = (List) l.filter(Match.field(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).gt(60)).asList(); + System.assertEquals(1, filtered.size()); + + List remaining = (List) l.remove(Match.field(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).gt(60)).asList(); + System.assertEquals(3, remaining.size()); + } + + @IsTest + static void testSameTokenSandwichCriteria() { + Collection l = Collection.of(testFilterAccounts()); + + List filtered = (List) l.filter(Match.field(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).gt(60)).asList(); + System.assertEquals(1, filtered.size()); + + List remaining = (List) l.remove(Match.field(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).gt(60)).asList(); + System.assertEquals(3, remaining.size()); + } + + @IsTest + static void testComparisonFilter() { + Collection l = Collection.of(testFilterAccounts()); + + List filtered = (List) l.filter(Match.record(new Account(AnnualRevenue = 150))).asList(); + System.assertEquals(2, filtered.size()); + for (Account acc : filtered) { + System.assertEquals(150, acc.AnnualRevenue); + } + + List remaining = (List) l.remove(Match.record(new Account(AnnualRevenue = 150))).asList(); + System.assertEquals(2, remaining.size()); + for (Account acc : remaining) { + System.assertNotEquals(150, acc.AnnualRevenue); + } + } + + @IsTest + static void testListTyping() { + Collection l = Collection.of(testFilterAccounts()); + List filtered = l.filter(Match.field(Account.AnnualRevenue).lt(150)).asList(List.class); + System.assert(filtered instanceof List); + System.assert(!(filtered instanceof List)); + + List filteredWithoutType = l.filter(Match.field(Account.AnnualRevenue).lt(150)).asList(); + // when no type is provided, the returned list is a list of sObjects which can be a list of anything! + System.assert(filteredWithoutType instanceof List); + System.assert(filteredWithoutType instanceof List); + + List remaining = l.remove(Match.field(Account.AnnualRevenue).lt(150)).asList(List.class); + System.assert(remaining instanceof List); + System.assert(!(remaining instanceof List)); + + List remainingWithoutType = l.remove(Match.field(Account.AnnualRevenue).lt(150)).asList(); + // when no type is provided, the returned list is a list of sObjects which can be a list of anything! + System.assert(remainingWithoutType instanceof List); + System.assert(remainingWithoutType instanceof List); + } +} diff --git a/src/classes/FilterQuery.cls-meta.xml b/src/classes/CollectionTest.cls-meta.xml similarity index 80% rename from src/classes/FilterQuery.cls-meta.xml rename to src/classes/CollectionTest.cls-meta.xml index b211a09..45aa0a0 100644 --- a/src/classes/FilterQuery.cls-meta.xml +++ b/src/classes/CollectionTest.cls-meta.xml @@ -1,5 +1,5 @@ - 28.0 + 44.0 Active diff --git a/src/classes/ComparisonResult.cls b/src/classes/ComparisonResult.cls new file mode 100644 index 0000000..561fae7 --- /dev/null +++ b/src/classes/ComparisonResult.cls @@ -0,0 +1,3 @@ +public enum ComparisonResult { + LESS_THAN, GREATER_THAN, EQUALS, NOT_EQUALS +} diff --git a/src/classes/FieldFilterQuery.cls-meta.xml b/src/classes/ComparisonResult.cls-meta.xml similarity index 80% rename from src/classes/FieldFilterQuery.cls-meta.xml rename to src/classes/ComparisonResult.cls-meta.xml index 53eefa5..45aa0a0 100644 --- a/src/classes/FieldFilterQuery.cls-meta.xml +++ b/src/classes/ComparisonResult.cls-meta.xml @@ -1,5 +1,5 @@ - 33.0 + 44.0 Active diff --git a/src/classes/FieldFilterQuery.cls b/src/classes/FieldFilterQuery.cls deleted file mode 100644 index d814d5f..0000000 --- a/src/classes/FieldFilterQuery.cls +++ /dev/null @@ -1,93 +0,0 @@ -public class FieldFilterQuery extends FilterQuery { - private List queryCriteria = new List(); - private PrimitiveComparer comparer = new PrimitiveComparer(); - - public FieldFilterQuery addCriterium(String fieldRelation, Comparison criterium, Object value) { - this.queryCriteria.add(new FieldFilterQueryCriterium(fieldRelation, criterium, value)); - return this; - } - - public PartialFieldFilterQuery field(Schema.SObjectField field) { - return new PartialFieldFilterQuery(this, field); - } - - public PartialFieldFilterQuery field(String fieldRelation) { - return new PartialFieldFilterQuery(this, fieldRelation); - } - - public PartialFieldFilterQuery also(Schema.SObjectField field) { - return this.field(field); - } - - public PartialFieldFilterQuery also(String fieldRelation) { - return this.field(fieldRelation); - } - - private Boolean contains(Object valueSet, Object value) { - if (valueSet instanceof Set) { - Set validValues = (Set) valueSet; - return validValues.contains((Boolean) value); - } else if (valueSet instanceof Set) { - Set validValues = (Set) valueSet; - return validValues.contains((Date) value); - } else if (valueSet instanceof Set) { - Set validValues = (Set) valueSet; - return validValues.contains((Decimal) value); - } else if (valueSet instanceof Set) { - Set validValues = (Set) valueSet; - return validValues.contains((Double) value); - } else if (valueSet instanceof Set) { - Set validValues = (Set) valueSet; - return validValues.contains((Id) value); - } else if (valueSet instanceof Set) { - Set validValues = (Set) valueSet; - return validValues.contains((Integer) value); - } else if (valueSet instanceof Set) { - Set validValues = (Set) valueSet; - return validValues.contains((String) value); - } - throw new LambdaException('Provided set type is not supported by the filter'); - } - - public override Boolean isValid(sObject record) { - Boolean isValid = true; - PrimitiveComparer comparer = new PrimitiveComparer(); - RelationFieldReader reader = new RelationFieldReader(); - for (FieldFilterQueryCriterium c : queryCriteria) { - Object fieldValue = reader.read(record, c.fieldRelation); - if (c.criterium == Comparison.EQUALS && comparer.compare(fieldValue, c.value) != Comparison.EQUALS) { - isValid = false; - break; - } - if (c.criterium == Comparison.NOT_EQUALS && comparer.compare(fieldValue, c.value) == Comparison.EQUALS) { - isValid = false; - break; - } - if (c.criterium == Comparison.LESS_THAN && comparer.compare(fieldValue, c.value) != Comparison.LESS_THAN) { - isValid = false; - break; - } - if (c.criterium == Comparison.LESS_THAN_OR_EQUALS && (comparer.compare(fieldValue, c.value) == Comparison.GREATER_THAN || comparer.compare(fieldValue, c.value) == Comparison.NOT_EQUALS)) { - isValid = false; - break; - } - if (c.criterium == Comparison.GREATER_THAN && comparer.compare(fieldValue, c.value) != Comparison.GREATER_THAN) { - isValid = false; - break; - } - if (c.criterium == Comparison.GREATER_THAN_OR_EQUALS && (comparer.compare(fieldValue, c.value) == Comparison.LESS_THAN || comparer.compare(fieldValue, c.value) == Comparison.NOT_EQUALS)) { - isValid = false; - break; - } - if (c.criterium == Comparison.IS_IN && (contains(c.value, fieldValue) == false)) { - isValid = false; - break; - } - if (c.criterium == Comparison.NOT_IN && (contains(c.value, fieldValue) == true)) { - isValid = false; - break; - } - } - return isValid; - } -} \ No newline at end of file diff --git a/src/classes/FieldFilterQueryCriterium.cls b/src/classes/FieldFilterQueryCriterium.cls deleted file mode 100644 index 8411c9f..0000000 --- a/src/classes/FieldFilterQueryCriterium.cls +++ /dev/null @@ -1,11 +0,0 @@ -public class FieldFilterQueryCriterium { - public String fieldRelation {get; set;} - public Comparison criterium {get; set;} - public Object value {get; set;} - - public FieldFilterQueryCriterium(String fieldRelation, Comparison criterium, Object value) { - this.fieldRelation = fieldRelation; - this.criterium = criterium; - this.value = value; - } -} \ No newline at end of file diff --git a/src/classes/FieldMatchCondition.cls b/src/classes/FieldMatchCondition.cls new file mode 100644 index 0000000..c6669fd --- /dev/null +++ b/src/classes/FieldMatchCondition.cls @@ -0,0 +1,12 @@ +public class FieldMatchCondition { + + public String fieldPath { get; set;} + public Relation relation { get; set;} + public Object value { get; set;} + + public FieldMatchCondition(String fieldPath, Relation relation, Object value) { + this.fieldPath = fieldPath; + this.relation = relation; + this.value = value; + } +} diff --git a/src/classes/FilterResult.cls-meta.xml b/src/classes/FieldMatchCondition.cls-meta.xml similarity index 80% rename from src/classes/FilterResult.cls-meta.xml rename to src/classes/FieldMatchCondition.cls-meta.xml index 38aa015..45aa0a0 100644 --- a/src/classes/FilterResult.cls-meta.xml +++ b/src/classes/FieldMatchCondition.cls-meta.xml @@ -1,5 +1,5 @@ - 36.0 + 44.0 Active diff --git a/src/classes/FieldsMatch.cls b/src/classes/FieldsMatch.cls new file mode 100644 index 0000000..893c2a3 --- /dev/null +++ b/src/classes/FieldsMatch.cls @@ -0,0 +1,129 @@ +public class FieldsMatch implements SObjectPredicate { + + private PrimitiveComparer primitiveComparer = new PrimitiveComparer(); + private SObjectFieldReader fieldReader = new SObjectFieldReader(); + + private static Set setComparisons = new Set{ + Relation.IS_IN, + Relation.NOT_IN + }; + + private List matchConditions; + + public FieldsMatch() { + this.matchConditions = new List(); + } + + public FieldsMatch addCondition(FieldMatchCondition condition) { + matchConditions.add(condition); + return this; + } + + public IncompleteFieldsMatch also(Schema.SObjectField field) { + return field(field); + } + + public IncompleteFieldsMatch also(String fieldPath) { + return field(fieldPath); + } + + public IncompleteFieldsMatch field(Schema.SObjectField field) { + return new IncompleteFieldsMatch(this, field); + } + + public IncompleteFieldsMatch field(String fieldPath) { + return new IncompleteFieldsMatch(this, fieldPath); + } + + public Boolean apply(SObject record) { + for (FieldMatchCondition condition : matchConditions) { + if (!conditionSatisfied(condition, record)) { + return false; + } + } + return true; + } + + private Boolean conditionSatisfied(FieldMatchCondition condition, SObject record) { + Object fieldValue = fieldReader.read(record, condition.fieldPath); + if (setComparisons.contains(condition.relation)) { + return setConditionSatisfied(condition, fieldValue); + } else { + return comparisonConditionSatisfied(condition, fieldValue); + } + } + + private Boolean setConditionSatisfied(FieldMatchCondition condition, Object fieldValue) { + Boolean isValueContained = contains(condition.value, fieldValue); + switch on condition.relation { + when IS_IN { + return isValueContained == true; + } + when NOT_IN { + return isValueContained == false; + } + when else { + return false; + } + } + } + + private Boolean comparisonConditionSatisfied(FieldMatchCondition condition, Object fieldValue) { + ComparisonResult result = primitiveComparer.compare(fieldValue, condition.value); + switch on condition.relation { + when EQUALS { + return result == ComparisonResult.EQUALS; + } + when NOT_EQUALS { + return result == ComparisonResult.NOT_EQUALS; + } + when LESS_THAN { + return result == ComparisonResult.LESS_THAN; + } + when LESS_THAN_OR_EQUALS { + return result == ComparisonResult.LESS_THAN || result == ComparisonResult.EQUALS; + } + when GREATER_THAN { + return result == ComparisonResult.GREATER_THAN; + } + when GREATER_THAN_OR_EQUALS { + return result == ComparisonResult.GREATER_THAN || result == ComparisonResult.EQUALS; + } + when else { + return false; + } + } + } + + private Boolean contains(Object valueSet, Object value) { + if (valueSet instanceof Set) { + Set validValues = (Set) valueSet; + return validValues.contains((Boolean) value); + } else if (valueSet instanceof Set) { + Set validValues = (Set) valueSet; + return validValues.contains((Date) value); + } else if (valueSet instanceof Set) { + Set validValues = (Set) valueSet; + return validValues.contains((Datetime) value); + } else if (valueSet instanceof Set) { + Set validValues = (Set) valueSet; + return validValues.contains((Decimal) value); + } else if (valueSet instanceof Set) { + Set validValues = (Set) valueSet; + return validValues.contains((Double) value); + } else if (valueSet instanceof Set) { + Set validValues = (Set) valueSet; + return validValues.contains((Id) value); + } else if (valueSet instanceof Set) { + Set validValues = (Set) valueSet; + return validValues.contains((Integer) value); + } else if (valueSet instanceof Set) { + Set validValues = (Set) valueSet; + return validValues.contains((Long) value); + } else if (valueSet instanceof Set) { + Set validValues = (Set) valueSet; + return validValues.contains((String) value); + } + throw new LambdaException('Provided set type is not supported by the filter.'); + } +} diff --git a/src/classes/FieldsMatch.cls-meta.xml b/src/classes/FieldsMatch.cls-meta.xml new file mode 100644 index 0000000..45aa0a0 --- /dev/null +++ b/src/classes/FieldsMatch.cls-meta.xml @@ -0,0 +1,5 @@ + + + 44.0 + Active + diff --git a/src/classes/Filter.cls b/src/classes/Filter.cls deleted file mode 100644 index 6835b14..0000000 --- a/src/classes/Filter.cls +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Starting class for filtering queries on the list - */ -public class Filter { - /** - * Initiates a matching filtering query - * @param obj Object to compare to in a query - */ - public static RecordMatchingFilterQuery match(sObject prototype) { - return new RecordMatchingFilterQuery(prototype); - } - - /** - * Initiates a filtering query with field token - * @param token Field token to query - */ - public static PartialFieldFilterQuery field(Schema.SObjectField field) { - return new PartialFieldFilterQuery(new FieldFilterQuery(), field); - } - - /** - * Initiates a filtering query with field relation - * @param token Field token to query - */ - public static PartialFieldFilterQuery field(String fieldRelation) { - return new PartialFieldFilterQuery(new FieldFilterQuery(), fieldRelation); - } -} \ No newline at end of file diff --git a/src/classes/FilterQuery.cls b/src/classes/FilterQuery.cls deleted file mode 100644 index 66903fc..0000000 --- a/src/classes/FilterQuery.cls +++ /dev/null @@ -1,61 +0,0 @@ -public abstract class FilterQuery { - - public abstract Boolean isValid(sObject obj); - - public Iterable applyLazy(Iterable records) { - return new FilterResult(this, records); - } - - /** - * Applies the filter to the list and returns the elements satisfying the filter. - * The original list is not changed. - */ - public List apply(Iterable records) { - return apply(records, List.class); - } - - public List apply(Iterable records, Type listType) { - List filtered = (List) listType.newInstance(); - - Iterator iter = records.iterator(); - while (iter.hasNext()) { - sObject obj = iter.next(); - if (isValid(obj)) { - filtered.add(obj); - } - } - return filtered; - } - - /** - * Applies the filter to the list and returns the elements satisfying the filter. - * The filtered elements are removed from the original list. - */ - public List extract(List records, Type listType) { - - List filtered = (List) listType.newInstance(); - List nonFiltered = (List) listType.newInstance(); - - Iterator iter = records.iterator(); - while (iter.hasNext()) { - sObject obj = iter.next(); - if (isValid(obj)) { - filtered.add(obj); - } else { - nonFiltered.add(obj); - } - } - - records.clear(); - records.addAll(nonFiltered); - return filtered; - } - - /** - * Applies the filter to the list and returns the elements satisfying the filter. - * The filtered elements are removed from the original list. - */ - public List extract(List records) { - return extract(records, List.class); - } -} \ No newline at end of file diff --git a/src/classes/FilterResult.cls b/src/classes/FilterResult.cls deleted file mode 100644 index a8ea919..0000000 --- a/src/classes/FilterResult.cls +++ /dev/null @@ -1,13 +0,0 @@ -public class FilterResult implements Iterable { - private FilterQuery query; - private Iterable records; - - public FilterResult(FilterQuery query, Iterable records) { - this.query = query; - this.records = records; - } - - public Iterator iterator() { - return new FilterResultIterator(query, records.iterator()); - } -} \ No newline at end of file diff --git a/src/classes/FilterResultIterator.cls b/src/classes/FilterResultIterator.cls deleted file mode 100644 index e155eb4..0000000 --- a/src/classes/FilterResultIterator.cls +++ /dev/null @@ -1,36 +0,0 @@ -public class FilterResultIterator implements Iterator { - - private FilterQuery query; - private Iterator objectsIterator; - private sObject next; - - public FilterResultIterator(FilterQuery query, Iterator objectsIterator) { - this.query = query; - this.objectsIterator = objectsIterator; - findNext(); - } - - public Boolean hasNext() { - return next != null; - } - - private void findNext() { - while (objectsIterator.hasNext()) { - sObject obj = objectsIterator.next(); - if (query.isValid(obj)) { - next = obj; - return; - } - } - next = null; - } - - public SObject next() { - if (next == null) { - throw new NoSuchElementException(); - } - SObject current = next; - findNext(); - return current; - } -} \ No newline at end of file diff --git a/src/classes/FilterResultIterator.cls-meta.xml b/src/classes/FilterResultIterator.cls-meta.xml deleted file mode 100644 index 38aa015..0000000 --- a/src/classes/FilterResultIterator.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 36.0 - Active - diff --git a/src/classes/FilterTest.cls b/src/classes/FilterTest.cls deleted file mode 100644 index 3bb8ecc..0000000 --- a/src/classes/FilterTest.cls +++ /dev/null @@ -1,275 +0,0 @@ -@IsTest -private class FilterTest { - static List testData() { - List accounts = new List{ - new Account(Name = 'Ok', AnnualRevenue = 100), - new Account(Name = 'Wrong', AnnualRevenue = 60), - new Account(Name = 'Ok', AnnualRevenue = 150), - new Account(Name = 'Wrong', AnnualRevenue = 150) - }; - return accounts; - } - - static List relationalTestData() { - List accounts = new List{ - new Account(Name = 'Ok', AnnualRevenue = 100), - new Account(Name = 'Wrong', AnnualRevenue = 60) - }; - List opps = new List{ - new Opportunity( - Name = 'First', - CloseDate = Date.today().addDays(3), - Account = accounts[0] - ), - new Opportunity( - Name = 'Second', - CloseDate = Date.today().addDays(6), - Account = accounts[1] - ) - }; - return opps; - } - - @IsTest - static void testRelationalFiltering() { - List opps = relationalTestData(); - List filtered = (List) Filter.field('Account.AnnualRevenue').greaterThan(70).apply(opps); - System.assertEquals(1, filtered.size()); - System.assertEquals('First', filtered[0].Name); - } - - @IsTest - static void testHasValue() { - List accounts = testData(); - List filtered = (List) Filter.field(Account.Industry).hasValue().apply(accounts); - System.assertEquals(0, filtered.size()); - - filtered = (List) Filter.field(Account.Name).hasValue().apply(accounts); - System.assertEquals(4, filtered.size()); - } - - @IsTest - static void testIsIn() { - List accounts = testData(); - List filtered = (List) Filter.field(Account.AnnualRevenue).isIn(new Set{60, 150}).apply(accounts); - System.assertEquals(3, filtered.size()); - for (Account acc : filtered) { - System.assert(acc.AnnualRevenue == 60 || acc.AnnualRevenue == 150); - } - } - - @IsTest - static void testIsNotIn() { - List accounts = testData(); - List filtered = (List) Filter.field(Account.AnnualRevenue).notIn(new Set{60}).apply(accounts); - System.assertEquals(3, filtered.size()); - for (Account acc : filtered) { - System.assert(acc.AnnualRevenue == 100 || acc.AnnualRevenue == 150); - } - } - - @IsTest - static void testFieldEqualsOkFilter() { - List accounts = testData(); - - List filtered = (List) Filter.field(Account.Name).equals('Ok').apply(accounts); - - System.assertEquals(2, filtered.size()); - for (Account acc : filtered) { - System.assertEquals('Ok', acc.Name); - } - - System.assertEquals(4, accounts.size()); - - List extracted = (List) Filter.field(Account.Name).equals('Ok').extract(accounts); - - System.assertEquals(2, accounts.size()); - System.assertEquals(2, extracted.size()); - for (Account acc : extracted) { - System.assertEquals('Ok', acc.Name); - } - } - - @IsTest - static void testMultipleFieldFilter() { - - List accounts = testData(); - List filtered = (List) Filter.field(Account.Name).equals('Ok').also(Account.AnnualRevenue).gt(100).apply(accounts); - - System.assertEquals(1, filtered.size()); - for (Account acc : filtered) { - System.assertEquals('Ok', acc.Name); - System.assert(acc.AnnualRevenue > 100); - } - - List extracted = (List) Filter.field(Account.Name).equals('Ok').also(Account.AnnualRevenue).gt(100).extract(accounts); - - System.assertEquals(3, accounts.size()); - System.assertEquals(1, extracted.size()); - for (Account acc : extracted) { - System.assertEquals('Ok', acc.Name); - System.assert(acc.AnnualRevenue > 100); - } - } - - @IsTest - static void testSameFieldTokenExclusionCriteria() { - - List accounts = testData(); - - List filtered = (List) Filter.field(Account.Name).equals('Ok').also(Account.Name).neq('Ok').apply(accounts); - System.assertEquals(0, filtered.size()); - - List extracted = (List) Filter.field(Account.Name).equals('Ok').also(Account.Name).neq('Ok').extract(accounts); - System.assertEquals(4, accounts.size()); - System.assertEquals(0, extracted.size()); - } - - @IsTest - static void testSameFieldExclusionCriteria() { - List accounts = testData(); - - List filtered = (List) Filter.field(Account.Name).equals('Ok').also(Account.Name).neq('Ok').apply(accounts); - - System.assertEquals(0, filtered.size()); - - List extracted = (List) Filter.field(Account.Name).equals('Ok').also(Account.Name).neq('Ok').extract(accounts); - - System.assertEquals(4, accounts.size()); - System.assertEquals(0, extracted.size()); - } - - @IsTest - static void testLongChaining() { - List accounts = testData(); - - List filtered = (List) Filter.field(Account.Name).equals('Ok').also(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).geq(100).apply(accounts); - - System.assertEquals(1, filtered.size()); - - List extracted = (List) Filter.field(Account.Name).equals('Ok').also(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).geq(100).extract(accounts); - - System.assertEquals(3, accounts.size()); - System.assertEquals(1, extracted.size()); - } - - @IsTest - static void testSameFieldSandwichCriteria() { - List accounts = testData(); - - List filtered = (List) Filter.field(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).gt(60).apply(accounts); - - System.assertEquals(1, filtered.size()); - - List extracted = (List) Filter.field(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).gt(60).extract(accounts); - - System.assertEquals(3, accounts.size()); - System.assertEquals(1, extracted.size()); - } - - @IsTest - static void testSameTokenSandwichCriteria() { - List accounts = testData(); - - List filtered = (List) Filter.field(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).gt(60).apply(accounts); - System.assertEquals(1, filtered.size()); - - List extracted = (List) Filter.field(Account.AnnualRevenue).lt(150).also(Account.AnnualRevenue).gt(60).extract(accounts); - - System.assertEquals(3, accounts.size()); - System.assertEquals(1, extracted.size()); - } - - @IsTest - static void testComparisonFilter() { - List accounts = testData(); - - List filtered = (List) Filter.match(new Account(AnnualRevenue = 150)).apply(accounts); - System.assertEquals(2, filtered.size()); - for (Account acc : filtered) { - System.assertEquals(150, acc.AnnualRevenue); - } - - List extracted = (List) Filter.match(new Account(AnnualRevenue = 150)).extract(accounts); - System.assertEquals(2, accounts.size()); - System.assertEquals(2, extracted.size()); - for (Account acc : extracted) { - System.assertEquals(150, acc.AnnualRevenue); - } - } - - @IsTest - static void testListTyping() { - List accounts = testData(); - List filtered = Filter.field(Account.AnnualRevenue).lt(150).apply(accounts, List.class); - System.assert(filtered instanceof List); - System.assert(!(filtered instanceof List)); - - List filteredWithoutType = Filter.field(Account.AnnualRevenue).lt(150).apply(accounts); - // when no type is provided, the returned list is a list of sObjects which can be a list of anything! - System.assert(filteredWithoutType instanceof List); - System.assert(filteredWithoutType instanceof List); - - List extracted = Filter.field(Account.AnnualRevenue).lt(150).apply(accounts, List.class); - System.assert(extracted instanceof List); - System.assert(!(extracted instanceof List)); - - List extractedWithoutType = Filter.field(Account.AnnualRevenue).lt(150).apply(accounts); - // when no type is provided, the returned list is a list of sObjects which can be a list of anything! - System.assert(extractedWithoutType instanceof List); - System.assert(extractedWithoutType instanceof List); - } - - @IsTest - static void testLazyFiltering() { - List accounts = testData(); - Iterable lazyResults = (Iterable) Filter.field(Account.Name).eq('Ok').applyLazy(accounts); - Iterator iter = lazyResults.iterator(); - System.assert(iter.hasNext()); - Account next = iter.next(); - System.assertEquals('Ok', next.Name); - System.assertEquals(100, next.AnnualRevenue); - System.assert(iter.hasNext()); - next = iter.next(); - System.assertEquals('Ok', next.Name); - System.assertEquals(150, next.AnnualRevenue); - System.assert(!iter.hasNext()); - Boolean isExceptionThrown = false; - try { - iter.next(); - } catch (NoSuchElementException e) { - isExceptionThrown = true; - } - System.assert(isExceptionThrown, 'NoSuchElementException should have been thrown.'); - } - - @IsTest - static void testLazyFilteringWithNoResults() { - List accounts = testData(); - Iterable lazyResults = (Iterable) Filter.field(Account.Name).eq('Foo').applyLazy(accounts); - Iterator iter = lazyResults.iterator(); - System.assert(!iter.hasNext()); - } - - @IsTest - static void testMultipleLazyFiltering() { - List accounts = testData(); - Iterable lazyResults = (Iterable) Filter.field(Account.Name).eq('Ok').applyLazy(accounts); - Integer countOk = 0; - Iterator iter = lazyResults.iterator(); - while (iter.hasNext()) { - iter.next(); - countOk++; - } - System.assertEquals(2, countOk); - - lazyResults = (Iterable) Filter.field(Account.Name).eq('Wrong').applyLazy(accounts); - Integer countWrong = 0; - iter = lazyResults.iterator(); - while (iter.hasNext()) { - iter.next(); - countWrong++; - } - System.assertEquals(2, countWrong); - } -} \ No newline at end of file diff --git a/src/classes/FilterTest.cls-meta.xml b/src/classes/FilterTest.cls-meta.xml deleted file mode 100644 index b211a09..0000000 --- a/src/classes/FilterTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 28.0 - Active - diff --git a/src/classes/GroupBy.cls b/src/classes/GroupBy.cls deleted file mode 100644 index 9c05af9..0000000 --- a/src/classes/GroupBy.cls +++ /dev/null @@ -1,81 +0,0 @@ -public class GroupBy { - public static Map> booleans(Schema.SObjectField field, List records, Type listType) { - Map> grouped = new Map>(); - for (sObject rec : records) { - Boolean key = (Boolean)rec.get(field); - if (!grouped.containsKey(key)) { - grouped.put(key, (List)listType.newInstance()); - } - grouped.get(key).add(rec); - } - return grouped; - } - - public static Map> booleans(Schema.SObjectField field, List records) { - return booleans(field, records, List.class); - } - - public static Map> dates(Schema.SObjectField field, List records, Type listType) { - Map> grouped = new Map>(); - for (sObject rec : records) { - Date key = (Date)rec.get(field); - if (!grouped.containsKey(key)) { - grouped.put(key, (List)listType.newInstance()); - } - grouped.get(key).add(rec); - } - return grouped; - } - - public static Map> dates(Schema.SObjectField field, List records) { - return dates(field, records, List.class); - } - - public static Map> decimals(Schema.SObjectField field, List records, Type listType) { - Map> grouped = new Map>(); - for (sObject rec : records) { - Decimal key = (Decimal)rec.get(field); - if (!grouped.containsKey(key)) { - grouped.put(key, (List)listType.newInstance()); - } - grouped.get(key).add(rec); - } - return grouped; - } - - public static Map> decimals(Schema.SObjectField field, List records) { - return decimals(field, records, List.class); - } - - public static Map> ids(Schema.SObjectField field, List records, Type listType) { - Map> grouped = new Map>(); - for (sObject rec : records) { - Id key = (Id)rec.get(field); - if (!grouped.containsKey(key)) { - grouped.put(key, (List)listType.newInstance()); - } - grouped.get(key).add(rec); - } - return grouped; - } - - public static Map> ids(Schema.SObjectField field, List records) { - return ids(field, records, List.class); - } - - public static Map> strings(Schema.SObjectField field, List records, Type listType) { - Map> grouped = new Map>(); - for (sObject rec : records) { - String key = (String)rec.get(field); - if (!grouped.containsKey(key)) { - grouped.put(key, (List)listType.newInstance()); - } - grouped.get(key).add(rec); - } - return grouped; - } - - public static Map> strings(Schema.SObjectField field, List records) { - return strings(field, records, List.class); - } -} diff --git a/src/classes/GroupBy.cls-meta.xml b/src/classes/GroupBy.cls-meta.xml deleted file mode 100644 index 38aa015..0000000 --- a/src/classes/GroupBy.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 36.0 - Active - diff --git a/src/classes/GroupByTest.cls b/src/classes/GroupByTest.cls deleted file mode 100644 index 1f641a1..0000000 --- a/src/classes/GroupByTest.cls +++ /dev/null @@ -1,96 +0,0 @@ -@IsTest -private class GroupByTest { - static Id firstUserId = TestUtility.getTestId(User.SObjectType); - static Id secondUserId = TestUtility.getTestId(User.SObjectType); - - static List testData() { - List accounts = new List(); - accounts.add(new Account(OwnerId = firstUserId, Name = 'Foo', AnnualRevenue = 100)); - accounts.add(new Account(OwnerId = firstUserId, Name = 'Bar', AnnualRevenue = 60)); - accounts.add(new Account(OwnerId = firstUserId, Name = 'Foo', AnnualRevenue = 150)); - accounts.add(new Account(OwnerId = secondUserId, Name = 'Bar', AnnualRevenue = 150)); - return accounts; - } - - @IsTest - public static void testGroupByStrings() { - List accounts = testData(); - Map> accountsByName = GroupBy.strings(Account.Name, accounts); - System.assertEquals(2, accountsByName.size()); - System.assert(accountsByName.keySet().contains('Foo')); - System.assert(accountsByName.keySet().contains('Bar')); - System.assertEquals(2, accountsByName.get('Foo').size()); - System.assertEquals(2, accountsByName.get('Bar').size()); - for (Account a : accountsByName.get('Foo')) { - System.assertEquals('Foo', a.Name); - } - for (Account a : accountsByName.get('Bar')) { - System.assertEquals('Bar', a.Name); - } - } - - @IsTest - public static void testGroupByStringTyping() { - List accounts = testData(); - Map> accountsByName = GroupBy.strings(Account.Name, accounts); - List fooAccounts = accountsByName.get('Foo'); - List objects = fooAccounts; - // since fooAccounts points to a returned list of SObjects, it can be anything! - System.assert(objects instanceof List); - - accountsByName = GroupBy.strings(Account.Name, accounts, List.class); - fooAccounts = accountsByName.get('Foo'); - objects = fooAccounts; - // this time around, it works fine! - System.assert(!(objects instanceof List)); - System.assert(objects instanceof List); - } - - @IsTest - public static void testGroupByDecimals() { - List accounts = testData(); - Map> accountsByRevenue = GroupBy.decimals(Account.AnnualRevenue, accounts); - System.assertEquals(3, accountsByRevenue.size()); - System.assert(accountsByRevenue.keySet().contains(60)); - System.assert(accountsByRevenue.keySet().contains(100)); - System.assert(accountsByRevenue.keySet().contains(150)); - System.assertEquals(1, accountsByRevenue.get(60).size()); - System.assertEquals(1, accountsByRevenue.get(100).size()); - System.assertEquals(2, accountsByRevenue.get(150).size()); - for (Account a : accountsByRevenue.get(150)) { - System.assertEquals(150.0, a.AnnualRevenue); - } - } - - @IsTest - public static void testGroupByIds() { - List accounts = testData(); - Map> accountsByOwners = GroupBy.ids(Account.OwnerId, accounts); - System.assertEquals(2, accountsByOwners.size()); - System.assert(accountsByOwners.keySet().contains(firstUserId)); - System.assert(accountsByOwners.keySet().contains(secondUserId)); - System.assertEquals(3, accountsByOwners.get(firstUserId).size()); - System.assertEquals(1, accountsByOwners.get(secondUserId).size()); - for (Account a : accountsByOwners.get(firstUserId)) { - System.assertEquals(firstUserId, a.OwnerId); - } - } - - @IsTest - public static void testGroupByBooleans() { - List users = new List{ - new User(Title = 'Foo', IsActive = true), - new User(Title = 'Bar', IsActive = true), - new User(Title = 'Baz', IsActive = false) - }; - Map> usersByActive = GroupBy.booleans(User.IsActive, users); - System.assertEquals(2, usersByActive.size()); - System.assert(usersByActive.keySet().contains(true)); - System.assert(usersByActive.keySet().contains(false)); - System.assertEquals(2, usersByActive.get(true).size()); - System.assertEquals(1, usersByActive.get(false).size()); - for (User u : usersByActive.get(true)) { - System.assertEquals(true, u.IsActive); - } - } -} diff --git a/src/classes/GroupByTest.cls-meta.xml b/src/classes/GroupByTest.cls-meta.xml deleted file mode 100644 index 38aa015..0000000 --- a/src/classes/GroupByTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 36.0 - Active - diff --git a/src/classes/IncompleteFieldsMatch.cls b/src/classes/IncompleteFieldsMatch.cls new file mode 100644 index 0000000..4106462 --- /dev/null +++ b/src/classes/IncompleteFieldsMatch.cls @@ -0,0 +1,84 @@ +public class IncompleteFieldsMatch { + private FieldsMatch baseMatch; + private String fieldPath; + + public IncompleteFieldsMatch(FieldsMatch baseMatch, Schema.SObjectField field) { + this.baseMatch = baseMatch; + this.fieldPath = field.getDescribe().getName(); + } + + public IncompleteFieldsMatch(FieldsMatch baseMatch, String fieldPath) { + this.baseMatch = baseMatch; + this.fieldPath = fieldPath; + } + + // helper for all other methods + private FieldsMatch filterWith(Relation relation, Object value) { + baseMatch.addCondition(new FieldMatchCondition(fieldPath, relation, value)); + return baseMatch; + } + + public FieldsMatch equals(Object value) { + return filterWith(Relation.EQUALS, value); + } + + public FieldsMatch eq(Object value) { + return equals(value); + } + + public FieldsMatch notEquals(Object value) { + return filterWith(Relation.NOT_EQUALS, value); + } + + public FieldsMatch neq(Object value) { + return notEquals(value); + } + + public FieldsMatch lessThan(Object value) { + return filterWith(Relation.LESS_THAN, value); + } + + public FieldsMatch lt(Object value) { + return lessThan(value); + } + + public FieldsMatch lessThanOrEquals(Object value) { + return filterWith(Relation.LESS_THAN_OR_EQUALS, value); + } + + public FieldsMatch leq(Object value) { + return lessThanOrEquals(value); + } + + public FieldsMatch greaterThan(Object value) { + return filterWith(Relation.GREATER_THAN, value); + } + + public FieldsMatch gt(Object value) { + return greaterThan(value); + } + + public FieldsMatch greaterThanOrEquals(Object value) { + return filterWith(Relation.GREATER_THAN_OR_EQUALS, value); + } + + public FieldsMatch geq(Object value) { + return greaterThanOrEquals(value); + } + + public FieldsMatch hasValue() { + return notEquals(null); + } + + public FieldsMatch isIn(Object value) { + return filterWith(Relation.IS_IN, value); + } + + public FieldsMatch notIn(Object value) { + return filterWith(Relation.NOT_IN, value); + } + + public FieldsMatch isNotIn(Object value) { + return notIn(value); + } +} diff --git a/src/classes/IncompleteFieldsMatch.cls-meta.xml b/src/classes/IncompleteFieldsMatch.cls-meta.xml new file mode 100644 index 0000000..45aa0a0 --- /dev/null +++ b/src/classes/IncompleteFieldsMatch.cls-meta.xml @@ -0,0 +1,5 @@ + + + 44.0 + Active + diff --git a/src/classes/Match.cls b/src/classes/Match.cls new file mode 100644 index 0000000..045a654 --- /dev/null +++ b/src/classes/Match.cls @@ -0,0 +1,13 @@ +public class Match { + public static RecordMatch record(SObject record) { + return new RecordMatch(record); + } + + public static IncompleteFieldsMatch field(Schema.SObjectField field) { + return new IncompleteFieldsMatch(new FieldsMatch(), field); + } + + public static IncompleteFieldsMatch field(String fieldPath) { + return new IncompleteFieldsMatch(new FieldsMatch(), fieldPath); + } +} diff --git a/src/classes/Match.cls-meta.xml b/src/classes/Match.cls-meta.xml new file mode 100644 index 0000000..45aa0a0 --- /dev/null +++ b/src/classes/Match.cls-meta.xml @@ -0,0 +1,5 @@ + + + 44.0 + Active + diff --git a/src/classes/PartialFieldFilterQuery.cls b/src/classes/PartialFieldFilterQuery.cls deleted file mode 100644 index 8e25ea3..0000000 --- a/src/classes/PartialFieldFilterQuery.cls +++ /dev/null @@ -1,112 +0,0 @@ -public class PartialFieldFilterQuery { - private FieldFilterQuery query; - private String fieldRelation; - - /** - * Constructor. Takes a valid query and a field name to append to the query - * @param query Field filtering query - * @param field Field to be appended to the query along with an operation - */ - public PartialFieldFilterQuery(FieldFilterQuery query, Schema.SObjectField field) { - this.query = query; - this.fieldRelation = field.getDescribe().getName(); - } - - /** - * Constructor. Takes a valid query and a field name to append to the query - * @param query Field filtering query - * @param field Field to be appended to the query along with an operation - */ - public PartialFieldFilterQuery(FieldFilterQuery query, String fieldRelation) { - this.query = query; - this.fieldRelation = fieldRelation; - } - - - /** - * Helper method. Adds a filtering criterium to the query that consists of - * a field with which the element was constructed, a comparison criterium and - * a value to compare with - * @param criterium Comparison criterium - * @param value Value to compare field with - */ - private FieldFilterQuery filterWith(Comparison criterium, Object value) { - return query.addCriterium(fieldRelation, criterium, value); - } - - /** - * Equality comparison - */ - public FieldFilterQuery equals(Object value) { - return filterWith(Comparison.EQUALS, value); - } - - /** - * equals alias method - */ - public FieldFilterQuery eq(Object value) { - return equals(value); - } - - /** - * Inequality comparison - */ - public FieldFilterQuery notEquals(Object value) { - return filterWith(Comparison.NOT_EQUALS, value); - } - - /** - * notEquals alias method - */ - public FieldFilterQuery neq(Object value) { - return notEquals(value); - } - - public FieldFilterQuery lessThan(Object value) { - return filterWith(Comparison.LESS_THAN, value); - } - - public FieldFilterQuery lt(Object value) { - return lessThan(value); - } - - public FieldFilterQuery lessThanOrEquals(Object value) { - return filterWith(Comparison.LESS_THAN_OR_EQUALS, value); - } - - public FieldFilterQuery leq(Object value) { - return lessThanOrEquals(value); - } - - public FieldFilterQuery greaterThan(Object value) { - return filterWith(Comparison.GREATER_THAN, value); - } - - public FieldFilterQuery gt(Object value) { - return greaterThan(value); - } - - public FieldFilterQuery greaterThanOrEquals(Object value) { - return filterWith(Comparison.GREATER_THAN_OR_EQUALS, value); - } - - public FieldFilterQuery geq(Object value) { - return greaterThanOrEquals(value); - } - - public FieldFilterQuery hasValue() { - return notEquals(null); - } - - public FieldFilterQuery isIn(Object value) { - return filterWith(Comparison.IS_IN, value); - } - - public FieldFilterQuery notIn(Object value) { - return filterWith(Comparison.NOT_IN, value); - } - - public FieldFilterQuery isNotIn(Object value) { - return notIn(value); - } -} \ No newline at end of file diff --git a/src/classes/Pick.cls b/src/classes/Pick.cls deleted file mode 100644 index 4dfc2b9..0000000 --- a/src/classes/Pick.cls +++ /dev/null @@ -1,36 +0,0 @@ -public class Pick { - - public static List fields(List fields, Iterable records) { - return Pick.fields(new Set(fields), records); - } - - public static List fields(Set fields, Iterable records) { - Set fieldNames = new Set(); - for (Schema.SObjectField field : fields) { - Schema.DescribeFieldResult describe = field.getDescribe(); - fieldNames.add(describe.getName()); - } - return Pick.fields(fieldNames, records); - } - - public static List fields(List apiFieldNames, Iterable records) { - return Pick.fields(new Set(apiFieldNames), records); - } - - public static List fields(Set apiFieldNames, Iterable records) { - List results = new List(); - Iterator iter = records.iterator(); - while (iter.hasNext()) { - SObject obj = iter.next(); - SObject picked = obj.getSObjectType().newSObject(); - Map fieldMap = obj.getPopulatedFieldsAsMap(); - for (String fieldName : apiFieldNames) { - if (fieldMap.containsKey(fieldName)) { - picked.put(fieldName, obj.get(fieldName)); - } - } - results.add(picked); - } - return results; - } -} diff --git a/src/classes/Pick.cls-meta.xml b/src/classes/Pick.cls-meta.xml deleted file mode 100644 index 38aa015..0000000 --- a/src/classes/Pick.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 36.0 - Active - diff --git a/src/classes/PickTest.cls b/src/classes/PickTest.cls deleted file mode 100644 index ffd3703..0000000 --- a/src/classes/PickTest.cls +++ /dev/null @@ -1,56 +0,0 @@ -@IsTest -private class PickTest { - @IsTest - public static void pickShouldPickFields() { - List accounts = new List{ - new Account(Name = 'Test1', AnnualRevenue = 100), - new Account(Name = 'Test2', AnnualRevenue = 200) - }; - verifyNamePick(Pick.fields(new List{Account.Name}, accounts)); - verifyNamePick(Pick.fields(new Set{Account.Name}, accounts)); - verifyNamePick(Pick.fields(new List{'Name'}, accounts)); - verifyNamePick(Pick.fields(new Set{'Name'}, accounts)); - } - - @IsTest - public static void pickedFieldsShouldHaveValues() { - List opportunities = new List{ - new Opportunity(Name = 'Test', Amount = 100, Description = 'Test description') - }; - List picked = Pick.fields(new List{'Name', 'Amount'}, opportunities); - System.assertEquals(1, picked.size()); - for (Opportunity opp : picked) { - System.assertEquals('Test', opp.Name); - System.assertEquals(100, opp.Amount); - } - } - - @IsTest - public static void pickShouldPickHeterogenousRecords() { - List records = new List{ - new Account(Name = 'Test1', AnnualRevenue = 100), - new Opportunity(Name = 'Test1', Description = 'Test description') - }; - verifyNamePick(Pick.fields(new List{'Name'}, records)); - verifyNamePick(Pick.fields(new Set{'Name'}, records)); - } - - @IsTest - public static void pickShouldHaveMatchingObjectTypes() { - List records = new List{ - new Account(Name = 'Test1', AnnualRevenue = 100), - new Opportunity(Name = 'Test1', Description = 'Test description') - }; - List picked = Pick.fields(new List{'Name'}, records); - System.assertEquals(Account.sObjectType, picked[0].getSObjectType(), 'First picked element should be an Account.'); - System.assertEquals(Opportunity.sObjectType, picked[1].getSObjectType(), 'Second picked element should be an Opportunity.'); - } - - private static void verifyNamePick(List picked) { - for (SObject obj : picked) { - Map fields = obj.getPopulatedFieldsAsMap(); - System.assertEquals(1, fields.size()); - System.assert(fields.containsKey('Name')); - } - } -} diff --git a/src/classes/PickTest.cls-meta.xml b/src/classes/PickTest.cls-meta.xml deleted file mode 100644 index 38aa015..0000000 --- a/src/classes/PickTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 36.0 - Active - diff --git a/src/classes/Pluck.cls b/src/classes/Pluck.cls deleted file mode 100644 index 0c8a753..0000000 --- a/src/classes/Pluck.cls +++ /dev/null @@ -1,84 +0,0 @@ -public class Pluck { - public static List booleans(Schema.SObjectField field, List records) { - List results = new List(); - for (SObject rec : records) { - results.add((Boolean)rec.get(field)); - } - return results; - } - public static List dates(Schema.SObjectField field, List records) { - List results = new List(); - for (SObject rec : records) { - results.add((Date)rec.get(field)); - } - return results; - } - public static List decimals(Schema.SObjectField field, List records) { - List results = new List(); - for (SObject rec : records) { - results.add((Decimal)rec.get(field)); - } - return results; - } - public Static Set ids(Schema.SObjectField field, List records) { - Set results = new Set(); - for (SObject rec : records) { - results.add((Id)rec.get(field)); - } - return results; - } - public Static Set ids(List records) { - Set results = new Set(); - for (SObject rec : records) { - results.add(rec.Id); - } - return results; - } - public static List strings(Schema.SObjectField field, List records) { - List results = new List(); - for (SObject rec : records) { - results.add((String)rec.get(field)); - } - return results; - } - public static List booleans(String relation, List records) { - List results = new List(); - RelationFieldReader reader = new RelationFieldReader(); - for (SObject rec : records) { - results.add((Boolean)reader.read(rec, relation)); - } - return results; - } - public static List dates(String relation, List records) { - List results = new List(); - RelationFieldReader reader = new RelationFieldReader(); - for (SObject rec : records) { - results.add((Date)reader.read(rec, relation)); - } - return results; - } - public static List decimals(String relation, List records) { - List results = new List(); - RelationFieldReader reader = new RelationFieldReader(); - for (SObject rec : records) { - results.add((Decimal)reader.read(rec, relation)); - } - return results; - } - public static List ids(String relation, List records) { - List results = new List(); - RelationFieldReader reader = new RelationFieldReader(); - for (SObject rec : records) { - results.add((Id)reader.read(rec, relation)); - } - return results; - } - public static List strings(String relation, List records) { - List results = new List(); - RelationFieldReader reader = new RelationFieldReader(); - for (SObject rec : records) { - results.add((String) reader.read(rec, relation)); - } - return results; - } -} diff --git a/src/classes/Pluck.cls-meta.xml b/src/classes/Pluck.cls-meta.xml deleted file mode 100644 index 38aa015..0000000 --- a/src/classes/Pluck.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 36.0 - Active - diff --git a/src/classes/PluckTest.cls b/src/classes/PluckTest.cls deleted file mode 100644 index 0155afe..0000000 --- a/src/classes/PluckTest.cls +++ /dev/null @@ -1,81 +0,0 @@ -@IsTest -private class PluckTest { - static Id firstUserId = TestUtility.getTestId(User.SObjectType); - static Id secondUserId = TestUtility.getTestId(User.SObjectType); - - static List testData() { - List accounts = new List(); - accounts.add(new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = firstUserId, Name = 'Foo', AnnualRevenue = 100)); - accounts.add(new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = firstUserId, Name = 'Bar', AnnualRevenue = 60)); - accounts.add(new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = secondUserId, Name = 'Foo', AnnualRevenue = 150)); - accounts.add(new Account(Id = TestUtility.getTestId(Account.SObjectType), OwnerId = secondUserId, Name = 'Bar', AnnualRevenue = 150)); - return accounts; - } - - @IsTest - public static void testPluckDecimals() { - List accounts = testData(); - List revenues = Pluck.decimals(Account.AnnualRevenue, accounts); - System.assertEquals(4, revenues.size()); - System.assertEquals(100.0, revenues[0]); - System.assertEquals(60.0, revenues[1]); - System.assertEquals(150.0, revenues[2]); - System.assertEquals(150.0, revenues[3]); - } - - @IsTest - public static void testPluckStrings() { - List accounts = testData(); - List names = Pluck.strings(Account.Name, accounts); - System.assertEquals(4, names.size()); - System.assertEquals('Foo', names[0]); - System.assertEquals('Bar', names[1]); - System.assertEquals('Foo', names[2]); - System.assertEquals('Bar', names[3]); - } - - @IsTest - public static void testPluckIdsAsStrings() { - List accounts = testData(); - List ownerIds = Pluck.strings(Account.OwnerId, accounts); - System.assertEquals(4, ownerIds.size()); - System.assertEquals(firstUserId, ownerIds[0]); - System.assertEquals(firstUserId, ownerIds[1]); - System.assertEquals(secondUserId, ownerIds[2]); - System.assertEquals(secondUserId, ownerIds[3]); - } - - @IsTest - public static void testPluckIds() { - List accounts = testData(); - Set ownerIds = Pluck.ids(Account.OwnerId, accounts); - System.assertEquals(2, ownerIds.size()); - System.assert(ownerIds.contains(firstUserId)); - System.assert(ownerIds.contains(secondUserId)); - } - - @IsTest - public static void testPluckRecordIds() { - List accounts = testData(); - Set recordIds = Pluck.ids(accounts); - System.assertEquals(4, recordIds.size()); - System.assert(recordIds.contains(accounts[0].Id)); - System.assert(recordIds.contains(accounts[1].Id)); - System.assert(recordIds.contains(accounts[2].Id)); - System.assert(recordIds.contains(accounts[3].Id)); - } - - @IsTest - public static void testPluckBooleans() { - List users = new List{ - new User(Title = 'Foo', IsActive = true), - new User(Title = 'Bar', IsActive = true), - new User(Title = 'Baz', IsActive = false) - }; - List activity = Pluck.booleans(User.IsActive, users); - System.assertEquals(3, activity.size()); - System.assertEquals(true, activity[0]); - System.assertEquals(true, activity[1]); - System.assertEquals(false, activity[2]); - } -} diff --git a/src/classes/PluckTest.cls-meta.xml b/src/classes/PluckTest.cls-meta.xml deleted file mode 100644 index 38aa015..0000000 --- a/src/classes/PluckTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 36.0 - Active - diff --git a/src/classes/PrimitiveComparer.cls b/src/classes/PrimitiveComparer.cls index 1e2b44d..dcc3793 100644 --- a/src/classes/PrimitiveComparer.cls +++ b/src/classes/PrimitiveComparer.cls @@ -1,104 +1,106 @@ public class PrimitiveComparer { - public Comparison compareBooleans(Boolean a, Boolean b) { + public ComparisonResult compareBooleans(Boolean a, Boolean b) { if (a == b) { - return Comparison.EQUALS; + return ComparisonResult.EQUALS; } else { - return Comparison.NOT_EQUALS; + return ComparisonResult.NOT_EQUALS; } } - public Comparison compareDates(Date a, Date b) { + public ComparisonResult compareDates(Date a, Date b) { if (a < b) { - return Comparison.LESS_THAN; + return ComparisonResult.LESS_THAN; } else if (a == b) { - return Comparison.EQUALS; + return ComparisonResult.EQUALS; } else { - return Comparison.GREATER_THAN; + return ComparisonResult.GREATER_THAN; } } - public Comparison compareTimes(Time a, Time b) { + public ComparisonResult compareTimes(Time a, Time b) { if (a < b) { - return Comparison.LESS_THAN; + return ComparisonResult.LESS_THAN; } else if (a == b) { - return Comparison.EQUALS; + return ComparisonResult.EQUALS; } else { - return Comparison.GREATER_THAN; + return ComparisonResult.GREATER_THAN; } } - public Comparison compareDatetimes(Datetime a, Datetime b) { + public ComparisonResult compareDatetimes(Datetime a, Datetime b) { if (a < b) { - return Comparison.LESS_THAN; + return ComparisonResult.LESS_THAN; } else if (a == b) { - return Comparison.EQUALS; + return ComparisonResult.EQUALS; } else { - return Comparison.GREATER_THAN; + return ComparisonResult.GREATER_THAN; } } - public Comparison compareDecimals(Decimal a, Decimal b) { + public ComparisonResult compareDecimals(Decimal a, Decimal b) { if (a < b) { - return Comparison.LESS_THAN; + return ComparisonResult.LESS_THAN; } else if (a == b) { - return Comparison.EQUALS; + return ComparisonResult.EQUALS; } else { - return Comparison.GREATER_THAN; + return ComparisonResult.GREATER_THAN; } } - public Comparison compareDoubles(Double a, Double b) { + public ComparisonResult compareDoubles(Double a, Double b) { if (a < b) { - return Comparison.LESS_THAN; + return ComparisonResult.LESS_THAN; } else if (a == b) { - return Comparison.EQUALS; + return ComparisonResult.EQUALS; } else { - return Comparison.GREATER_THAN; + return ComparisonResult.GREATER_THAN; } - } + } - public Comparison compareIds(Id a, Id b) { + public ComparisonResult compareIds(Id a, Id b) { if (a == b) { - return Comparison.EQUALS; + return ComparisonResult.EQUALS; } else { - return Comparison.NOT_EQUALS; + return ComparisonResult.NOT_EQUALS; } } - public Comparison compareIntegers(Integer a, Integer b) { + public ComparisonResult compareIntegers(Integer a, Integer b) { if (a < b) { - return Comparison.LESS_THAN; + return ComparisonResult.LESS_THAN; } else if (a == b) { - return Comparison.EQUALS; + return ComparisonResult.EQUALS; } else { - return Comparison.GREATER_THAN; + return ComparisonResult.GREATER_THAN; } } - public Comparison compareLongs(Long a, Long b) { + public ComparisonResult compareLongs(Long a, Long b) { if (a < b) { - return Comparison.LESS_THAN; + return ComparisonResult.LESS_THAN; } else if (a == b) { - return Comparison.EQUALS; + return ComparisonResult.EQUALS; } else { - return Comparison.GREATER_THAN; + return ComparisonResult.GREATER_THAN; } } - public Comparison compareStrings(String a, String b) { - if (a < b) { - return Comparison.LESS_THAN; + public ComparisonResult compareStrings(String a, String b) { + if (a != b && (a == null || b == null)) { + return ComparisonResult.NOT_EQUALS; + } else if (a < b) { + return ComparisonResult.LESS_THAN; } else if (a == b) { - return Comparison.EQUALS; + return ComparisonResult.EQUALS; } else { - return Comparison.GREATER_THAN; + return ComparisonResult.GREATER_THAN; } } /** * A comparison for primitive data types - */ - public Comparison compare(Object first, Object second) { + */ + public ComparisonResult compare(Object first, Object second) { if (first instanceof Boolean && second instanceof Boolean) { return this.compareBooleans((Boolean)first, (Boolean)second); } @@ -129,7 +131,7 @@ public class PrimitiveComparer { else if (first instanceof Time && second instanceof Time) { return this.compareTimes((Time)first, (Time)second); } else { - return Comparison.NOT_EQUALS; + return ComparisonResult.NOT_EQUALS; } } -} \ No newline at end of file +} diff --git a/src/classes/PrimitiveComparerTest.cls b/src/classes/PrimitiveComparerTest.cls index 007d338..4b9f23b 100644 --- a/src/classes/PrimitiveComparerTest.cls +++ b/src/classes/PrimitiveComparerTest.cls @@ -3,72 +3,72 @@ private class PrimitiveComparerTest { @isTest static void booleanComparison() { PrimitiveComparer comparer = new PrimitiveComparer(); - system.assertEquals(Comparison.EQUALS, comparer.compareBooleans(true, true)); - system.assertEquals(Comparison.EQUALS, comparer.compareBooleans(false, false)); - system.assertEquals(Comparison.NOT_EQUALS, comparer.compareBooleans(false, true)); - system.assertEquals(Comparison.NOT_EQUALS, comparer.compareBooleans(true, false)); + system.assertEquals(Relation.EQUALS, comparer.compareBooleans(true, true)); + system.assertEquals(Relation.EQUALS, comparer.compareBooleans(false, false)); + system.assertEquals(Relation.NOT_EQUALS, comparer.compareBooleans(false, true)); + system.assertEquals(Relation.NOT_EQUALS, comparer.compareBooleans(true, false)); } - + @isTest static void dateComparison() { PrimitiveComparer comparer = new PrimitiveComparer(); - system.assertEquals(Comparison.LESS_THAN, comparer.compareDates(Date.newInstance(2013,5,1), Date.newInstance(2013,6,1))); - system.assertEquals(Comparison.EQUALS, comparer.compareDates(Date.newInstance(2013,4,1), Date.newInstance(2013,4,1))); - system.assertEquals(Comparison.GREATER_THAN, comparer.compareDates(Date.newInstance(2013,8,1), Date.newInstance(2013,7,1))); + system.assertEquals(Relation.LESS_THAN, comparer.compareDates(Date.newInstance(2013,5,1), Date.newInstance(2013,6,1))); + system.assertEquals(Relation.EQUALS, comparer.compareDates(Date.newInstance(2013,4,1), Date.newInstance(2013,4,1))); + system.assertEquals(Relation.GREATER_THAN, comparer.compareDates(Date.newInstance(2013,8,1), Date.newInstance(2013,7,1))); } - + @isTest static void datetimeComparison() { PrimitiveComparer comparer = new PrimitiveComparer(); - system.assertEquals(Comparison.LESS_THAN, comparer.compareDatetimes(Datetime.newInstance(2013,5,1), Datetime.newInstance(2013,6,1))); - system.assertEquals(Comparison.EQUALS, comparer.compareDatetimes(Datetime.newInstance(2013,4,1), Datetime.newInstance(2013,4,1))); - system.assertEquals(Comparison.GREATER_THAN, comparer.compareDatetimes(Datetime.newInstance(2013,8,1), Datetime.newInstance(2013,7,1))); + system.assertEquals(Relation.LESS_THAN, comparer.compareDatetimes(Datetime.newInstance(2013,5,1), Datetime.newInstance(2013,6,1))); + system.assertEquals(Relation.EQUALS, comparer.compareDatetimes(Datetime.newInstance(2013,4,1), Datetime.newInstance(2013,4,1))); + system.assertEquals(Relation.GREATER_THAN, comparer.compareDatetimes(Datetime.newInstance(2013,8,1), Datetime.newInstance(2013,7,1))); } - + @isTest static void timeComparison() { PrimitiveComparer comparer = new PrimitiveComparer(); - system.assertEquals(Comparison.LESS_THAN, comparer.compareTimes(Time.newInstance(1,0,0,0), Time.newInstance(2,0,0,0))); - system.assertEquals(Comparison.EQUALS, comparer.compareTimes(Time.newInstance(3,0,0,0), Time.newInstance(3,0,0,0))); - system.assertEquals(Comparison.GREATER_THAN, comparer.compareTimes(Time.newInstance(5,0,0,0), Time.newInstance(4,0,0,0))); + system.assertEquals(Relation.LESS_THAN, comparer.compareTimes(Time.newInstance(1,0,0,0), Time.newInstance(2,0,0,0))); + system.assertEquals(Relation.EQUALS, comparer.compareTimes(Time.newInstance(3,0,0,0), Time.newInstance(3,0,0,0))); + system.assertEquals(Relation.GREATER_THAN, comparer.compareTimes(Time.newInstance(5,0,0,0), Time.newInstance(4,0,0,0))); } @isTest static void decimalComparison() { PrimitiveComparer comparer = new PrimitiveComparer(); - system.assertEquals(Comparison.LESS_THAN, comparer.compareDecimals(Decimal.valueOf(1), Decimal.valueOf(3))); - system.assertEquals(Comparison.GREATER_THAN, comparer.compareDecimals(Decimal.valueOf(3), Decimal.valueOf(1))); - system.assertEquals(Comparison.EQUALS, comparer.compareDecimals(Decimal.valueOf(1), Decimal.valueOf(1))); + system.assertEquals(Relation.LESS_THAN, comparer.compareDecimals(Decimal.valueOf(1), Decimal.valueOf(3))); + system.assertEquals(Relation.GREATER_THAN, comparer.compareDecimals(Decimal.valueOf(3), Decimal.valueOf(1))); + system.assertEquals(Relation.EQUALS, comparer.compareDecimals(Decimal.valueOf(1), Decimal.valueOf(1))); } @isTest static void doubleComparison() { PrimitiveComparer comparer = new PrimitiveComparer(); - system.assertEquals(Comparison.LESS_THAN, comparer.compareDoubles(Double.valueOf(1), Double.valueOf(3))); - system.assertEquals(Comparison.GREATER_THAN, comparer.compareDoubles(Double.valueOf(3), Double.valueOf(1))); - system.assertEquals(Comparison.EQUALS, comparer.compareDoubles(Double.valueOf(1), Double.valueOf(1))); + system.assertEquals(Relation.LESS_THAN, comparer.compareDoubles(Double.valueOf(1), Double.valueOf(3))); + system.assertEquals(Relation.GREATER_THAN, comparer.compareDoubles(Double.valueOf(3), Double.valueOf(1))); + system.assertEquals(Relation.EQUALS, comparer.compareDoubles(Double.valueOf(1), Double.valueOf(1))); } - + @isTest static void integerComparison() { PrimitiveComparer comparer = new PrimitiveComparer(); - system.assertEquals(Comparison.LESS_THAN, comparer.compareIntegers(1, 3)); - system.assertEquals(Comparison.GREATER_THAN, comparer.compareIntegers(3, 1)); - system.assertEquals(Comparison.EQUALS, comparer.compareIntegers(1, 1)); + system.assertEquals(Relation.LESS_THAN, comparer.compareIntegers(1, 3)); + system.assertEquals(Relation.GREATER_THAN, comparer.compareIntegers(3, 1)); + system.assertEquals(Relation.EQUALS, comparer.compareIntegers(1, 1)); } - + @isTest static void idComparison() { PrimitiveComparer comparer = new PrimitiveComparer(); Id a = '000000000000001', b = '000000000000001', c = '000000000000002'; - system.assertEquals(Comparison.EQUALS, comparer.compareIds(a, b)); - system.assertEquals(Comparison.NOT_EQUALS, comparer.compareIds(a, c)); + system.assertEquals(Relation.EQUALS, comparer.compareIds(a, b)); + system.assertEquals(Relation.NOT_EQUALS, comparer.compareIds(a, c)); } @isTest static void longComparison() { PrimitiveComparer comparer = new PrimitiveComparer(); - system.assertEquals(Comparison.LESS_THAN, comparer.compareLongs(1L, 3L)); - system.assertEquals(Comparison.GREATER_THAN, comparer.compareLongs(3L, 1L)); - system.assertEquals(Comparison.EQUALS, comparer.compareLongs(1L, 1L)); + system.assertEquals(Relation.LESS_THAN, comparer.compareLongs(1L, 3L)); + system.assertEquals(Relation.GREATER_THAN, comparer.compareLongs(3L, 1L)); + system.assertEquals(Relation.EQUALS, comparer.compareLongs(1L, 1L)); } - + @isTest static void stringComparison() { PrimitiveComparer comparer = new PrimitiveComparer(); - system.assertEquals(Comparison.LESS_THAN, comparer.compareStrings('abc', 'bbc')); - system.assertEquals(Comparison.GREATER_THAN, comparer.compareStrings('cdc', 'bbc')); - system.assertEquals(Comparison.EQUALS, comparer.compareStrings('efg', 'efg')); + system.assertEquals(Relation.LESS_THAN, comparer.compareStrings('abc', 'bbc')); + system.assertEquals(Relation.GREATER_THAN, comparer.compareStrings('cdc', 'bbc')); + system.assertEquals(Relation.EQUALS, comparer.compareStrings('efg', 'efg')); } -} \ No newline at end of file +} diff --git a/src/classes/RecordMatch.cls b/src/classes/RecordMatch.cls new file mode 100644 index 0000000..508d565 --- /dev/null +++ b/src/classes/RecordMatch.cls @@ -0,0 +1,18 @@ +public class RecordMatch implements SObjectPredicate { + private SObject prototype; + private Map populatedFieldsMap; + + public Boolean apply(SObject record) { + for (String field : populatedFieldsMap.keySet()) { + if (record.get(field) != prototype.get(field)) { + return false; + } + } + return true; + } + + public RecordMatch(sObject prototype) { + this.prototype = prototype; + this.populatedFieldsMap = prototype.getPopulatedFieldsAsMap(); + } +} diff --git a/src/classes/RecordMatch.cls-meta.xml b/src/classes/RecordMatch.cls-meta.xml new file mode 100644 index 0000000..45aa0a0 --- /dev/null +++ b/src/classes/RecordMatch.cls-meta.xml @@ -0,0 +1,5 @@ + + + 44.0 + Active + diff --git a/src/classes/RecordMatchingFilterQuery.cls b/src/classes/RecordMatchingFilterQuery.cls deleted file mode 100644 index 6b12916..0000000 --- a/src/classes/RecordMatchingFilterQuery.cls +++ /dev/null @@ -1,31 +0,0 @@ -public class RecordMatchingFilterQuery extends FilterQuery { - - private sObject record; - private Map availableFields = new Map(); - - /** - * Constructor. Takes a comparison sObject to compare list elements with. - * The comparison checks for equality with the comparison object and only - * non-null fields are considered. - * @param obj Comparison sObject - */ - public RecordMatchingFilterQuery(sObject record) { - this.record = record; - for (String field : record.getSObjectType().getDescribe().fields.getMap().keyset()) { - if (record.get(field) != null) { - availableFields.put(field, record.get(field)); - } - } - } - - public override Boolean isValid(sObject o) { - Boolean isValid = true; - for (String field : availableFields.keySet()) { - if (record.get(field) != o.get(field)) { - isValid = false; - break; - } - } - return isValid; - } -} \ No newline at end of file diff --git a/src/classes/RecordMatchingFilterQuery.cls-meta.xml b/src/classes/RecordMatchingFilterQuery.cls-meta.xml deleted file mode 100644 index f3bac1f..0000000 --- a/src/classes/RecordMatchingFilterQuery.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 41.0 - Active - diff --git a/src/classes/Comparison.cls b/src/classes/Relation.cls similarity index 78% rename from src/classes/Comparison.cls rename to src/classes/Relation.cls index 9b6aacc..8ecce00 100644 --- a/src/classes/Comparison.cls +++ b/src/classes/Relation.cls @@ -1,3 +1,3 @@ -public enum Comparison { +public enum Relation { LESS_THAN, GREATER_THAN, EQUALS, NOT_EQUALS, LESS_THAN_OR_EQUALS, GREATER_THAN_OR_EQUALS, IS_IN, NOT_IN -} \ No newline at end of file +} diff --git a/src/classes/Comparison.cls-meta.xml b/src/classes/Relation.cls-meta.xml similarity index 100% rename from src/classes/Comparison.cls-meta.xml rename to src/classes/Relation.cls-meta.xml diff --git a/src/classes/RelationFieldReader.cls-meta.xml b/src/classes/RelationFieldReader.cls-meta.xml deleted file mode 100644 index f3bac1f..0000000 --- a/src/classes/RelationFieldReader.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 41.0 - Active - diff --git a/src/classes/RelationFieldReaderTest.cls-meta.xml b/src/classes/RelationFieldReaderTest.cls-meta.xml deleted file mode 100644 index f3bac1f..0000000 --- a/src/classes/RelationFieldReaderTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 41.0 - Active - diff --git a/src/classes/RelationFieldReader.cls b/src/classes/SObjectFieldReader.cls similarity index 54% rename from src/classes/RelationFieldReader.cls rename to src/classes/SObjectFieldReader.cls index 1a07cbe..dc37e24 100644 --- a/src/classes/RelationFieldReader.cls +++ b/src/classes/SObjectFieldReader.cls @@ -1,35 +1,35 @@ -public class RelationFieldReader { - public Object read(SObject record, String relation) { - if (String.isBlank(relation)) { - throw new LambdaException('Provided relation is blank'); +public class SObjectFieldReader { + public Object read(SObject record, String fieldPath) { + if (String.isBlank(fieldPath)) { + throw new LambdaException('Provided field path is blank'); } - String[] relationParts = relation.split('\\.'); + String[] pathParts = fieldPath.split('\\.'); SObject currentRecord = record; - for (Integer i = 0; i < relationParts.size() - 1; i++) { - String relationPart = relationParts[i]; + for (Integer i = 0; i < pathParts.size() - 1; i++) { + String relationPart = pathParts[i]; try { SObject nextRecord = currentRecord.getSObject(relationPart); if (nextRecord == null) { throw new LambdaException(String.format('Cannot resolve "{0}" ({1}) on {2} object', new String[]{ - relationPart, relation, currentRecord.getSObjectType().getDescribe().getName() + relationPart, fieldPath, currentRecord.getSObjectType().getDescribe().getName() })); } currentRecord = currentRecord.getSObject(relationPart); } catch (SObjectException e) { throw new LambdaException(String.format('Cannot resolve "{0}" ({1}) on {2} object', new String[]{ - relationPart, relation, currentRecord.getSObjectType().getDescribe().getName() + relationPart, fieldPath, currentRecord.getSObjectType().getDescribe().getName() })); } } - String lastPart = relationParts[relationParts.size() - 1]; + String lastPart = pathParts[pathParts.size() - 1]; Object fieldValue; try { fieldValue = currentRecord.get(lastPart); } catch (SObjectException e) { throw new LambdaException(String.format('Cannot resolve "{0}" ({1}) on {2} object', new String[]{ - lastPart, relation, currentRecord.getSObjectType().getDescribe().getName() + lastPart, fieldPath, currentRecord.getSObjectType().getDescribe().getName() })); } return fieldValue; } -} \ No newline at end of file +} diff --git a/src/classes/FieldFilterQueryCriterium.cls-meta.xml b/src/classes/SObjectFieldReader.cls-meta.xml similarity index 100% rename from src/classes/FieldFilterQueryCriterium.cls-meta.xml rename to src/classes/SObjectFieldReader.cls-meta.xml diff --git a/src/classes/RelationFieldReaderTest.cls b/src/classes/SObjectFieldReaderTest.cls similarity index 82% rename from src/classes/RelationFieldReaderTest.cls rename to src/classes/SObjectFieldReaderTest.cls index be430c3..b3ee295 100644 --- a/src/classes/RelationFieldReaderTest.cls +++ b/src/classes/SObjectFieldReaderTest.cls @@ -1,12 +1,12 @@ @IsTest -private class RelationFieldReaderTest { +private class SObjectFieldReaderTest { @IsTest static void testResolving() { Account acc = new Account(Name = 'Test'); Opportunity opp = new Opportunity(AccountId = acc.Id, Name = 'Opportunity', StageName = 'Prospecting', CloseDate = Date.today().addDays(7)); opp.Account = acc; - Object value = new RelationFieldReader().read(opp, 'Account.Name'); + Object value = new SObjectFieldReader().read(opp, 'Account.Name'); System.assertEquals('Test', value); } @@ -17,7 +17,7 @@ private class RelationFieldReaderTest { Exception ex; try { - new RelationFieldReader().read(opp, 'ObjectThatShouldNotExist.Name'); + new SObjectFieldReader().read(opp, 'ObjectThatShouldNotExist.Name'); } catch (LambdaException e) { ex = e; } @@ -34,7 +34,7 @@ private class RelationFieldReaderTest { Exception ex; try { - new RelationFieldReader().read(opp, 'Account.FieldThatShouldNotExist'); + new SObjectFieldReader().read(opp, 'Account.FieldThatShouldNotExist'); } catch (LambdaException e) { ex = e; } @@ -49,11 +49,11 @@ private class RelationFieldReaderTest { Exception ex; try { - new RelationFieldReader().read(opp, ''); + new SObjectFieldReader().read(opp, ''); } catch (LambdaException e) { ex = e; } System.assert(ex != null); System.assert(ex.getMessage().contains('is blank')); } -} \ No newline at end of file +} diff --git a/src/classes/PartialFieldFilterQuery.cls-meta.xml b/src/classes/SObjectFieldReaderTest.cls-meta.xml similarity index 100% rename from src/classes/PartialFieldFilterQuery.cls-meta.xml rename to src/classes/SObjectFieldReaderTest.cls-meta.xml diff --git a/src/classes/SObjectPredicate.cls b/src/classes/SObjectPredicate.cls new file mode 100644 index 0000000..25465f5 --- /dev/null +++ b/src/classes/SObjectPredicate.cls @@ -0,0 +1,3 @@ +public interface SObjectPredicate { + Boolean apply(SObject record); +} diff --git a/src/classes/SObjectPredicate.cls-meta.xml b/src/classes/SObjectPredicate.cls-meta.xml new file mode 100644 index 0000000..45aa0a0 --- /dev/null +++ b/src/classes/SObjectPredicate.cls-meta.xml @@ -0,0 +1,5 @@ + + + 44.0 + Active + diff --git a/src/classes/SObjectToSObjectFunction.cls b/src/classes/SObjectToSObjectFunction.cls new file mode 100644 index 0000000..46cc81b --- /dev/null +++ b/src/classes/SObjectToSObjectFunction.cls @@ -0,0 +1,3 @@ +public interface SObjectToSObjectFunction { + SObject apply(SObject record); +} diff --git a/src/classes/SObjectToSObjectFunction.cls-meta.xml b/src/classes/SObjectToSObjectFunction.cls-meta.xml new file mode 100644 index 0000000..45aa0a0 --- /dev/null +++ b/src/classes/SObjectToSObjectFunction.cls-meta.xml @@ -0,0 +1,5 @@ + + + 44.0 + Active +