From 5ed9a8ea62ce20fe251623dba98f8f9926b06eb8 Mon Sep 17 00:00:00 2001 From: Gregory Higley Date: Thu, 31 Dec 2015 00:00:14 -0500 Subject: [PATCH] CDQI v3.0 --- .../project.pbxproj | 132 +++--- CoreDataQueryInterface/Aggregable.swift | 20 + CoreDataQueryInterface/Alias.swift | 40 -- CoreDataQueryInterface/Attribute.swift | 80 +++- CoreDataQueryInterface/AttributeType.swift | 27 -- CoreDataQueryInterface/Countable.swift | 19 + .../CustomExpressionConvertible.swift | 69 +++ .../CustomPropertyConvertible.swift | 27 ++ ... => CustomSortDescriptorConvertible.swift} | 16 +- CoreDataQueryInterface/EntityType.swift | 2 +- CoreDataQueryInterface/Executing.swift | 12 +- CoreDataQueryInterface/Expression.swift | 18 - CoreDataQueryInterface/ExpressionHelper.swift | 74 ---- CoreDataQueryInterface/ExpressionType.swift | 59 --- CoreDataQueryInterface/Filtering.swift | 5 +- CoreDataQueryInterface/Function.swift | 92 ---- CoreDataQueryInterface/Grouping.swift | 14 +- CoreDataQueryInterface/KeyAttribute.swift | 16 + CoreDataQueryInterface/Operators.swift | 398 +----------------- CoreDataQueryInterface/Ordering.swift | 48 +-- CoreDataQueryInterface/Predicate.swift | 16 + CoreDataQueryInterface/QueryBuilder.swift | 9 +- CoreDataQueryInterface/Selecting.swift | 14 +- .../BaseTestCase.swift | 2 +- CoreDataQueryInterfaceTests/Department.swift | 3 +- .../DepartmentAttribute.swift | 28 +- CoreDataQueryInterfaceTests/Employee.swift | 3 +- .../EmployeeAttribute.swift | 37 +- CoreDataQueryInterfaceTests/FilterTests.swift | 13 +- CoreDataQueryInterfaceTests/OrderTests.swift | 2 +- .../SelectionAndGroupingTests.swift | 8 +- README.md | 383 ++++++++++++----- README.v2.4.md | 198 +++++++++ bin/cdqi | 213 ++++++++++ 34 files changed, 1108 insertions(+), 989 deletions(-) create mode 100644 CoreDataQueryInterface/Aggregable.swift delete mode 100644 CoreDataQueryInterface/Alias.swift delete mode 100644 CoreDataQueryInterface/AttributeType.swift create mode 100644 CoreDataQueryInterface/Countable.swift create mode 100644 CoreDataQueryInterface/CustomExpressionConvertible.swift create mode 100644 CoreDataQueryInterface/CustomPropertyConvertible.swift rename CoreDataQueryInterface/{OrderType.swift => CustomSortDescriptorConvertible.swift} (74%) delete mode 100644 CoreDataQueryInterface/Expression.swift delete mode 100644 CoreDataQueryInterface/ExpressionHelper.swift delete mode 100644 CoreDataQueryInterface/ExpressionType.swift delete mode 100644 CoreDataQueryInterface/Function.swift create mode 100644 CoreDataQueryInterface/KeyAttribute.swift create mode 100644 README.v2.4.md create mode 100755 bin/cdqi diff --git a/CoreDataQueryInterface.xcodeproj/project.pbxproj b/CoreDataQueryInterface.xcodeproj/project.pbxproj index 95d2063..093361e 100644 --- a/CoreDataQueryInterface.xcodeproj/project.pbxproj +++ b/CoreDataQueryInterface.xcodeproj/project.pbxproj @@ -26,31 +26,35 @@ F3251A131B2BDE6E0031B817 /* Employee.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3251A0F1B2BDE6E0031B817 /* Employee.swift */; }; F33567661B2D2EF7007853DB /* SelectionAndGroupingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33567641B2D2EF7007853DB /* SelectionAndGroupingTests.swift */; }; F33567681B2D2EF7007853DB /* SelectionAndGroupingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33567641B2D2EF7007853DB /* SelectionAndGroupingTests.swift */; }; - F34E74941B2E38C900FAA04A /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E74931B2E38C900FAA04A /* Expression.swift */; }; - F34E74951B2E38C900FAA04A /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E74931B2E38C900FAA04A /* Expression.swift */; }; - F34E74961B2E38C900FAA04A /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E74931B2E38C900FAA04A /* Expression.swift */; }; - F34E74971B2E38C900FAA04A /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E74931B2E38C900FAA04A /* Expression.swift */; }; - F34E74991B2E3A9900FAA04A /* Alias.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E74981B2E3A9900FAA04A /* Alias.swift */; }; - F34E749A1B2E3A9900FAA04A /* Alias.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E74981B2E3A9900FAA04A /* Alias.swift */; }; - F34E749B1B2E3A9900FAA04A /* Alias.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E74981B2E3A9900FAA04A /* Alias.swift */; }; - F34E749C1B2E3A9900FAA04A /* Alias.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E74981B2E3A9900FAA04A /* Alias.swift */; }; + F33863931C322AFE00818352 /* CustomExpressionConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863921C322AFE00818352 /* CustomExpressionConvertible.swift */; }; + F33863941C322AFE00818352 /* CustomExpressionConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863921C322AFE00818352 /* CustomExpressionConvertible.swift */; }; + F33863951C322AFE00818352 /* CustomExpressionConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863921C322AFE00818352 /* CustomExpressionConvertible.swift */; }; + F33863961C322AFE00818352 /* CustomExpressionConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863921C322AFE00818352 /* CustomExpressionConvertible.swift */; }; + F33863981C322BDB00818352 /* Aggregable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863971C322BDB00818352 /* Aggregable.swift */; }; + F33863991C322BDB00818352 /* Aggregable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863971C322BDB00818352 /* Aggregable.swift */; }; + F338639A1C322BDB00818352 /* Aggregable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863971C322BDB00818352 /* Aggregable.swift */; }; + F338639B1C322BDB00818352 /* Aggregable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863971C322BDB00818352 /* Aggregable.swift */; }; + F338639D1C322C0B00818352 /* KeyAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F338639C1C322C0B00818352 /* KeyAttribute.swift */; }; + F338639E1C322C0B00818352 /* KeyAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F338639C1C322C0B00818352 /* KeyAttribute.swift */; }; + F338639F1C322C0B00818352 /* KeyAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F338639C1C322C0B00818352 /* KeyAttribute.swift */; }; + F33863A01C322C0B00818352 /* KeyAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F338639C1C322C0B00818352 /* KeyAttribute.swift */; }; + F33863A21C322DB700818352 /* CustomPropertyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863A11C322DB700818352 /* CustomPropertyConvertible.swift */; }; + F33863A31C322DB700818352 /* CustomPropertyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863A11C322DB700818352 /* CustomPropertyConvertible.swift */; }; + F33863A41C322DB700818352 /* CustomPropertyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863A11C322DB700818352 /* CustomPropertyConvertible.swift */; }; + F33863A51C322DB700818352 /* CustomPropertyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33863A11C322DB700818352 /* CustomPropertyConvertible.swift */; }; F34E749E1B2E576000FAA04A /* Grouping.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E749D1B2E576000FAA04A /* Grouping.swift */; }; F34E749F1B2E576000FAA04A /* Grouping.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E749D1B2E576000FAA04A /* Grouping.swift */; }; F34E74A01B2E576000FAA04A /* Grouping.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E749D1B2E576000FAA04A /* Grouping.swift */; }; F34E74A11B2E576000FAA04A /* Grouping.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E749D1B2E576000FAA04A /* Grouping.swift */; }; - F367DE301B2D6149009C1020 /* OrderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367DE2F1B2D6149009C1020 /* OrderType.swift */; }; - F367DE311B2D6149009C1020 /* OrderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367DE2F1B2D6149009C1020 /* OrderType.swift */; }; - F367DE321B2D6149009C1020 /* OrderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367DE2F1B2D6149009C1020 /* OrderType.swift */; }; - F367DE331B2D6149009C1020 /* OrderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367DE2F1B2D6149009C1020 /* OrderType.swift */; }; + F367DE301B2D6149009C1020 /* CustomSortDescriptorConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367DE2F1B2D6149009C1020 /* CustomSortDescriptorConvertible.swift */; }; + F367DE311B2D6149009C1020 /* CustomSortDescriptorConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367DE2F1B2D6149009C1020 /* CustomSortDescriptorConvertible.swift */; }; + F367DE321B2D6149009C1020 /* CustomSortDescriptorConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367DE2F1B2D6149009C1020 /* CustomSortDescriptorConvertible.swift */; }; + F367DE331B2D6149009C1020 /* CustomSortDescriptorConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367DE2F1B2D6149009C1020 /* CustomSortDescriptorConvertible.swift */; }; F36D9D2B1B40FE5E00A3D4EC /* DepartmentAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36D9D291B40FE5E00A3D4EC /* DepartmentAttribute.swift */; }; F36D9D2C1B40FE5E00A3D4EC /* DepartmentAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36D9D291B40FE5E00A3D4EC /* DepartmentAttribute.swift */; }; F36D9D2D1B40FE5E00A3D4EC /* EmployeeAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36D9D2A1B40FE5E00A3D4EC /* EmployeeAttribute.swift */; }; F36D9D2E1B40FE5E00A3D4EC /* EmployeeAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36D9D2A1B40FE5E00A3D4EC /* EmployeeAttribute.swift */; }; - F371ABA61B2B8EE2002A8A85 /* AttributeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F371ABA51B2B8EE2002A8A85 /* AttributeType.swift */; }; - F371ABA71B2B8EE2002A8A85 /* AttributeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F371ABA51B2B8EE2002A8A85 /* AttributeType.swift */; }; - F371ABB31B2BA5FA002A8A85 /* AttributeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F371ABA51B2B8EE2002A8A85 /* AttributeType.swift */; }; F371ABB41B2BA5FA002A8A85 /* EntityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3158D1B1B2A91040041B125 /* EntityType.swift */; }; - F371ABB51B2BA5FA002A8A85 /* AttributeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F371ABA51B2B8EE2002A8A85 /* AttributeType.swift */; }; F371ABB61B2BA5FA002A8A85 /* EntityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3158D1B1B2A91040041B125 /* EntityType.swift */; }; F371ABB81B2BA8BA002A8A85 /* QueryBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F371ABB71B2BA8BA002A8A85 /* QueryBuilder.swift */; }; F371ABB91B2BA8BA002A8A85 /* QueryBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F371ABB71B2BA8BA002A8A85 /* QueryBuilder.swift */; }; @@ -100,6 +104,10 @@ F3754CCC1B35FBE5004B46B3 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3754CCA1B35FBE5004B46B3 /* Operators.swift */; }; F3754CCD1B35FBE5004B46B3 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3754CCA1B35FBE5004B46B3 /* Operators.swift */; }; F3754CCE1B35FBE5004B46B3 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3754CCA1B35FBE5004B46B3 /* Operators.swift */; }; + F379BB011C3470FA001BC25B /* Countable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F379BB001C3470FA001BC25B /* Countable.swift */; }; + F379BB021C3470FA001BC25B /* Countable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F379BB001C3470FA001BC25B /* Countable.swift */; }; + F379BB031C3470FA001BC25B /* Countable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F379BB001C3470FA001BC25B /* Countable.swift */; }; + F379BB041C3470FA001BC25B /* Countable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F379BB001C3470FA001BC25B /* Countable.swift */; }; F37A46F01B2BD61700D4EE61 /* Attribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37A46EF1B2BD61700D4EE61 /* Attribute.swift */; }; F37A46F11B2BD61700D4EE61 /* Attribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37A46EF1B2BD61700D4EE61 /* Attribute.swift */; }; F37A46F21B2BD61700D4EE61 /* Attribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37A46EF1B2BD61700D4EE61 /* Attribute.swift */; }; @@ -112,18 +120,6 @@ F37C46AD1B2A780200B35B1B /* CoreDataQueryInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F37C46A21B2A780200B35B1B /* CoreDataQueryInterface.framework */; }; F37C46B21B2A780200B35B1B /* SanityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37C46B11B2A780200B35B1B /* SanityTests.swift */; }; F39C7A741B718E34008DB31C /* CoreDataQueryInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = F37C46A51B2A780200B35B1B /* CoreDataQueryInterface.h */; settings = {ATTRIBUTES = (Public, ); }; }; - F3B0E32D1B2DEEB5008D7EEF /* ExpressionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E32C1B2DEEB5008D7EEF /* ExpressionHelper.swift */; }; - F3B0E32E1B2DEEB5008D7EEF /* ExpressionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E32C1B2DEEB5008D7EEF /* ExpressionHelper.swift */; }; - F3B0E32F1B2DEEB5008D7EEF /* ExpressionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E32C1B2DEEB5008D7EEF /* ExpressionHelper.swift */; }; - F3B0E3301B2DEEB5008D7EEF /* ExpressionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E32C1B2DEEB5008D7EEF /* ExpressionHelper.swift */; }; - F3B0E33A1B2DFD70008D7EEF /* Function.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E3391B2DFD70008D7EEF /* Function.swift */; }; - F3B0E33B1B2DFD70008D7EEF /* Function.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E3391B2DFD70008D7EEF /* Function.swift */; }; - F3B0E33C1B2DFD70008D7EEF /* Function.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E3391B2DFD70008D7EEF /* Function.swift */; }; - F3B0E33D1B2DFD70008D7EEF /* Function.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E3391B2DFD70008D7EEF /* Function.swift */; }; - F3B0E33F1B2E0351008D7EEF /* ExpressionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E33E1B2E0351008D7EEF /* ExpressionType.swift */; }; - F3B0E3401B2E0351008D7EEF /* ExpressionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E33E1B2E0351008D7EEF /* ExpressionType.swift */; }; - F3B0E3411B2E0351008D7EEF /* ExpressionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E33E1B2E0351008D7EEF /* ExpressionType.swift */; }; - F3B0E3421B2E0351008D7EEF /* ExpressionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B0E33E1B2E0351008D7EEF /* ExpressionType.swift */; }; F3E6B5501B2BFCA500F735EE /* FilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E6B54F1B2BFCA500F735EE /* FilterTests.swift */; }; F3E6B5511B2BFCA500F735EE /* FilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E6B54F1B2BFCA500F735EE /* FilterTests.swift */; }; F3E6B5531B2C01CC00F735EE /* OrderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E6B5521B2C01CC00F735EE /* OrderTests.swift */; }; @@ -160,14 +156,15 @@ F3251A0E1B2BDE6E0031B817 /* Department.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Department.swift; sourceTree = ""; }; F3251A0F1B2BDE6E0031B817 /* Employee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Employee.swift; sourceTree = ""; }; F33567641B2D2EF7007853DB /* SelectionAndGroupingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionAndGroupingTests.swift; sourceTree = ""; }; - F34E74931B2E38C900FAA04A /* Expression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Expression.swift; sourceTree = ""; }; - F34E74981B2E3A9900FAA04A /* Alias.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Alias.swift; sourceTree = ""; }; + F33863921C322AFE00818352 /* CustomExpressionConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomExpressionConvertible.swift; sourceTree = ""; }; + F33863971C322BDB00818352 /* Aggregable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Aggregable.swift; sourceTree = ""; }; + F338639C1C322C0B00818352 /* KeyAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyAttribute.swift; sourceTree = ""; }; + F33863A11C322DB700818352 /* CustomPropertyConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPropertyConvertible.swift; sourceTree = ""; }; F34E749D1B2E576000FAA04A /* Grouping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Grouping.swift; sourceTree = ""; }; F356831B1B43AC040089F0A0 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - F367DE2F1B2D6149009C1020 /* OrderType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderType.swift; sourceTree = ""; }; + F367DE2F1B2D6149009C1020 /* CustomSortDescriptorConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomSortDescriptorConvertible.swift; sourceTree = ""; }; F36D9D291B40FE5E00A3D4EC /* DepartmentAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DepartmentAttribute.swift; sourceTree = ""; }; F36D9D2A1B40FE5E00A3D4EC /* EmployeeAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmployeeAttribute.swift; sourceTree = ""; }; - F371ABA51B2B8EE2002A8A85 /* AttributeType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeType.swift; sourceTree = ""; }; F371ABB71B2BA8BA002A8A85 /* QueryBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryBuilder.swift; sourceTree = ""; }; F371ABBC1B2BA9DA002A8A85 /* EntityQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntityQuery.swift; sourceTree = ""; }; F371ABC11B2BAC28002A8A85 /* ExpressionQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpressionQuery.swift; sourceTree = ""; }; @@ -180,6 +177,7 @@ F371ABE41B2BC2E7002A8A85 /* ManagedObjectIDQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedObjectIDQuery.swift; sourceTree = ""; }; F371ABEA1B2BC4CB002A8A85 /* ManagedObjectContextType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextType.swift; sourceTree = ""; }; F3754CCA1B35FBE5004B46B3 /* Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; }; + F379BB001C3470FA001BC25B /* Countable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Countable.swift; sourceTree = ""; }; F37A46EF1B2BD61700D4EE61 /* Attribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Attribute.swift; sourceTree = ""; }; F37A46F41B2BD77900D4EE61 /* Predicate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Predicate.swift; sourceTree = ""; }; F37C46A21B2A780200B35B1B /* CoreDataQueryInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataQueryInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -189,9 +187,6 @@ F37C46B11B2A780200B35B1B /* SanityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SanityTests.swift; sourceTree = ""; }; F37C46B31B2A780200B35B1B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F39C7A711B718DB2008DB31C /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; - F3B0E32C1B2DEEB5008D7EEF /* ExpressionHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpressionHelper.swift; sourceTree = ""; }; - F3B0E3391B2DFD70008D7EEF /* Function.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Function.swift; sourceTree = ""; }; - F3B0E33E1B2E0351008D7EEF /* ExpressionType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpressionType.swift; sourceTree = ""; }; F3DEBD9F1B40FCCD00EE1382 /* bin */ = {isa = PBXFileReference; lastKnownFileType = folder; path = bin; sourceTree = ""; }; F3E6B54F1B2BFCA500F735EE /* FilterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterTests.swift; sourceTree = ""; }; F3E6B5521B2C01CC00F735EE /* OrderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderTests.swift; sourceTree = ""; }; @@ -258,25 +253,24 @@ F371ABE91B2BC3FF002A8A85 /* Source */ = { isa = PBXGroup; children = ( - F34E74981B2E3A9900FAA04A /* Alias.swift */, + F33863971C322BDB00818352 /* Aggregable.swift */, F37A46EF1B2BD61700D4EE61 /* Attribute.swift */, - F371ABA51B2B8EE2002A8A85 /* AttributeType.swift */, + F379BB001C3470FA001BC25B /* Countable.swift */, + F33863921C322AFE00818352 /* CustomExpressionConvertible.swift */, + F33863A11C322DB700818352 /* CustomPropertyConvertible.swift */, + F367DE2F1B2D6149009C1020 /* CustomSortDescriptorConvertible.swift */, F371ABBC1B2BA9DA002A8A85 /* EntityQuery.swift */, F3158D1B1B2A91040041B125 /* EntityType.swift */, F371ABDA1B2BC15C002A8A85 /* Executing.swift */, - F34E74931B2E38C900FAA04A /* Expression.swift */, - F3B0E32C1B2DEEB5008D7EEF /* ExpressionHelper.swift */, F371ABC11B2BAC28002A8A85 /* ExpressionQuery.swift */, F371ABCB1B2BBB15002A8A85 /* ExpressionQueryType.swift */, - F3B0E33E1B2E0351008D7EEF /* ExpressionType.swift */, F371ABD01B2BC122002A8A85 /* Filtering.swift */, - F3B0E3391B2DFD70008D7EEF /* Function.swift */, F34E749D1B2E576000FAA04A /* Grouping.swift */, + F338639C1C322C0B00818352 /* KeyAttribute.swift */, F371ABEA1B2BC4CB002A8A85 /* ManagedObjectContextType.swift */, F371ABE41B2BC2E7002A8A85 /* ManagedObjectIDQuery.swift */, F3754CCA1B35FBE5004B46B3 /* Operators.swift */, F371ABD51B2BC14A002A8A85 /* Ordering.swift */, - F367DE2F1B2D6149009C1020 /* OrderType.swift */, F37A46F41B2BD77900D4EE61 /* Predicate.swift */, F371ABB71B2BA8BA002A8A85 /* QueryBuilder.swift */, F371ABC61B2BB80E002A8A85 /* QueryType.swift */, @@ -508,28 +502,27 @@ buildActionMask = 2147483647; files = ( F371ABCE1B2BBB15002A8A85 /* ExpressionQueryType.swift in Sources */, + F33863951C322AFE00818352 /* CustomExpressionConvertible.swift in Sources */, F371ABBA1B2BA8BA002A8A85 /* QueryBuilder.swift in Sources */, F371ABE21B2BC1F7002A8A85 /* Selecting.swift in Sources */, - F3B0E3411B2E0351008D7EEF /* ExpressionType.swift in Sources */, - F34E74961B2E38C900FAA04A /* Expression.swift in Sources */, - F371ABA71B2B8EE2002A8A85 /* AttributeType.swift in Sources */, + F33863A41C322DB700818352 /* CustomPropertyConvertible.swift in Sources */, F37A46F21B2BD61700D4EE61 /* Attribute.swift in Sources */, F371ABD81B2BC14A002A8A85 /* Ordering.swift in Sources */, F371ABDD1B2BC15C002A8A85 /* Executing.swift in Sources */, - F34E749B1B2E3A9900FAA04A /* Alias.swift in Sources */, F37A46F71B2BD77900D4EE61 /* Predicate.swift in Sources */, - F3B0E32F1B2DEEB5008D7EEF /* ExpressionHelper.swift in Sources */, F3158D1D1B2A91040041B125 /* EntityType.swift in Sources */, F371ABC41B2BAC28002A8A85 /* ExpressionQuery.swift in Sources */, F371ABE71B2BC2E7002A8A85 /* ManagedObjectIDQuery.swift in Sources */, - F367DE321B2D6149009C1020 /* OrderType.swift in Sources */, + F367DE321B2D6149009C1020 /* CustomSortDescriptorConvertible.swift in Sources */, F3754CCD1B35FBE5004B46B3 /* Operators.swift in Sources */, F371ABD31B2BC122002A8A85 /* Filtering.swift in Sources */, + F379BB031C3470FA001BC25B /* Countable.swift in Sources */, + F338639F1C322C0B00818352 /* KeyAttribute.swift in Sources */, F371ABED1B2BC4CB002A8A85 /* ManagedObjectContextType.swift in Sources */, F371ABC91B2BB80E002A8A85 /* QueryType.swift in Sources */, F371ABBF1B2BA9DA002A8A85 /* EntityQuery.swift in Sources */, + F338639A1C322BDB00818352 /* Aggregable.swift in Sources */, F34E74A01B2E576000FAA04A /* Grouping.swift in Sources */, - F3B0E33C1B2DFD70008D7EEF /* Function.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -543,35 +536,34 @@ F31C642A1B696CD900235764 /* NSManagedObjectContextExtensions.swift in Sources */, F371ABE31B2BC1F7002A8A85 /* Selecting.swift in Sources */, F371ABC01B2BA9DA002A8A85 /* EntityQuery.swift in Sources */, - F3B0E33D1B2DFD70008D7EEF /* Function.swift in Sources */, - F367DE331B2D6149009C1020 /* OrderType.swift in Sources */, - F371ABB51B2BA5FA002A8A85 /* AttributeType.swift in Sources */, + F367DE331B2D6149009C1020 /* CustomSortDescriptorConvertible.swift in Sources */, F371ABE81B2BC2E7002A8A85 /* ManagedObjectIDQuery.swift in Sources */, F371ABDE1B2BC15C002A8A85 /* Executing.swift in Sources */, F36D9D2E1B40FE5E00A3D4EC /* EmployeeAttribute.swift in Sources */, + F33863A01C322C0B00818352 /* KeyAttribute.swift in Sources */, F371ABC51B2BAC28002A8A85 /* ExpressionQuery.swift in Sources */, F3251A0D1B2BDA2D0031B817 /* Company.xcdatamodeld in Sources */, + F33863A51C322DB700818352 /* CustomPropertyConvertible.swift in Sources */, F371ABB61B2BA5FA002A8A85 /* EntityType.swift in Sources */, - F3B0E3301B2DEEB5008D7EEF /* ExpressionHelper.swift in Sources */, F371ABCF1B2BBB15002A8A85 /* ExpressionQueryType.swift in Sources */, F3754CCE1B35FBE5004B46B3 /* Operators.swift in Sources */, F3251A061B2BDA190031B817 /* BaseTestCase.swift in Sources */, F371ABEE1B2BC4CB002A8A85 /* ManagedObjectContextType.swift in Sources */, F371ABCA1B2BB80E002A8A85 /* QueryType.swift in Sources */, F34E74A11B2E576000FAA04A /* Grouping.swift in Sources */, - F34E749C1B2E3A9900FAA04A /* Alias.swift in Sources */, F30C0B7D1B37A20A00D8D762 /* Util.swift in Sources */, F3251A131B2BDE6E0031B817 /* Employee.swift in Sources */, F37A46F81B2BD77900D4EE61 /* Predicate.swift in Sources */, - F3B0E3421B2E0351008D7EEF /* ExpressionType.swift in Sources */, F371ABBB1B2BA8BA002A8A85 /* QueryBuilder.swift in Sources */, F3E6B5511B2BFCA500F735EE /* FilterTests.swift in Sources */, F371ABD91B2BC14A002A8A85 /* Ordering.swift in Sources */, + F33863961C322AFE00818352 /* CustomExpressionConvertible.swift in Sources */, F371ABD41B2BC122002A8A85 /* Filtering.swift in Sources */, F3FE7AD11B2B87CF00AA5845 /* SanityTests.swift in Sources */, F3251A111B2BDE6E0031B817 /* Department.swift in Sources */, + F379BB041C3470FA001BC25B /* Countable.swift in Sources */, F36D9D2C1B40FE5E00A3D4EC /* DepartmentAttribute.swift in Sources */, - F34E74971B2E38C900FAA04A /* Expression.swift in Sources */, + F338639B1C322BDB00818352 /* Aggregable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -580,28 +572,27 @@ buildActionMask = 2147483647; files = ( F371ABCC1B2BBB15002A8A85 /* ExpressionQueryType.swift in Sources */, + F33863931C322AFE00818352 /* CustomExpressionConvertible.swift in Sources */, F371ABB81B2BA8BA002A8A85 /* QueryBuilder.swift in Sources */, F371ABE01B2BC1F7002A8A85 /* Selecting.swift in Sources */, - F3B0E33F1B2E0351008D7EEF /* ExpressionType.swift in Sources */, - F34E74941B2E38C900FAA04A /* Expression.swift in Sources */, - F371ABA61B2B8EE2002A8A85 /* AttributeType.swift in Sources */, + F33863A21C322DB700818352 /* CustomPropertyConvertible.swift in Sources */, F37A46F01B2BD61700D4EE61 /* Attribute.swift in Sources */, F371ABD61B2BC14A002A8A85 /* Ordering.swift in Sources */, F371ABDB1B2BC15C002A8A85 /* Executing.swift in Sources */, - F34E74991B2E3A9900FAA04A /* Alias.swift in Sources */, F37A46F51B2BD77900D4EE61 /* Predicate.swift in Sources */, - F3B0E32D1B2DEEB5008D7EEF /* ExpressionHelper.swift in Sources */, F3158D1C1B2A91040041B125 /* EntityType.swift in Sources */, F371ABC21B2BAC28002A8A85 /* ExpressionQuery.swift in Sources */, F371ABE51B2BC2E7002A8A85 /* ManagedObjectIDQuery.swift in Sources */, - F367DE301B2D6149009C1020 /* OrderType.swift in Sources */, + F367DE301B2D6149009C1020 /* CustomSortDescriptorConvertible.swift in Sources */, F3754CCB1B35FBE5004B46B3 /* Operators.swift in Sources */, F371ABD11B2BC122002A8A85 /* Filtering.swift in Sources */, + F379BB011C3470FA001BC25B /* Countable.swift in Sources */, + F338639D1C322C0B00818352 /* KeyAttribute.swift in Sources */, F371ABEB1B2BC4CB002A8A85 /* ManagedObjectContextType.swift in Sources */, F371ABC71B2BB80E002A8A85 /* QueryType.swift in Sources */, F371ABBD1B2BA9DA002A8A85 /* EntityQuery.swift in Sources */, + F33863981C322BDB00818352 /* Aggregable.swift in Sources */, F34E749E1B2E576000FAA04A /* Grouping.swift in Sources */, - F3B0E33A1B2DFD70008D7EEF /* Function.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -615,35 +606,34 @@ F31C64291B696CD900235764 /* NSManagedObjectContextExtensions.swift in Sources */, F371ABE11B2BC1F7002A8A85 /* Selecting.swift in Sources */, F371ABBE1B2BA9DA002A8A85 /* EntityQuery.swift in Sources */, - F3B0E33B1B2DFD70008D7EEF /* Function.swift in Sources */, - F367DE311B2D6149009C1020 /* OrderType.swift in Sources */, - F371ABB31B2BA5FA002A8A85 /* AttributeType.swift in Sources */, + F367DE311B2D6149009C1020 /* CustomSortDescriptorConvertible.swift in Sources */, F371ABE61B2BC2E7002A8A85 /* ManagedObjectIDQuery.swift in Sources */, F371ABDC1B2BC15C002A8A85 /* Executing.swift in Sources */, F36D9D2D1B40FE5E00A3D4EC /* EmployeeAttribute.swift in Sources */, + F338639E1C322C0B00818352 /* KeyAttribute.swift in Sources */, F371ABC31B2BAC28002A8A85 /* ExpressionQuery.swift in Sources */, F3251A0C1B2BDA2D0031B817 /* Company.xcdatamodeld in Sources */, + F33863A31C322DB700818352 /* CustomPropertyConvertible.swift in Sources */, F371ABB41B2BA5FA002A8A85 /* EntityType.swift in Sources */, - F3B0E32E1B2DEEB5008D7EEF /* ExpressionHelper.swift in Sources */, F371ABCD1B2BBB15002A8A85 /* ExpressionQueryType.swift in Sources */, F3754CCC1B35FBE5004B46B3 /* Operators.swift in Sources */, F3251A051B2BDA190031B817 /* BaseTestCase.swift in Sources */, F371ABEC1B2BC4CB002A8A85 /* ManagedObjectContextType.swift in Sources */, F371ABC81B2BB80E002A8A85 /* QueryType.swift in Sources */, F34E749F1B2E576000FAA04A /* Grouping.swift in Sources */, - F34E749A1B2E3A9900FAA04A /* Alias.swift in Sources */, F30C0B7C1B37A20A00D8D762 /* Util.swift in Sources */, F3251A121B2BDE6E0031B817 /* Employee.swift in Sources */, F37A46F61B2BD77900D4EE61 /* Predicate.swift in Sources */, - F3B0E3401B2E0351008D7EEF /* ExpressionType.swift in Sources */, F371ABB91B2BA8BA002A8A85 /* QueryBuilder.swift in Sources */, F3E6B5501B2BFCA500F735EE /* FilterTests.swift in Sources */, F371ABD71B2BC14A002A8A85 /* Ordering.swift in Sources */, + F33863941C322AFE00818352 /* CustomExpressionConvertible.swift in Sources */, F371ABD21B2BC122002A8A85 /* Filtering.swift in Sources */, F37C46B21B2A780200B35B1B /* SanityTests.swift in Sources */, F3251A101B2BDE6E0031B817 /* Department.swift in Sources */, + F379BB021C3470FA001BC25B /* Countable.swift in Sources */, F36D9D2B1B40FE5E00A3D4EC /* DepartmentAttribute.swift in Sources */, - F34E74951B2E38C900FAA04A /* Expression.swift in Sources */, + F33863991C322BDB00818352 /* Aggregable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -775,6 +765,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.9; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = "-DDEBUG"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; @@ -817,6 +808,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.0; MACOSX_DEPLOYMENT_TARGET = 10.9; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_SWIFT_FLAGS = "-DRELEASE"; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; diff --git a/CoreDataQueryInterface/Aggregable.swift b/CoreDataQueryInterface/Aggregable.swift new file mode 100644 index 0000000..f85772f --- /dev/null +++ b/CoreDataQueryInterface/Aggregable.swift @@ -0,0 +1,20 @@ +// +// Aggregable.swift +// CoreDataQueryInterface +// +// Created by Gregory Higley on 12/28/15. +// Copyright © 2015 Prosumma LLC. All rights reserved. +// + +import Foundation + +public protocol Aggregable: Countable { + typealias AggregateType: Attribute = Self + func subquery(variable: String, predicate: AggregateType -> NSPredicate) -> NSExpression + func subquery(predicate: AggregateType -> NSPredicate) -> NSExpression + var average: AggregateType { get } + var sum: AggregateType { get } + var max: AggregateType { get } + var min: AggregateType { get } +} + diff --git a/CoreDataQueryInterface/Alias.swift b/CoreDataQueryInterface/Alias.swift deleted file mode 100644 index d94d531..0000000 --- a/CoreDataQueryInterface/Alias.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// Alias.swift -// CoreDataQueryInterface -// -// Created by Gregory Higley on 6/14/15. -// Copyright © 2015 Prosumma LLC. All rights reserved. -// - -import CoreData -import Foundation - -public struct Alias { - public let name: String - public let expression: ExpressionType -} - -extension Alias : ExpressionType { - public func toPropertyDescription(entityDescription: NSEntityDescription) -> NSPropertyDescription { - let expressionDescription = NSExpressionDescription() - expressionDescription.name = name - expressionDescription.expression = expression.toExpression(entityDescription) - expressionDescription.expressionResultType = ExpressionHelper.attributeTypeForPropertyDescription(expression.toPropertyDescription(entityDescription)) - return expressionDescription - } - public func toExpression(entityDescription: NSEntityDescription) -> NSExpression { - return expression.toExpression(entityDescription) - } -} - -extension Expression { - - /** - Aliases an expression by creating an alternate name for it, e.g., - `Expression.alias(Expression.max("salary"), "foo")`. - */ - public static func alias(expression: ExpressionType, _ name: String) -> Alias { - return Alias(name: name, expression: expression) - } - -} \ No newline at end of file diff --git a/CoreDataQueryInterface/Attribute.swift b/CoreDataQueryInterface/Attribute.swift index 7a9a01c..021fcd9 100644 --- a/CoreDataQueryInterface/Attribute.swift +++ b/CoreDataQueryInterface/Attribute.swift @@ -7,30 +7,82 @@ // import Foundation +import CoreData /** -The default implementation of `AttributeType`. + The base class for attributes. + - note: Don't use this class directly. Create or use one of its subclasses, such as `KeyAttribute`. */ -public class Attribute : AttributeType { - +public class Attribute: CustomStringConvertible, CustomExpressionConvertible { private let _name: String? - private let _parent: AttributeType? - - public required init(_ name: String? = nil, parent: AttributeType? = nil) { - if let _ = parent { assert(name != nil) } - if let name = name { assert(!name.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()).isEmpty) } + private let _parent: Attribute? + public required init() { + _name = nil + _parent = nil + } + public required init(_ name: String, parent: Attribute? = nil) { _name = name _parent = parent } - - public var description: String { - if let parent = _parent { + public private(set) lazy var description: String = { + if let parent = self._parent { let parentName = String(parent) let prefix = parentName.isEmpty ? "" : (parentName + ".") - return prefix + _name! + return prefix + self._name! + } else { + return self._name ?? "" + } + }() + public private(set) lazy var expression: NSExpression = { + let keyPath = String(self) + if keyPath.hasPrefix("$") { + return NSExpression(forVariable: keyPath.substringFromIndex(keyPath.startIndex.successor())) + } else if keyPath == "" { + return NSExpression(format: "SELF") } else { - return _name ?? "" + return NSExpression(forKeyPath: keyPath) } + }() + public func named(name: String, type: NSAttributeType = .UndefinedAttributeType) -> NSExpressionDescription { + let e = NSExpressionDescription() + e.expression = expression + e.name = name + e.expressionResultType = type + return e + } +} + +extension Aggregable where Self: Attribute { + public func subquery(variable: String, predicate: AggregateType -> NSPredicate) -> NSExpression { + let collection = AggregateType(_name!) // Can't do a subquery unless _name is not nil. + let iteratorVariable = AggregateType(variable) + return NSExpression(forSubquery: collection.expression, usingIteratorVariable: iteratorVariable.expression.variable, predicate: predicate(iteratorVariable)) + } + public func subquery(predicate: AggregateType -> NSPredicate) -> NSExpression { + var identifier = NSProcessInfo().globallyUniqueString.stringByReplacingOccurrencesOfString("-", withString: "") + identifier = identifier.substringToIndex(identifier.startIndex.advancedBy(10)).lowercaseString + let variable = "$v\(identifier)" + return subquery(variable, predicate: predicate) + } + public var count: NSExpression { + return NSExpression(format: "%@.@count", expression) + } + public var average: AggregateType { + return AggregateType("@avg", parent: self) + } + public var sum: AggregateType { + return AggregateType("@sum", parent: self) + } + public var max: AggregateType { + return AggregateType("@max", parent: self) + } + public var min: AggregateType { + return AggregateType("@min", parent: self) + } +} + +extension Attribute: CustomPropertyConvertible { + public var property: AnyObject { + return String(self) } - } diff --git a/CoreDataQueryInterface/AttributeType.swift b/CoreDataQueryInterface/AttributeType.swift deleted file mode 100644 index aa98b95..0000000 --- a/CoreDataQueryInterface/AttributeType.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// AttributedType.swift -// CoreDataQueryInterface -// -// Created by Gregory Higley on 6/12/15. -// Copyright © 2015 Prosumma LLC. All rights reserved. -// - -import CoreData -import Foundation - -/** -The protocol implemented by classes that represent object dot notation. -*/ -public protocol AttributeType: CustomStringConvertible, OrderType, ExpressionType { - init(_ name: String?, parent: AttributeType?) -} - -extension AttributeType { - public func toPropertyDescription(entityDescription: NSEntityDescription) -> NSPropertyDescription { - return String(self).toPropertyDescription(entityDescription) - } - public func toExpression(entityDescription: NSEntityDescription) -> NSExpression { - return String(self).toExpression(entityDescription) - } -} - diff --git a/CoreDataQueryInterface/Countable.swift b/CoreDataQueryInterface/Countable.swift new file mode 100644 index 0000000..be0ee98 --- /dev/null +++ b/CoreDataQueryInterface/Countable.swift @@ -0,0 +1,19 @@ +// +// Countable.swift +// CoreDataQueryInterface +// +// Created by Gregory Higley on 12/30/15. +// Copyright © 2015 Prosumma LLC. All rights reserved. +// + +import Foundation + +public protocol Countable { + var count: NSExpression { get } +} + +extension NSExpression: Countable { + public var count: NSExpression { + return NSExpression(format: "%@.@count", self) + } +} \ No newline at end of file diff --git a/CoreDataQueryInterface/CustomExpressionConvertible.swift b/CoreDataQueryInterface/CustomExpressionConvertible.swift new file mode 100644 index 0000000..75786a1 --- /dev/null +++ b/CoreDataQueryInterface/CustomExpressionConvertible.swift @@ -0,0 +1,69 @@ +// +// CustomExpressionConvertible.swift +// CoreDataQueryInterface +// +// Created by Gregory Higley on 12/28/15. +// Copyright © 2015 Prosumma LLC. All rights reserved. +// + +import Foundation +import CoreData + +public protocol CustomExpressionConvertible { + var expression: NSExpression { get } +} + +extension CustomExpressionConvertible { + public func compare(rhs: Any?, type: NSPredicateOperatorType, options: NSComparisonPredicateOptions) -> NSPredicate { + let rightExpression: NSExpression + if let rhs = rhs as? CustomExpressionConvertible { + rightExpression = rhs.expression + } else { + rightExpression = NSExpression(forConstantValue: rhs as! AnyObject?) + } + debugPrint(rightExpression) + return NSComparisonPredicate(leftExpression: expression, rightExpression: rightExpression, modifier: .DirectPredicateModifier, type: type, options: options) + } + public func equalTo(rhs: Any?, options: NSComparisonPredicateOptions = []) -> NSPredicate { + return compare(rhs, type: .EqualToPredicateOperatorType, options: options) + } + public func greaterThan(rhs: Any?, options: NSComparisonPredicateOptions = []) -> NSPredicate { + return compare(rhs, type: .GreaterThanPredicateOperatorType, options: options) + } + public func greaterThanOrEqualTo(rhs: Any?, options: NSComparisonPredicateOptions = []) -> NSPredicate { + return compare(rhs, type: .GreaterThanOrEqualToPredicateOperatorType, options: options) + } + public func lessThan(rhs: Any?, options: NSComparisonPredicateOptions = []) -> NSPredicate { + return compare(rhs, type: .LessThanPredicateOperatorType, options: options) + } + public func lessThanOrEqualTo(rhs: Any?, options: NSComparisonPredicateOptions = []) -> NSPredicate { + return compare(rhs, type: .LessThanOrEqualToPredicateOperatorType, options: options) + } + public func beginsWith(rhs: Any?, options: NSComparisonPredicateOptions = []) -> NSPredicate { + return compare(rhs, type: .BeginsWithPredicateOperatorType, options: options) + } + public func contains(rhs: Any?, options: NSComparisonPredicateOptions = []) -> NSPredicate { + return compare(rhs, type: .ContainsPredicateOperatorType, options: options) + } + public func endsWith(rhs: Any?, options: NSComparisonPredicateOptions = []) -> NSPredicate { + return compare(rhs, type: .EndsWithPredicateOperatorType, options: options) + } + public func among(rhs: [Any], options: NSComparisonPredicateOptions = []) -> NSPredicate { + var expressions = [AnyObject]() + for elem in rhs { + let o = elem as! AnyObject + let e = NSExpression(forConstantValue: o) + expressions.append(e) + } + return compare(NSExpression(forAggregate: expressions), type: .InPredicateOperatorType, options: options) + } + public func like(rhs: Any?, options: NSComparisonPredicateOptions = []) -> NSPredicate { + return compare(rhs, type: .LikePredicateOperatorType, options: options) + } +} + +extension NSExpression: CustomExpressionConvertible { + public var expression: NSExpression { + return self + } +} diff --git a/CoreDataQueryInterface/CustomPropertyConvertible.swift b/CoreDataQueryInterface/CustomPropertyConvertible.swift new file mode 100644 index 0000000..c11e156 --- /dev/null +++ b/CoreDataQueryInterface/CustomPropertyConvertible.swift @@ -0,0 +1,27 @@ +// +// CustomPropertyConvertible.swift +// CoreDataQueryInterface +// +// Created by Gregory Higley on 12/28/15. +// Copyright © 2015 Prosumma LLC. All rights reserved. +// + +import Foundation +import CoreData + +public protocol CustomPropertyConvertible { + var property: AnyObject { get } +} + +extension String: CustomPropertyConvertible { + public var property: AnyObject { + return self + } +} + +extension NSPropertyDescription: CustomPropertyConvertible { + public var property: AnyObject { + return self + } +} + diff --git a/CoreDataQueryInterface/OrderType.swift b/CoreDataQueryInterface/CustomSortDescriptorConvertible.swift similarity index 74% rename from CoreDataQueryInterface/OrderType.swift rename to CoreDataQueryInterface/CustomSortDescriptorConvertible.swift index 48b94e8..9921dfe 100644 --- a/CoreDataQueryInterface/OrderType.swift +++ b/CoreDataQueryInterface/CustomSortDescriptorConvertible.swift @@ -1,5 +1,5 @@ // -// OrderType.swift +// CustomSortDescriptorConvertible.swift // CoreDataQueryInterface // // Created by Gregory Higley on 6/14/15. @@ -9,7 +9,7 @@ import Foundation /** -`OrderType` represents a type that can be converted to a sort descriptor. +`CustomSortDescriptorConvertible` represents a type that can be converted to a sort descriptor. - note: `NSSortDescriptor` itself implements this protocol. It ignores the `ascending` parameter of `toSortDescriptor`. So, `order(descending: NSSortDescriptor(key: "foo", ascending: true))` @@ -17,25 +17,25 @@ sorts ascending, *not* descending. For this reason, it's best to use `NSSortDescriptor` with the overloads of `order()` that do not contain the `descending` keyword argument. */ -public protocol OrderType { +public protocol CustomSortDescriptorConvertible { /** Convert the underlying type to `NSSortDescriptor` */ func toSortDescriptor(ascending ascending: Bool) -> NSSortDescriptor } -extension NSSortDescriptor : OrderType { +extension NSSortDescriptor: CustomSortDescriptorConvertible { public func toSortDescriptor(ascending ascending: Bool) -> NSSortDescriptor { return self } } -extension String : OrderType { +extension String: CustomSortDescriptorConvertible { public func toSortDescriptor(ascending ascending: Bool) -> NSSortDescriptor { return NSSortDescriptor(key: self, ascending: ascending) } } -extension AttributeType { +extension Attribute: CustomSortDescriptorConvertible { public func toSortDescriptor(ascending ascending: Bool) -> NSSortDescriptor { - return NSSortDescriptor(key: String(self), ascending: ascending) + return NSSortDescriptor(key: String(self), ascending: ascending) } -} +} \ No newline at end of file diff --git a/CoreDataQueryInterface/EntityType.swift b/CoreDataQueryInterface/EntityType.swift index eee780a..9f8acd2 100644 --- a/CoreDataQueryInterface/EntityType.swift +++ b/CoreDataQueryInterface/EntityType.swift @@ -11,7 +11,7 @@ import Foundation import ObjectiveC public protocol EntityType: class { - typealias EntityAttributeType: AttributeType = Attribute + typealias EntityAttributeType: Attribute = Attribute static func entityNameInManagedObjectModel(managedObjectModel: NSManagedObjectModel) -> String } diff --git a/CoreDataQueryInterface/Executing.swift b/CoreDataQueryInterface/Executing.swift index fb7d88c..8e8def1 100644 --- a/CoreDataQueryInterface/Executing.swift +++ b/CoreDataQueryInterface/Executing.swift @@ -66,7 +66,7 @@ extension ExpressionQueryType { - warning: Any preceding expressions in the CDQI chain are overwritten by `expression`. If the cast to `T` fails, a runtime exception will occur. */ - public func array(expression: ExpressionType, managedObjectContext: NSManagedObjectContext? = nil) throws -> [T] { + public func array(expression: CustomPropertyConvertible, managedObjectContext: NSManagedObjectContext? = nil) throws -> [T] { var builder = self.builder builder.expressions = [expression] let results = try ExpressionQuery(builder: builder).all(managedObjectContext) as NSArray @@ -84,8 +84,8 @@ extension ExpressionQueryType { - warning: Any preceding expressions in the CDQI chain are overwritten by `expression`. If the cast to `T` fails, a runtime exception will occur. */ - public func array(managedObjectContext managedObjectContext: NSManagedObjectContext? = nil, _ expression: QueryEntityType.EntityAttributeType -> ExpressionType) throws -> [T] { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) + public func array(managedObjectContext managedObjectContext: NSManagedObjectContext? = nil, _ expression: QueryEntityType.EntityAttributeType -> CustomPropertyConvertible) throws -> [T] { + let attribute = QueryEntityType.EntityAttributeType() return try array(expression(attribute), managedObjectContext: managedObjectContext) } @@ -96,7 +96,7 @@ extension ExpressionQueryType { - warning: Any preceding expressions in the CDQI chain are overwritten by `expression`. If the cast to `T` fails, a runtime exception will occur. */ - public func value(expression: ExpressionType, managedObjectContext: NSManagedObjectContext? = nil) throws -> T? { + public func value(expression: CustomPropertyConvertible, managedObjectContext: NSManagedObjectContext? = nil) throws -> T? { var builder = self.builder builder.expressions = [expression] if let result = try ExpressionQuery(builder: builder).first(managedObjectContext) { @@ -114,8 +114,8 @@ extension ExpressionQueryType { - warning: Any preceding expressions in the CDQI chain are overwritten by `expression`. If the cast to `T` fails, a runtime exception will occur. */ - public func value(managedObjectContext managedObjectContext: NSManagedObjectContext? = nil, _ expression: QueryEntityType.EntityAttributeType -> ExpressionType) throws -> T? { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) + public func value(managedObjectContext managedObjectContext: NSManagedObjectContext? = nil, _ expression: QueryEntityType.EntityAttributeType -> CustomPropertyConvertible) throws -> T? { + let attribute = QueryEntityType.EntityAttributeType() return try value(expression(attribute), managedObjectContext: managedObjectContext) } } \ No newline at end of file diff --git a/CoreDataQueryInterface/Expression.swift b/CoreDataQueryInterface/Expression.swift deleted file mode 100644 index 5ebd1fb..0000000 --- a/CoreDataQueryInterface/Expression.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Expression.swift -// CoreDataQueryInterface -// -// Created by Gregory Higley on 6/14/15. -// Copyright © 2015 Prosumma LLC. All rights reserved. -// - -import Foundation - -/** -A struct from which to hang static methods that represent expressions, -such as `alias`, `max`, `min`, etc. Use an extension to add your own -expressions. - -Instances of this struct do not play a role in CoreDataQueryInterface. -*/ -public struct Expression {} diff --git a/CoreDataQueryInterface/ExpressionHelper.swift b/CoreDataQueryInterface/ExpressionHelper.swift deleted file mode 100644 index dcc6a0b..0000000 --- a/CoreDataQueryInterface/ExpressionHelper.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// ExpressionHelper.swift -// CoreDataQueryInterface -// -// Created by Gregory Higley on 6/14/15. -// Copyright © 2015 Prosumma LLC. All rights reserved. -// - -import CoreData -import Foundation - -public final class ExpressionHelper { - public static func attributeTypeForKeyPath(keyPath: String, inEntity entity: NSEntityDescription) -> NSAttributeType { - var attributeType = NSAttributeType.UndefinedAttributeType - let keys = keyPath.componentsSeparatedByString(".") - let key = keys.first! - if keys.count > 1 { - // If we have more than one key, the current key MUST be `NSRelationshipDescription`. - let relationshipDescription = entity.propertiesByName[key]! as! NSRelationshipDescription - attributeType = attributeTypeForKeyPath(keys.dropFirst().joinWithSeparator("."), inEntity: relationshipDescription.destinationEntity!) - } else { - let propertyDescription = entity.propertiesByName[key]! - if let attributeDescription = propertyDescription as? NSAttributeDescription { - attributeType = attributeDescription.attributeType - } else if propertyDescription is NSRelationshipDescription { - attributeType = .ObjectIDAttributeType - } - } - return attributeType - } - public static func keyPathForExpression(expression: NSExpression) -> String? { - var keyPath: String? - switch expression.expressionType { - case .KeyPathExpressionType: - keyPath = expression.keyPath - case .FunctionExpressionType: - if let arguments = expression.arguments where arguments.count == 1 { - keyPath = keyPathForExpression(arguments[0]) - } - default: break - } - return keyPath - } - public static func nameForKeyPath(keyPath: String, prefix: String? = nil) -> String { - var keys = keyPath.componentsSeparatedByString(".") - let key = keys.first! - if prefix != nil { keys.insert(prefix!, atIndex: 0) } - if keys.count == 1 { - return key - } else { - var name: String = "" - for var key in keys { - if name.characters.count > 0 { - let firstCharacter = String(key.characters.first!).uppercaseString - let remainingCharacters = String(key.characters.dropFirst()) - key = firstCharacter + remainingCharacters - } - name.appendContentsOf(key) - } - return name - } - } - public static func attributeTypeForPropertyDescription(propertyDescription: NSPropertyDescription) -> NSAttributeType { - var attributeType = NSAttributeType.UndefinedAttributeType - if let expressionDescription = propertyDescription as? NSExpressionDescription { - attributeType = expressionDescription.expressionResultType - } else if let attributeDescription = propertyDescription as? NSAttributeDescription { - attributeType = attributeDescription.attributeType - } else { - attributeType = .ObjectIDAttributeType - } - return attributeType - } -} \ No newline at end of file diff --git a/CoreDataQueryInterface/ExpressionType.swift b/CoreDataQueryInterface/ExpressionType.swift deleted file mode 100644 index 092daf5..0000000 --- a/CoreDataQueryInterface/ExpressionType.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// PropertyType.swift -// CoreDataQueryInterface -// -// Created by Gregory Higley on 6/14/15. -// Copyright © 2015 Prosumma LLC. All rights reserved. -// - -import CoreData -import Foundation - -public protocol ExpressionType { - func toPropertyDescription(entityDescription: NSEntityDescription) -> NSPropertyDescription - func toExpression(entityDescription: NSEntityDescription) -> NSExpression -} - -extension NSPropertyDescription : ExpressionType { - public func toPropertyDescription(entityDescription: NSEntityDescription) -> NSPropertyDescription { - return self - } - public func toExpression(entityDescription: NSEntityDescription) -> NSExpression { - if let expressionDescription = self as? NSExpressionDescription { - return expressionDescription.expression! - } else { - return NSExpression(forKeyPath: name) - } - } -} - -extension NSExpression : ExpressionType { - public func toPropertyDescription(entityDescription: NSEntityDescription) -> NSPropertyDescription { - let expressionDescription = NSExpressionDescription() - expressionDescription.expression = self - if let keyPath = ExpressionHelper.keyPathForExpression(self) { - expressionDescription.name = ExpressionHelper.nameForKeyPath(keyPath) - expressionDescription.expressionResultType = ExpressionHelper.attributeTypeForKeyPath(keyPath, inEntity: entityDescription) - } else { - expressionDescription.name = "expression" - } - return expressionDescription - } - - public func toExpression(_: NSEntityDescription) -> NSExpression { - return self - } -} - -extension String : ExpressionType { - public func toPropertyDescription(entityDescription: NSEntityDescription) -> NSPropertyDescription { - let expressionDescription = NSExpressionDescription() - expressionDescription.name = ExpressionHelper.nameForKeyPath(self) - expressionDescription.expression = toExpression(entityDescription) - expressionDescription.expressionResultType = ExpressionHelper.attributeTypeForKeyPath(self, inEntity: entityDescription) - return expressionDescription - } - public func toExpression(_: NSEntityDescription) -> NSExpression { - return NSExpression(forKeyPath: self) - } -} \ No newline at end of file diff --git a/CoreDataQueryInterface/Filtering.swift b/CoreDataQueryInterface/Filtering.swift index 92e3fc0..1a484cd 100644 --- a/CoreDataQueryInterface/Filtering.swift +++ b/CoreDataQueryInterface/Filtering.swift @@ -19,6 +19,9 @@ extension QueryType { one of the other overloads. */ public func filter(predicate: NSPredicate) -> Self { +#if DEBUG + debugPrint(predicate) +#endif var builder = self.builder builder.predicates.append(predicate) return Self(builder: builder) @@ -33,7 +36,7 @@ extension QueryType { - parameter createPredicate: A block that takes an `AttributeType` and returns an `NSPredicate`. */ public func filter(createPredicate: QueryEntityType.EntityAttributeType -> NSPredicate) -> Self { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) + let attribute = QueryEntityType.EntityAttributeType() let predicate = createPredicate(attribute) return filter(predicate) } diff --git a/CoreDataQueryInterface/Function.swift b/CoreDataQueryInterface/Function.swift deleted file mode 100644 index 4e516ab..0000000 --- a/CoreDataQueryInterface/Function.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// Expression.swift -// CoreDataQueryInterface -// -// Created by Gregory Higley on 6/14/15. -// Copyright © 2015 Prosumma LLC. All rights reserved. -// - -import CoreData -import Foundation - -public struct Function { - public let function: String - public let arguments: [ExpressionType] - public let name: String? - public let prefix: String - public let resultType: NSAttributeType? -} - -extension Function : ExpressionType { - public func toPropertyDescription(entityDescription: NSEntityDescription) -> NSPropertyDescription { - let expressionDescription = NSExpressionDescription() - expressionDescription.expression = toExpression(entityDescription) - var propertyDescription: NSPropertyDescription! - if let name = name { - expressionDescription.name = name - } else if arguments.count == 1 { - propertyDescription = arguments.first!.toPropertyDescription(entityDescription) - expressionDescription.name = ExpressionHelper.nameForKeyPath(propertyDescription.name, prefix: prefix) - } else { - expressionDescription.name = "expression" - } - if arguments.count == 1 { - if propertyDescription == nil { propertyDescription = arguments.first!.toPropertyDescription(entityDescription) } - expressionDescription.expressionResultType = ExpressionHelper.attributeTypeForPropertyDescription(propertyDescription) - } - return expressionDescription - } - - public func toExpression(entityDescription: NSEntityDescription) -> NSExpression { - return NSExpression(forFunction: function, arguments: arguments.map({$0.toExpression(entityDescription)})) - } -} - -extension Expression { - - public static func max(argument: ExpressionType, name: String? = nil, resultType: NSAttributeType? = nil) -> Function { - return Function(function: "max:", arguments: [argument], name: name, prefix: "max", resultType: resultType) - } - - public static func min(argument: ExpressionType, name: String? = nil, resultType: NSAttributeType? = nil) -> Function { - return Function(function: "min:", arguments: [argument], name: name, prefix: "min", resultType: resultType) - } - - public static func average(argument: ExpressionType, name: String? = nil, resultType: NSAttributeType? = nil) -> Function { - return Function(function: "average:", arguments: [argument], name: name, prefix: "average", resultType: resultType) - } - -} - -// MARK: - Shortcut Aggregates - -extension ExpressionQueryType { - - public func max(expression: ExpressionType) -> ExpressionQuery { - return select(Expression.max(expression)) - } - - public func max(expression: QueryEntityType.EntityAttributeType -> ExpressionType) -> ExpressionQuery { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) - return select(Expression.max(expression(attribute))) - } - - public func min(expression: ExpressionType) -> ExpressionQuery { - return select(Expression.min(expression)) - } - - public func min(expression: QueryEntityType.EntityAttributeType -> ExpressionType) -> ExpressionQuery { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) - return select(Expression.min(expression(attribute))) - } - - public func average(expression: ExpressionType) -> ExpressionQuery { - return select(Expression.average(expression)) - } - - public func average(expression: QueryEntityType.EntityAttributeType -> ExpressionType) -> ExpressionQuery { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) - return select(Expression.average(expression(attribute))) - } - -} \ No newline at end of file diff --git a/CoreDataQueryInterface/Grouping.swift b/CoreDataQueryInterface/Grouping.swift index 08b9093..f83fcb6 100644 --- a/CoreDataQueryInterface/Grouping.swift +++ b/CoreDataQueryInterface/Grouping.swift @@ -10,27 +10,27 @@ import Foundation extension ExpressionQueryType { - public func groupBy(expressions: [ExpressionType]) -> ExpressionQuery { + public func groupBy(expressions: [CustomPropertyConvertible]) -> ExpressionQuery { var builder = self.builder builder.groupings.appendContentsOf(expressions) return ExpressionQuery(builder: builder) } - public func groupBy(expressions: ExpressionType...) -> ExpressionQuery { + public func groupBy(expressions: CustomPropertyConvertible...) -> ExpressionQuery { return groupBy(expressions) } - public func groupBy(expressions: QueryEntityType.EntityAttributeType -> [ExpressionType]) -> ExpressionQuery { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) + public func groupBy(expressions: QueryEntityType.EntityAttributeType -> [CustomPropertyConvertible]) -> ExpressionQuery { + let attribute = QueryEntityType.EntityAttributeType() return groupBy(expressions(attribute)) } - public func groupBy(expressions: [QueryEntityType.EntityAttributeType -> ExpressionType]) -> ExpressionQuery { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) + public func groupBy(expressions: [QueryEntityType.EntityAttributeType -> CustomPropertyConvertible]) -> ExpressionQuery { + let attribute = QueryEntityType.EntityAttributeType() return groupBy(expressions.map() { $0(attribute) }) } - public func groupBy(expressions: (QueryEntityType.EntityAttributeType -> ExpressionType)...) -> ExpressionQuery { + public func groupBy(expressions: (QueryEntityType.EntityAttributeType -> CustomPropertyConvertible)...) -> ExpressionQuery { return groupBy(expressions) } diff --git a/CoreDataQueryInterface/KeyAttribute.swift b/CoreDataQueryInterface/KeyAttribute.swift new file mode 100644 index 0000000..a4d3597 --- /dev/null +++ b/CoreDataQueryInterface/KeyAttribute.swift @@ -0,0 +1,16 @@ +// +// KeyAttribute.swift +// CoreDataQueryInterface +// +// Created by Gregory Higley on 12/28/15. +// Copyright © 2015 Prosumma LLC. All rights reserved. +// + +import Foundation + +/** + Use this subclass of `Attribute` for simple keys. + */ +class KeyAttribute: Attribute, Aggregable { + typealias AggregateType = KeyAttribute +} diff --git a/CoreDataQueryInterface/Operators.swift b/CoreDataQueryInterface/Operators.swift index 9129101..c400aa7 100644 --- a/CoreDataQueryInterface/Operators.swift +++ b/CoreDataQueryInterface/Operators.swift @@ -8,400 +8,22 @@ import Foundation -public func predicate(lhs: A, _ op: String, _ formatSpecifier: String, _ rhs: T?) -> NSPredicate { - let key = lhs.description - if let rhs = rhs where !key.isEmpty { - return NSPredicate(format: "%K \(op) \(formatSpecifier)", lhs.description, rhs) - } else if let rhs = rhs where key.isEmpty { - return NSPredicate(format: "SELF \(op) \(formatSpecifier)", rhs) - } else if !key.isEmpty { - return NSPredicate(format: "%K \(op) NIL", lhs.description) - } else { - return NSPredicate(format: "SELF \(op) NIL") - } +public func ==(lhs: CustomExpressionConvertible, rhs: Any?) -> NSPredicate { + return lhs.equalTo(rhs as! AnyObject?) } -// NSObject - -public func ==(lhs: A, rhs: NSObject?) -> NSPredicate { - return predicate(lhs, "==", "%@", rhs) -} - -public func !=(lhs: A, rhs: NSObject?) -> NSPredicate { - return predicate(lhs, "!=", "%@", rhs) -} - -public func >(lhs: A, rhs: NSObject) -> NSPredicate { - return predicate(lhs, ">", "%@", rhs) -} - -public func >=(lhs: A, rhs: NSObject) -> NSPredicate { - return predicate(lhs, ">=", "%@", rhs) -} - -public func <(lhs: A, rhs: NSObject) -> NSPredicate { - return predicate(lhs, "<", "%@", rhs) -} - -public func <=(lhs: A, rhs: NSObject) -> NSPredicate { - return predicate(lhs, "<=", "%@", rhs) -} - -// [NSObject] - -public func ==(lhs: A, rhs: [NSObject]) -> NSPredicate { - return predicate(lhs, "IN", "%@", rhs as NSArray) -} - -public func !=(lhs: A, rhs: [NSObject]) -> NSPredicate { - return NSCompoundPredicate(notPredicateWithSubpredicate: lhs == rhs) -} - -// String - -public func ==(lhs: A, rhs: String) -> NSPredicate { - return predicate(lhs, "==", "%@", rhs as NSString) -} - -public func !=(lhs: A, rhs: String) -> NSPredicate { - return predicate(lhs, "!=", "%@", rhs as NSString) -} - -public func >(lhs: A, rhs: String) -> NSPredicate { - return predicate(lhs, ">", "%@", rhs as NSString) -} - -public func >=(lhs: A, rhs: String) -> NSPredicate { - return predicate(lhs, ">=", "%@", rhs as NSString) -} - -public func <(lhs: A, rhs: String) -> NSPredicate { - return predicate(lhs, "<", "%@", rhs as NSString) -} - -public func <=(lhs: A, rhs: String) -> NSPredicate { - return predicate(lhs, "<=", "%@", rhs as NSString) -} - -// [String] - -public func ==(lhs: A, rhs: [String]) -> NSPredicate { - return predicate(lhs, "IN", "%@", rhs as NSArray) -} - -public func !=(lhs: A, rhs: [String]) -> NSPredicate { - return NSCompoundPredicate(notPredicateWithSubpredicate: lhs == rhs) -} - -// Int - -public func ==(lhs: A, rhs: Int) -> NSPredicate { - return lhs == NSNumber(integer: rhs) -} - -public func !=(lhs: A, rhs: Int) -> NSPredicate { - return lhs != NSNumber(integer: rhs) -} - -public func >(lhs: A, rhs: Int) -> NSPredicate { - return lhs > NSNumber(integer: rhs) -} - -public func >=(lhs: A, rhs: Int) -> NSPredicate { - return lhs >= NSNumber(integer: rhs) -} - -public func <(lhs: A, rhs: Int) -> NSPredicate { - return lhs < NSNumber(integer: rhs) -} - -public func <=(lhs: A, rhs: Int) -> NSPredicate { - return lhs <= NSNumber(integer: rhs) -} - -// Int8 - -public func ==(lhs: A, rhs: Int8) -> NSPredicate { - return lhs == NSNumber(char: rhs) -} - -public func !=(lhs: A, rhs: Int8) -> NSPredicate { - return lhs != NSNumber(char: rhs) -} - -public func >(lhs: A, rhs: Int8) -> NSPredicate { - return lhs > NSNumber(char: rhs) -} - -public func >=(lhs: A, rhs: Int8) -> NSPredicate { - return lhs >= NSNumber(char: rhs) -} - -public func <(lhs: A, rhs: Int8) -> NSPredicate { - return lhs < NSNumber(char: rhs) -} - -public func <=(lhs: A, rhs: Int8) -> NSPredicate { - return lhs <= NSNumber(char: rhs) -} - -// Int16 - -public func ==(lhs: A, rhs: Int16) -> NSPredicate { - return lhs == NSNumber(short: rhs) -} - -public func !=(lhs: A, rhs: Int16) -> NSPredicate { - return lhs != NSNumber(short: rhs) -} - -public func >(lhs: A, rhs: Int16) -> NSPredicate { - return lhs > NSNumber(short: rhs) -} - -public func >=(lhs: A, rhs: Int16) -> NSPredicate { - return lhs >= NSNumber(short: rhs) -} - -public func <(lhs: A, rhs: Int16) -> NSPredicate { - return lhs < NSNumber(short: rhs) -} - -public func <=(lhs: A, rhs: Int16) -> NSPredicate { - return lhs <= NSNumber(short: rhs) -} - -// Int32 - -public func ==(lhs: A, rhs: Int32) -> NSPredicate { - return lhs == NSNumber(int: rhs) -} - -public func !=(lhs: A, rhs: Int32) -> NSPredicate { - return lhs != NSNumber(int: rhs) -} - -public func >(lhs: A, rhs: Int32) -> NSPredicate { - return lhs > NSNumber(int: rhs) -} - -public func >=(lhs: A, rhs: Int32) -> NSPredicate { - return lhs >= NSNumber(int: rhs) -} - -public func <(lhs: A, rhs: Int32) -> NSPredicate { - return lhs < NSNumber(int: rhs) -} - -public func <=(lhs: A, rhs: Int32) -> NSPredicate { - return lhs <= NSNumber(int: rhs) -} - -// Int64 - -public func ==(lhs: A, rhs: Int64) -> NSPredicate { - return lhs == NSNumber(longLong: rhs) -} - -public func !=(lhs: A, rhs: Int64) -> NSPredicate { - return lhs != NSNumber(longLong: rhs) -} - -public func >(lhs: A, rhs: Int64) -> NSPredicate { - return lhs > NSNumber(longLong: rhs) +public func >(lhs: CustomExpressionConvertible, rhs: Any?) -> NSPredicate { + return lhs.greaterThan(rhs) } -public func >=(lhs: A, rhs: Int64) -> NSPredicate { - return lhs >= NSNumber(longLong: rhs) +public func >=(lhs: CustomExpressionConvertible, rhs: Any?) -> NSPredicate { + return lhs.greaterThanOrEqualTo(rhs as! AnyObject?) } -public func <(lhs: A, rhs: Int64) -> NSPredicate { - return lhs < NSNumber(longLong: rhs) +public func <(lhs: CustomExpressionConvertible, rhs: Any?) -> NSPredicate { + return lhs.lessThan(rhs) } -public func <=(lhs: A, rhs: Int64) -> NSPredicate { - return lhs <= NSNumber(longLong: rhs) +public func <=(lhs: CustomExpressionConvertible, rhs: Any?) -> NSPredicate { + return lhs.lessThanOrEqualTo(rhs) } - -// UInt - -public func ==(lhs: A, rhs: UInt) -> NSPredicate { - return lhs == NSNumber(unsignedLong: rhs) -} - -public func !=(lhs: A, rhs: UInt) -> NSPredicate { - return lhs != NSNumber(unsignedLong: rhs) -} - -public func >(lhs: A, rhs: UInt) -> NSPredicate { - return lhs > NSNumber(unsignedLong: rhs) -} - -public func >=(lhs: A, rhs: UInt) -> NSPredicate { - return lhs >= NSNumber(unsignedLong: rhs) -} - -public func <(lhs: A, rhs: UInt) -> NSPredicate { - return lhs < NSNumber(unsignedLong: rhs) -} - -public func <=(lhs: A, rhs: UInt) -> NSPredicate { - return lhs <= NSNumber(unsignedLong: rhs) -} - -// UInt8 - -public func ==(lhs: A, rhs: UInt8) -> NSPredicate { - return lhs == NSNumber(unsignedChar: rhs) -} - -public func !=(lhs: A, rhs: UInt8) -> NSPredicate { - return lhs != NSNumber(unsignedChar: rhs) -} - -public func >(lhs: A, rhs: UInt8) -> NSPredicate { - return lhs > NSNumber(unsignedChar: rhs) -} - -public func >=(lhs: A, rhs: UInt8) -> NSPredicate { - return lhs >= NSNumber(unsignedChar: rhs) -} - -public func <(lhs: A, rhs: UInt8) -> NSPredicate { - return lhs < NSNumber(unsignedChar: rhs) -} - -public func <=(lhs: A, rhs: UInt8) -> NSPredicate { - return lhs <= NSNumber(unsignedChar: rhs) -} - -// UInt16 - -public func ==(lhs: A, rhs: UInt16) -> NSPredicate { - return lhs == NSNumber(unsignedShort: rhs) -} - -public func !=(lhs: A, rhs: UInt16) -> NSPredicate { - return lhs != NSNumber(unsignedShort: rhs) -} - -public func >(lhs: A, rhs: UInt16) -> NSPredicate { - return lhs > NSNumber(unsignedShort: rhs) -} - -public func >=(lhs: A, rhs: UInt16) -> NSPredicate { - return lhs >= NSNumber(unsignedShort: rhs) -} - -public func <(lhs: A, rhs: UInt16) -> NSPredicate { - return lhs < NSNumber(unsignedShort: rhs) -} - -public func <=(lhs: A, rhs: UInt16) -> NSPredicate { - return lhs <= NSNumber(unsignedShort: rhs) -} - -// UInt32 - -public func ==(lhs: A, rhs: UInt32) -> NSPredicate { - return lhs == NSNumber(unsignedInt: rhs) -} - -public func !=(lhs: A, rhs: UInt32) -> NSPredicate { - return lhs != NSNumber(unsignedInt: rhs) -} - -public func >(lhs: A, rhs: UInt32) -> NSPredicate { - return lhs > NSNumber(unsignedInt: rhs) -} - -public func >=(lhs: A, rhs: UInt32) -> NSPredicate { - return lhs >= NSNumber(unsignedInt: rhs) -} - -public func <(lhs: A, rhs: UInt32) -> NSPredicate { - return lhs < NSNumber(unsignedInt: rhs) -} - -public func <=(lhs: A, rhs: UInt32) -> NSPredicate { - return lhs <= NSNumber(unsignedInt: rhs) -} - -// UInt64 - -public func ==(lhs: A, rhs: UInt64) -> NSPredicate { - return lhs == NSNumber(unsignedLongLong: rhs) -} - -public func !=(lhs: A, rhs: UInt64) -> NSPredicate { - return lhs != NSNumber(unsignedLongLong: rhs) -} - -public func >(lhs: A, rhs: UInt64) -> NSPredicate { - return lhs > NSNumber(unsignedLongLong: rhs) -} - -public func >=(lhs: A, rhs: UInt64) -> NSPredicate { - return lhs >= NSNumber(unsignedLongLong: rhs) -} - -public func <(lhs: A, rhs: UInt64) -> NSPredicate { - return lhs < NSNumber(unsignedLongLong: rhs) -} - -public func <=(lhs: A, rhs: UInt64) -> NSPredicate { - return lhs <= NSNumber(unsignedLongLong: rhs) -} - -// Float - -public func ==(lhs: A, rhs: Float) -> NSPredicate { - return lhs == NSNumber(float: rhs) -} - -public func !=(lhs: A, rhs: Float) -> NSPredicate { - return lhs != NSNumber(float: rhs) -} - -public func >(lhs: A, rhs: Float) -> NSPredicate { - return lhs > NSNumber(float: rhs) -} - -public func >=(lhs: A, rhs: Float) -> NSPredicate { - return lhs >= NSNumber(float: rhs) -} - -public func <(lhs: A, rhs: Float) -> NSPredicate { - return lhs < NSNumber(float: rhs) -} - -public func <=(lhs: A, rhs: Float) -> NSPredicate { - return lhs <= NSNumber(float: rhs) -} - -// Double - -public func ==(lhs: A, rhs: Double) -> NSPredicate { - return lhs == NSNumber(double: rhs) -} - -public func !=(lhs: A, rhs: Double) -> NSPredicate { - return lhs != NSNumber(double: rhs) -} - -public func >(lhs: A, rhs: Double) -> NSPredicate { - return lhs > NSNumber(double: rhs) -} - -public func >=(lhs: A, rhs: Double) -> NSPredicate { - return lhs >= NSNumber(double: rhs) -} - -public func <(lhs: A, rhs: Double) -> NSPredicate { - return lhs < NSNumber(double: rhs) -} - -public func <=(lhs: A, rhs: Double) -> NSPredicate { - return lhs <= NSNumber(double: rhs) -} - diff --git a/CoreDataQueryInterface/Ordering.swift b/CoreDataQueryInterface/Ordering.swift index 42d05a3..9596d8d 100644 --- a/CoreDataQueryInterface/Ordering.swift +++ b/CoreDataQueryInterface/Ordering.swift @@ -20,9 +20,9 @@ extension QueryType { `order` methods can be chained multiple times, e.g., `order(descending: "lastName").order("firstName")` sorts first _descending_ by `lastName` and then _ascending_ by `firstName`. - - parameter keys: An array of elements implementing the `OrderType` protocol. + - parameter keys: An array of elements implementing the `CustomSortDescriptorConvertible` protocol. */ - public func order(keys: [OrderType]) -> Self { + public func order(keys: [CustomSortDescriptorConvertible]) -> Self { let descriptors = keys.map() { $0.toSortDescriptor(ascending: true) } var builder = self.builder builder.descriptors.appendContentsOf(descriptors) @@ -39,9 +39,9 @@ extension QueryType { `order` methods can be chained multiple times, e.g., `order(descending: "lastName").order("firstName")` sorts first _descending_ by `lastName` and then _ascending_ by `firstName`. - - parameter keys: An array of elements implementing the `OrderType` protocol. + - parameter keys: An array of elements implementing the `CustomSortDescriptorConvertible` protocol. */ - public func order(keys: OrderType...) -> Self { + public func order(keys: CustomSortDescriptorConvertible...) -> Self { return order(keys) } @@ -51,10 +51,10 @@ extension QueryType { `order` methods can be chained multiple times, e.g., `order(descending: "lastName").order("firstName")` sorts first _descending_ by `lastName` and then _ascending_ by `firstName`. - - parameter keys: A block which returns an array of `OrderType`s to sort by. + - parameter keys: A block which returns an array of `CustomSortDescriptorConvertible`s to sort by. */ - public func order(keys: QueryEntityType.EntityAttributeType -> [OrderType]) -> Self { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) + public func order(keys: QueryEntityType.EntityAttributeType -> [CustomSortDescriptorConvertible]) -> Self { + let attribute = QueryEntityType.EntityAttributeType() return order(keys(attribute)) } @@ -64,10 +64,10 @@ extension QueryType { `order` methods can be chained multiple times, e.g., `order(descending: "lastName").order("firstName")` sorts first _descending_ by `lastName` and then _ascending_ by `firstName`. - - parameter keys: An array of blocks each of which returns an `OrderType` for sorting. + - parameter keys: An array of blocks each of which returns an `CustomSortDescriptorConvertible` for sorting. */ - public func order(keys: [QueryEntityType.EntityAttributeType -> OrderType]) -> Self { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) + public func order(keys: [QueryEntityType.EntityAttributeType -> CustomSortDescriptorConvertible]) -> Self { + let attribute = QueryEntityType.EntityAttributeType() return order(keys.map({$0(attribute)})) } @@ -77,9 +77,9 @@ extension QueryType { `order` methods can be chained multiple times, e.g., `order(descending: "lastName").order("firstName")` sorts first _descending_ by `lastName` and then _ascending_ by `firstName`. - - parameter keys: An array of blocks each of which returns an `OrderType` for sorting. + - parameter keys: An array of blocks each of which returns an `CustomSortDescriptorConvertible` for sorting. */ - public func order(keys: (QueryEntityType.EntityAttributeType -> OrderType)...) -> Self { + public func order(keys: (QueryEntityType.EntityAttributeType -> CustomSortDescriptorConvertible)...) -> Self { return order(keys) } @@ -93,9 +93,9 @@ extension QueryType { `order` methods can be chained multiple times, e.g., `order(descending: "lastName").order("firstName")` sorts first _descending_ by `lastName` and then _ascending_ by `firstName`. - - parameter keys: An array of elements implementing the `OrderType` protocol. + - parameter keys: An array of elements implementing the `CustomSortDescriptorConvertible` protocol. */ - public func order(descending keys: [OrderType]) -> Self { + public func order(descending keys: [CustomSortDescriptorConvertible]) -> Self { let descriptors = keys.map() { $0.toSortDescriptor(ascending: false) } var builder = self.builder builder.descriptors.appendContentsOf(descriptors) @@ -112,9 +112,9 @@ extension QueryType { `order` methods can be chained multiple times, e.g., `order(descending: "lastName").order("firstName")` sorts first _descending_ by `lastName` and then _ascending_ by `firstName`. - - parameter keys: An array of elements implementing the `OrderType` protocol. + - parameter keys: An array of elements implementing the `CustomSortDescriptorConvertible` protocol. */ - public func order(descending keys: OrderType...) -> Self { + public func order(descending keys: CustomSortDescriptorConvertible...) -> Self { return order(descending: keys) } @@ -124,10 +124,10 @@ extension QueryType { `order` methods can be chained multiple times, e.g., `order(descending: "lastName").order("firstName")` sorts first _descending_ by `lastName` and then _ascending_ by `firstName`. - - parameter keys: A block which returns an array of `OrderType`s to sort by. + - parameter keys: A block which returns an array of `CustomSortDescriptorConvertible`s to sort by. */ - public func order(descending keys: QueryEntityType.EntityAttributeType -> [OrderType]) -> Self { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) + public func order(descending keys: QueryEntityType.EntityAttributeType -> [CustomSortDescriptorConvertible]) -> Self { + let attribute = QueryEntityType.EntityAttributeType() return order(descending: keys(attribute)) } @@ -137,10 +137,10 @@ extension QueryType { `order` methods can be chained multiple times, e.g., `order(descending: "lastName").order("firstName")` sorts first _descending_ by `lastName` and then _ascending_ by `firstName`. - - parameter keys: An array of blocks each of which returns an `OrderType` for sorting. + - parameter keys: An array of blocks each of which returns an `CustomSortDescriptorConvertible` for sorting. */ - public func order(descending keys: [QueryEntityType.EntityAttributeType -> OrderType]) -> Self { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) + public func order(descending keys: [QueryEntityType.EntityAttributeType -> CustomSortDescriptorConvertible]) -> Self { + let attribute = QueryEntityType.EntityAttributeType() return order(descending: keys.map({$0(attribute)})) } @@ -150,9 +150,9 @@ extension QueryType { `order` methods can be chained multiple times, e.g., `order(descending: "lastName").order("firstName")` sorts first _descending_ by `lastName` and then _ascending_ by `firstName`. - - parameter keys: An array of blocks each of which returns an `OrderType` for sorting. + - parameter keys: An array of blocks each of which returns an `CustomSortDescriptorConvertible` for sorting. */ - public func order(descending keys: (QueryEntityType.EntityAttributeType -> OrderType)...) -> Self { + public func order(descending keys: (QueryEntityType.EntityAttributeType -> CustomSortDescriptorConvertible)...) -> Self { return order(descending: keys) } diff --git a/CoreDataQueryInterface/Predicate.swift b/CoreDataQueryInterface/Predicate.swift index d37501d..c96bcac 100644 --- a/CoreDataQueryInterface/Predicate.swift +++ b/CoreDataQueryInterface/Predicate.swift @@ -19,3 +19,19 @@ public func ||(lhs: NSPredicate, rhs: NSPredicate) -> NSPredicate { public prefix func !(predicate: NSPredicate) -> NSPredicate { return NSCompoundPredicate(notPredicateWithSubpredicate: predicate) } + +public func any(predicate: NSPredicate) -> NSPredicate { + return NSPredicate(format: "ANY \(predicate)") +} + +public func some(predicate: NSPredicate) -> NSPredicate { + return NSPredicate(format: "SOME \(predicate)") +} + +public func none(predicate: NSPredicate) -> NSPredicate { + return NSPredicate(format: "NONE \(predicate)") +} + +public func all(predicate: NSPredicate) -> NSPredicate { + return NSPredicate(format: "ALL \(predicate)") +} diff --git a/CoreDataQueryInterface/QueryBuilder.swift b/CoreDataQueryInterface/QueryBuilder.swift index d9c90a1..2a0ccb5 100644 --- a/CoreDataQueryInterface/QueryBuilder.swift +++ b/CoreDataQueryInterface/QueryBuilder.swift @@ -16,8 +16,8 @@ public struct QueryBuilder { public var managedObjectContext: NSManagedObjectContext? public var predicates = [NSPredicate]() public var descriptors = [NSSortDescriptor]() - public var expressions = [ExpressionType]() - public var groupings = [ExpressionType]() + public var expressions = [CustomPropertyConvertible]() + public var groupings = [CustomPropertyConvertible]() public var limit: UInt? public var offset: UInt? public var returnsDistinctResults: Bool = false @@ -36,12 +36,11 @@ public struct QueryBuilder { if !predicates.isEmpty { request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) } if !descriptors.isEmpty { request.sortDescriptors = descriptors } if !expressions.isEmpty || !groupings.isEmpty { - let entityDescription = managedObjectModel.entitiesByName[E.entityNameInManagedObjectModel(managedObjectModel)]! if !expressions.isEmpty { - request.propertiesToFetch = expressions.map() { $0.toPropertyDescription(entityDescription) } + request.propertiesToFetch = expressions.map() { $0.property } } if !groupings.isEmpty { - request.propertiesToGroupBy = groupings.map() { $0.toPropertyDescription(entityDescription) } + request.propertiesToGroupBy = groupings.map() { $0.property } } } return request diff --git a/CoreDataQueryInterface/Selecting.swift b/CoreDataQueryInterface/Selecting.swift index fc39bb1..e6d5a7a 100644 --- a/CoreDataQueryInterface/Selecting.swift +++ b/CoreDataQueryInterface/Selecting.swift @@ -11,27 +11,27 @@ import Foundation extension ExpressionQueryType { - public func select(expressions: [ExpressionType]) -> ExpressionQuery { + public func select(expressions: [CustomPropertyConvertible]) -> ExpressionQuery { var builder = self.builder builder.expressions.appendContentsOf(expressions) return ExpressionQuery(builder: builder) } - public func select(expressions: ExpressionType...) -> ExpressionQuery { + public func select(expressions: CustomPropertyConvertible...) -> ExpressionQuery { return select(expressions) } - public func select(expressions: QueryEntityType.EntityAttributeType -> [ExpressionType]) -> ExpressionQuery { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) + public func select(expressions: QueryEntityType.EntityAttributeType -> [CustomPropertyConvertible]) -> ExpressionQuery { + let attribute = QueryEntityType.EntityAttributeType() return select(expressions(attribute)) } - public func select(expressions: [QueryEntityType.EntityAttributeType -> ExpressionType]) -> ExpressionQuery { - let attribute = QueryEntityType.EntityAttributeType(nil, parent: nil) + public func select(expressions: [QueryEntityType.EntityAttributeType -> CustomPropertyConvertible]) -> ExpressionQuery { + let attribute = QueryEntityType.EntityAttributeType() return select(expressions.map() { $0(attribute) }) } - public func select(expressions: (QueryEntityType.EntityAttributeType -> ExpressionType)...) -> ExpressionQuery { + public func select(expressions: (QueryEntityType.EntityAttributeType -> CustomPropertyConvertible)...) -> ExpressionQuery { return select(expressions) } diff --git a/CoreDataQueryInterfaceTests/BaseTestCase.swift b/CoreDataQueryInterfaceTests/BaseTestCase.swift index 3ecd5fd..f3ef745 100644 --- a/CoreDataQueryInterfaceTests/BaseTestCase.swift +++ b/CoreDataQueryInterfaceTests/BaseTestCase.swift @@ -50,7 +50,7 @@ class BaseTestCase: XCTestCase { employee.nickName = nickNames[employee.firstName] employee.lastName = fields[0] employee.salary = Int32(fields[3])! - if let department = try! managedObjectContext.from(Department).filter({ $0.name == fields[2] }).first() { + if let department = try! managedObjectContext.from(Department).filter({ $0.name == fields[2] as AnyObject }).first() { employee.department = department } else { let department = managedObjectContext.newEntity(Department) diff --git a/CoreDataQueryInterfaceTests/Department.swift b/CoreDataQueryInterfaceTests/Department.swift index 6e99599..bbaaa83 100644 --- a/CoreDataQueryInterfaceTests/Department.swift +++ b/CoreDataQueryInterfaceTests/Department.swift @@ -9,8 +9,7 @@ import Foundation import CoreData -class Department: NSManagedObject, EntityType { - typealias EntityAttributeType = DepartmentAttribute +class Department: NSManagedObject { @NSManaged var name: String } diff --git a/CoreDataQueryInterfaceTests/DepartmentAttribute.swift b/CoreDataQueryInterfaceTests/DepartmentAttribute.swift index 0433ea3..8651af7 100644 --- a/CoreDataQueryInterfaceTests/DepartmentAttribute.swift +++ b/CoreDataQueryInterfaceTests/DepartmentAttribute.swift @@ -1,14 +1,16 @@ -class DepartmentAttribute: Attribute { - private(set) var name: Attribute! - - private var _employees: EmployeeAttribute! - var employees: EmployeeAttribute { - if _employees == nil { _employees = EmployeeAttribute("employees", parent: self) } - return _employees - } - - required init(_ name: String? = nil, parent: AttributeType? = nil) { - super.init(name, parent: parent) - self.name = Attribute("name", parent: self) - } +// +// Generated by CDQI on 2015-12-30. +// +// This file was generated by a tool. Further invocations of this tool will overwrite this file. +// Edit it at your own risk. +// + +class DepartmentAttribute: Attribute, Aggregable { + private(set) lazy var name: KeyAttribute = { KeyAttribute("name", parent: self) }() + private(set) lazy var employees: EmployeeAttribute = { EmployeeAttribute("employees", parent: self) }() } + +extension Department: EntityType { + typealias EntityAttributeType = DepartmentAttribute +} + diff --git a/CoreDataQueryInterfaceTests/Employee.swift b/CoreDataQueryInterfaceTests/Employee.swift index 86a19df..caf82d9 100644 --- a/CoreDataQueryInterfaceTests/Employee.swift +++ b/CoreDataQueryInterfaceTests/Employee.swift @@ -9,8 +9,7 @@ import Foundation import CoreData -class Employee: NSManagedObject, EntityType { - typealias EntityAttributeType = EmployeeAttribute +class Employee: NSManagedObject { @NSManaged var firstName: String @NSManaged var lastName: String @NSManaged var nickName: String? diff --git a/CoreDataQueryInterfaceTests/EmployeeAttribute.swift b/CoreDataQueryInterfaceTests/EmployeeAttribute.swift index ee92a2b..73eb4ed 100644 --- a/CoreDataQueryInterfaceTests/EmployeeAttribute.swift +++ b/CoreDataQueryInterfaceTests/EmployeeAttribute.swift @@ -1,20 +1,19 @@ -class EmployeeAttribute: Attribute { - private(set) var firstName: Attribute! - private(set) var lastName: Attribute! - private(set) var nickName: Attribute! - private(set) var salary: Attribute! - - private var _department: DepartmentAttribute! - var department: DepartmentAttribute { - if _department == nil { _department = DepartmentAttribute("department", parent: self) } - return _department - } - - required init(_ name: String? = nil, parent: AttributeType? = nil) { - super.init(name, parent: parent) - self.firstName = Attribute("firstName", parent: self) - self.lastName = Attribute("lastName", parent: self) - self.nickName = Attribute("nickName", parent: self) - self.salary = Attribute("salary", parent: self) - } +// +// Generated by CDQI on 2015-12-30. +// +// This file was generated by a tool. Further invocations of this tool will overwrite this file. +// Edit it at your own risk. +// + +class EmployeeAttribute: Attribute, Aggregable { + private(set) lazy var firstName: KeyAttribute = { KeyAttribute("firstName", parent: self) }() + private(set) lazy var lastName: KeyAttribute = { KeyAttribute("lastName", parent: self) }() + private(set) lazy var nickName: KeyAttribute = { KeyAttribute("nickName", parent: self) }() + private(set) lazy var salary: KeyAttribute = { KeyAttribute("salary", parent: self) }() + private(set) lazy var department: DepartmentAttribute = { DepartmentAttribute("department", parent: self) }() } + +extension Employee: EntityType { + typealias EntityAttributeType = EmployeeAttribute +} + diff --git a/CoreDataQueryInterfaceTests/FilterTests.swift b/CoreDataQueryInterfaceTests/FilterTests.swift index b258412..8be452b 100644 --- a/CoreDataQueryInterfaceTests/FilterTests.swift +++ b/CoreDataQueryInterfaceTests/FilterTests.swift @@ -26,7 +26,7 @@ class FilterTests : BaseTestCase { } func testCountEmployeesNameNotMortonOrJones() { - let notMortonOrJonesCount = try! managedObjectContext.from(Employee).filter({ $0.lastName != ["Morton", "Jones"] }).count() + let notMortonOrJonesCount = try! managedObjectContext.from(Employee).filter({ !$0.lastName.among(["Morton", "Jones"]) }).count() XCTAssertEqual(notMortonOrJonesCount, 15) } @@ -34,14 +34,21 @@ class FilterTests : BaseTestCase { let employeeQuery = managedObjectContext.from(Employee) let firstObjectID = try! employeeQuery.objectIDs().first()! // Since employee in the filter resolves to the empty string, it is treated as SELF in the query. - let firstEmployee = try! employeeQuery.filter({ employee in employee == [firstObjectID] }).first()! + let predicate: EmployeeAttribute -> NSPredicate = { employee in employee.among([firstObjectID]) } + let firstEmployee = try! employeeQuery.filter(predicate).first()! XCTAssertEqual(firstObjectID, firstEmployee.objectID) } func testEmployeesWithHighSalaries() { let salary = 80000.32 // This will be a Double - let highSalaryCount = try! managedObjectContext.from(Employee).filter({ $0.salary >= salary }).count() + let e = EmployeeAttribute() + let highSalaryCount = try! managedObjectContext.from(Employee).filter(e.salary >= salary).count() XCTAssertEqual(highSalaryCount, 8) } + func testNumberOfDepartmentsWithEmployeesWhoseLastNamesStartWithSUsingSubquery() { + let departmentCount = try! managedObjectContext.from(Department).filter({ $0.employees.subquery("$e", predicate: { some($0.lastName.beginsWith("S", options: .CaseInsensitivePredicateOption)) }).count > 0 }).count() + XCTAssertEqual(departmentCount, 2) + } + } \ No newline at end of file diff --git a/CoreDataQueryInterfaceTests/OrderTests.swift b/CoreDataQueryInterfaceTests/OrderTests.swift index 425b950..3a3b3cc 100644 --- a/CoreDataQueryInterfaceTests/OrderTests.swift +++ b/CoreDataQueryInterfaceTests/OrderTests.swift @@ -12,7 +12,7 @@ import XCTest class OrderTests : BaseTestCase { func testFirstEmployeeInSalesOrderedDescendingByLastNameThenAscendingByFirstName() { - let employee = try! managedObjectContext.from(Employee).filter({ employee in employee.department.name == "Sales" }).order(descending: {$0.lastName}).order({$0.firstName}).first()! + let employee = try! managedObjectContext.from(Employee).filter({ (employee: EmployeeAttribute) -> NSPredicate in employee.department.name == "Sales" }).order(descending: {$0.lastName}).order({$0.firstName}).first()! XCTAssertEqual(employee.lastName, "Smith") XCTAssertEqual(employee.firstName, "David") } diff --git a/CoreDataQueryInterfaceTests/SelectionAndGroupingTests.swift b/CoreDataQueryInterfaceTests/SelectionAndGroupingTests.swift index 41dad0e..09b24ca 100644 --- a/CoreDataQueryInterfaceTests/SelectionAndGroupingTests.swift +++ b/CoreDataQueryInterfaceTests/SelectionAndGroupingTests.swift @@ -19,16 +19,16 @@ class SelectionTests : BaseTestCase { func testMaximumSalaryGroupedByDepartment() { let employee = EmployeeAttribute() - let result = try! managedObjectContext.from(Employee).groupBy(employee.department.name).select(employee.department.name).max(employee.salary).order(descending: employee.department.name).all() - let salaries: [String: Int] = result.toDictionary() { ($0["departmentName"]! as! String, ($0["maxSalary"]! as! NSNumber).integerValue) } + let result = try! managedObjectContext.from(Employee).groupBy(employee.department.name).select(employee.department.name, employee.max.salary.named("maxSalary", type: .FloatAttributeType)).order(descending: employee.department.name).all() + let salaries: [String: Int] = result.toDictionary() { ($0["department.name"]! as! String, ($0["maxSalary"]! as! NSNumber).integerValue) } XCTAssertEqual(salaries["Accounting"]!, 97000) XCTAssertEqual(salaries["Engineering"]!, 100000) XCTAssertEqual(salaries["Sales"]!, 93000) } func testAverageSalaryGroupedByDepartment() { - let result = try! managedObjectContext.from(Employee).groupBy({$0.department.name}).select({$0.department.name}).average({$0.salary}).order(descending: {$0.department.name}).all() - let salaries: [String: Int] = result.toDictionary() { ($0["departmentName"]! as! String, ($0["averageSalary"]! as! NSNumber).integerValue) } + let result = try! managedObjectContext.from(Employee).groupBy({$0.department.name}).select({$0.department.name}, {$0.average.salary.named("averageSalary", type: .FloatAttributeType)}).order(descending: {$0.department.name}).all() + let salaries: [String: Int] = result.toDictionary() { ($0["department.name"]! as! String, ($0["averageSalary"]! as! NSNumber).integerValue) } XCTAssertEqual(salaries["Accounting"]!, 73750) XCTAssertEqual(salaries["Engineering"]!, 75625) XCTAssertEqual(salaries["Sales"]!, 75111) diff --git a/README.md b/README.md index 39959c1..360694e 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ ![CoreDataQueryInterface](CoreDataQueryInterface.png) -[![Build Status](https://travis-ci.org/Prosumma/CoreDataQueryInterface.svg)](https://travis-ci.org/Prosumma/CoreDataQueryInterface) [![CocoaPods compatible](https://img.shields.io/cocoapods/v/CoreDataQueryInterface.svg)](https://cocoapods.org) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) -Core Data Query Interface (CDQI) is a type-safe, fluent, intuitive library for working with Core Data in Swift. CDQI tremendously reduces the amount of code needed to do Core Data, and dramatically improves readability by allowing method chaining and by eliminating magic strings. If you've worked with LINQ in the C# world, CDQI is a bit like LINQ. +Core Data Query Interface (CDQI) is a type-safe, fluent, intuitive library for working with Core Data in Swift. CDQI tremendously reduces the amount of code needed to do Core Data, and dramatically improves readability by allowing method chaining and by eliminating magic strings. CDQI is a bit like jQuery or LINQ, but for Core Data. -## Advantages +#### Advantages -The best way to understand the advantages of CDQI is to see an example. (Be sure to scroll right to see the entire example). +The best way to understand the advantages of CDQI is to see an example. ```swift -for employee in try! managedObjectContext.from(Employee).filter({$0.salary > 70000 && $0.department.name == "Engineering"}).order(descending: {$0.lastName}, {$0.firstName}).all() { - debugPrintln("\(employee.lastName), \(employee.firstName)") -} +try! managedObjectContext + .from(Employee) + .filter({$0.salary > 70000 && $0.department.name == "Engineering"}) + .order(descending: {$0.lastName}) + .all() ``` Now compare this to vanilla Core Data: @@ -21,178 +22,354 @@ Now compare this to vanilla Core Data: ```swift let fetchRequest = NSFetchRequest(entityName: "Employee") fetchRequest.predicate = NSPredicate(format: "salary > %ld && department.name == %@", 70000, "Engineering") -fetchRequest.sortDescriptors = [NSSortDescriptor(key: "lastName", ascending: false), NSSortDescriptor(key: "firstName", ascending: false)] -for employee in managedObjectContext.executeFetchRequest(fetchRequest, error: nil)! as! [Employee] { - debugPrintln("\(employee.lastName), \(employee.firstName)") -} +fetchRequest.sortDescriptors = [ + NSSortDescriptor(key: "lastName", ascending: false), + NSSortDescriptor(key: "firstName", ascending: false) + ] +try! managedObjectContext.executeFetchRequest(fetchRequest) ``` Compare the quantity and readability of the code in the two examples. Which would you rather write? -## Features +#### Features - [x] [Fluent interface](http://en.wikipedia.org/wiki/Fluent_interface), i.e., chainable methods - [x] Large number of useful overloads -- [x] Type-safety -- [x] Lazy execution +- [x] Type-safety. If your data model changes, magic strings won't give compilation errors. - [x] Three main query types: Entity, ManagedObjectID, and Dictionary (called "Expression" in CDQI) -- [x] Grouping, sorting, counts, etc. +- [x] Filtering, grouping, sorting, counts, etc. - [x] Optionally eliminates the use of magic strings so common in Core Data - [x] Query reuse, i.e., no side-effects from chaining - [x] Support for iOS and OS X -## Requirements +#### Compatibility + +2.x proxy classes are not compatible with CDQI 3.x. You must regenerate your proxy classes to use 3.x. Also, you should remove references to `EntityType` from your managed objects. The `EntityType` protocol is now automatically generated for each managed object class by the `cdqi` tool (formerly the `mocdqi` tool), as it should have been from the start. Other than that, 3.x should be fully backwards compatible with 3.x. + +#### What's New + +Besides many small improvements, the primary change is support for a great deal more predicates: `BEGINSWITH`, `SUBQUERY`, case-insensitive and diacritic-insensitive comparisons, etc. All without magic strings. + +```swift +moc.from(Department).filter() { department in department.employees.subquery({ any(employee in employee.lastName.beginsWith("s", .CaseInsensitivePredicateOption)) }).count > 0 } +``` -- iOS 8.1+ / Mac OS X 10.9+ -- Xcode 7.0+ -- Swift 2.0+ +Examples occur throughout the README and in the unit tests and sample project. ### Integration -In order to use CoreDataQueryInterface with your models, they must implement the `EntityType` protocol: +CDQI works its magic by creating proxy objects for each entity in your model, and then associating them with the corresponding entity via a `typealias`. A tool in the `bin` folder of the project called `cdqi` will create the proxies and associate them for you. ```swift -class Department : NSManagedObject, EntityType { - @NSManaged var name: String +// All of the code below is automatically generated by the cdqi tool. + +class DepartmentAttribute: Attribute, Aggregable { + private(set) lazy var name: KeyAttribute = { KeyAttribute("name", parent: self) }() } -class Employee : NSManagedObject, EntityType { - @NSManaged var department: Department - @NSManaged var firstName: String - @NSManaged var lastName: String - @NSManaged var salary: Int +// This assumes you have a managed entity called Department. +extension Department: EntityType { + typealias EntityAttributeType = DepartmentAttribute } ``` -#### Attributes +Here are some examples of the use of `cdqi`: + +```sh +# This generates proxies for entities in the Company data model. +# The current directory and all subdirectories are recursively searched +# until the Company data model is found. +cdqi Company + +# Same as above, but ignores the Employee entity wherever it occurs. +# You should use the name of the entity here, not the represented class. (See below.) +cdqi -xEmployee Company + +# By default, the tool won't overwrite an existing file. You can fix this with -f. +cdqi -f Company + +# By default, the tool places output files next to the data model file. +# You can change this easily with --out. +cdqi --out=~/Desktop -f Company + +# You can tell cdqi to start searching in any arbitrary folder with --in. +cdqi --in=~/Projects/FooBar --out=~/Desktop -xStudent School + +# If you like all your proxies in one file, it can be done with -m. +cdqi -m School + +# If you only want to write one entity to the output, use -w. +# This can be useful if only one entity has changed and the proxy +# for it needs to be regenerated. +# Be careful, though, this does not play well with -m. +# -w is not the inverse of -x. -w still assumes that other +# entities will not be excluded, so if those proxies are +# not also created, there could be compilation errors. +cdqi -wDepartment Company + +# You can also combine -x and -w. +# This writes out only the Department entity, but excludes +# any references to the Employee entity. +cdqi -xEmployee -wDepartment Company +``` + +Core Data models can have multiple versions. The `cdqi` tool always uses the current version, which it determines by consulting the `.xccurrentversion` file inside the `.xcdatamodeld` bundle. + +For obvious reasons, `cdqi` uses the _represented class_, not the name of the entity, when generating proxies. If an entity has no represented class, it will be skipped, and a warning issued on the command line. + +For further help on `cdqi`, issue `/path/to/cdqi --help` on the command line. + +Use of proxies is optional. If you don't use proxies, CDQI still greatly reduces the amount of code you have to write, but unfortunately you will have to use magic strings. Use of the `cdqi` tool is optional. It's just a simple code-generation tool. You can hand-code your proxies if you like. They really aren't that difficult. + +#### NSManagedObjectContext Integration + +If you implement the `ManagedObjectContextType` protocol on `NSManagedObjectContext`, CDQI will add some useful extension methods, such as `from`. I highly recommend this, but it is optional. You can put this code anywhere in your project. + +```swift +extension NSManagedObjectContext: ManagedObjectContextType {} +``` + +### Usage + +See the unit tests for many useful usage examples. All examples assume a prior familiarity with Core Data and `NSPredicate`. + +#### Starting a Query + +All Core Data queries are relative to a specific entity. If you implement the `ManagedObjectContextType` protocol on `NSManagedObjectContext` (as discussed above), you can use the `from` method to start a query and specify the entity: + +```swift +moc.from(Employee) +``` + +Without `ManagedObjectContextType`, you can start a query as follows, but you will have to specify a managed object context when the query is executed: + +```swift +EntityQuery.from(Department) +``` + +Going forward, we will assume you have implemented `ManagedObjectContextType` on `NSManagedObjectContext`. + +#### Proxies & Predicates + +The generated proxy classes all descend ultimately from `Attribute`. This class has various methods and operators whose purpose is to generate keypath expressions and predicates. + +```swift +let department = DepartmentAttribute() +let predicate: NSPredicate = department.name == "Engineering" +``` + +This is functionally equivalent to the following: + +```swift +let predicate = NSPredicate(format: "name == %@", "Engineering") +``` + +Many complex keypaths and predicates can be generated: + +```swift +let department = DepartmentAttribute() + +// These two lines are functionally equivalent. +let predicate: NSPredicate = any(department.employees.name.beginsWith("s", .CaseInsensitivePredicateOption)) +let predicate = NSPredicate(format: "ANY employees.name BEGINSWITH[c] %@", "s") + +let predicate: NSPredicate = department.employees.subquery({any($0.name == "Smith")}).count > 0 +let predicate = NSPredicate(format: "SUBQUERY(employees, $e, ANY $e.name == %@).@count > 0", "Smith") + +let predicate: NSPredicate = department.name == "Engineering" || department.name == "Accounting" +let predicate = NSPredicate(format: "(name == %@) || (name == %@)", "Engineering", "Accounting") +``` + +These capabilities of `Attribute` and its descendants are what lie at the heart of CDQI. Most types of predicates (even nested subqueries!) can be generated with this technique and without magic strings. For more examples, see the code itself and especially the unit tests. + +#### Filtering + +Unsurprisingly, CDQI's `filter` method makes extensive use of the capabilities of the `Attribute` class described in the previous section. In the most common case, the `filter` method takes a block which is passed an `Attribute` subclass and returns an instance of `NSPredicate`: -In order to use CoreDataQueryInterface's support for sorting, filtering, and the like without magic strings, you have to do a little more work. _This is completely optional but highly recommended._ (Don't despair. You can use the `mocdqi` tool detailed below to generate the attribute classes automatically.) +```swift +moc.from(Department).filter({department in department.name == "Engineering"}) +``` + +CDQI knows which proxy class to pass because of the association made by the `cdqi` tool. As a reminder: ```swift -class Department : NSManagedObject, EntityType { - // IMPORTANT: This is where you tell CDQI about the corresponding attribute type generated by the mocdqi tool. - // Without this, queries without magic strings won't work. +extension Department: EntityType { typealias EntityAttributeType = DepartmentAttribute - @NSManaged var name: String } +``` -class DepartmentAttribute : Attribute { - let name: AttributeType - required init(name: String? = nil, parent: AttributeType? = nil) { - super.init(name, parent: parent) - name = Attribute("name", parent: self) - } -} +Because we passed `Department` to the `from` method, CDQI knows that any filter methods will have to be passed an instance of `DepartmentAttribute`, so it automatically creates one and passes it. We can then use it to generate predicates without magic strings. -class Employee : NSManagedObject, EntityType { - // IMPORTANT: This is where you tell CDQI about the corresponding attribute type generated by the mocdqi tool. - // Without this, queries without magic strings won't work. - typealias EntityAttributeType = EmployeeAttribute - @NSManaged var department: Department - @NSManaged var firstName: String - @NSManaged var lastName: String - @NSManaged var salary: Int -} +The `filter` method has an overload that takes a plain `NSPredicate`, which we can generate with `Attribute` expressions. So if you're not a fan of curly braces, you can do the following: -class EmployeeAttribute : Attribute { - let department: AttributeType - let firstName: AttributeType - let lastName: AttributeType - let salary: AttributeType - required init(name: String? = nil, parent: AttributeType? = nil) { - super.init(name: parent: parent) - department = DepartmentAttribute("department", parent: self) - firstName = Attribute("firstName", parent: self) - lastName = Attribute("lastName", parent: self) - salary = Attribute("salary", parent: self) - } -} +```swift +let department = DepartmentAttribute() +moc.from(Department).filter(department.name == "Engineering") ``` -##### MOCDQI +If you find all this too magical, another kind of magic is available: -A tool called `mocdqi` is available in the `bin` directory of the project to generate attribute classes. By default, it starts in the current directory and searches for the data model with the name specified. If it finds this data model, it generates a file for each model it finds and writes these files next to the data model package. For example: +```swift +moc.from(Department).filter("name == %@", "Engineering") +``` -```bash -cd MyAwesomeProject -/path/to/mocdqi MyAwesomeData +Chaining filter methods together is the same as if `&&` were written between the predicates, e.g., + +```swift +// The following two statements are functionally equivalent +moc.from(Department).filter({department in department.name.beginsWith("E") && department.name.endsWith("ng")}) +moc.from(Department) + .filter({department in department.name.beginsWith("E")}) + .filter({department in department.name.endsWith("ng")}) ``` -If `MyAwesomeData` contains two models called `Order` and `OrderItem`, `mocdqi` will generate `OrderAttribute.swift` and `OrderItemAttribute.swift`. If either of these files exist, `mocdqi` will refuse to overwrite them unless the `--force` option is passed. You can tell `mocdqi` to start in a diferent folder from the current one by using the `-i` or `--in` option, e.g., +As always, see the unit tests for more examples. -```bash -/path/to/mocdqi --in=~/Projects/MyAwesomeProject MyAwesomeData +#### Sorting + +Sorting makes use of `Attribute`'s ability to generate keypaths, e.g., + +```swift +moc.from(Department).order({$0.lastName}) ``` -If you want `mocdqi` to put the generated files into a different folder, specify `-o` or `--out`, e.g., +Just as with filtering, `$0` is an instance of the associated proxy `Attribute` subclass, in this case `DepartmentAttribute`. + +If we want to sort descending, it's pretty simple. -```bash -/path/to/mocdqi --out=~/Desktop/ --in=~/Projects/MyAwesomeProject MyAwesomeData +```swift +moc.from(Department).order(descending: {department in department.lastName}) ``` -You can create a single file with all the attributes using the `-m` or `--merged` option: +Just as filtering ultimately involves creating instances of `NSPredicate`, our sort expressions built with `Attribute` subclasses will ultimately create `NSSortDescriptor` instances. In the case of sorting, we are not creating sort descriptors directly, but indirectly. Our blocks and other expressions return strings containing keypaths, which are then converted to the appropriate ascending or descending sort descriptor depending upon which overload of `order` is used. + +Despite this, we can always use `NSSortDescriptor` directly if we wish: -```bash -/path/to/mocdqi --out=~/Desktop/ --in=~/Projects/MyAwesomeProject --merged MyAwesomeData +```swift +moc.from(Department).order(sortDescriptor1, sortDescriptor2) ``` -This will generate a file called `MyAwesomeDataAttribute.swift`. By default, the generated class are not marked public. If you want to make them so, use `-p` or `--public`: +And strings: -```bash -/path/to/mocdqi --public --merged MyAwesomeData +```swift +moc.from(Department).order(descending: "manager.name").order("name") ``` -Lastly, you can exclude a particular entity by using the `-x` or `--exclude=` option. This option can be used multiple times. +This last shows that `order` can be chained as well, and the effects are cumulative, as one would expect. -```bash -/path/to/mocdqi --public --exclude=BadModel --exclude=TerribleEntity MyAwesomeData +#### Projection + +In vanilla Core Data, it's possible to return an array of dictionaries instead of managed objects. CDQI can do the same. Simply use the `select` method and the query type will automatically be changed. + +```swift +let department = DepartmentAttribute() +moc.from(Department).select(department.name, department.employees.average.salary).groupBy(department.name) ``` -If you do this, not only will these models be excluded, but also all relationship properties in other entities that reference them. +It's also possible to do this by starting with `ExpressionQuery`: -There are a few other minor options. Examine the source code to see them. The code is pretty straightforward. +```swift +let department = DepartmentAttribute() +ExpressionQuery.from(Department) +``` -### Usage +In this case, since no managed object context has been specified, it will have to be passed when executing the query. -All of the examples in this section assume that the `mocdqi` tool has been used to generate attribute classes to eliminate the use of magic strings. To see more extensive examples, take a look at the unit tests. +#### Executing Queries -#### Starting a Query +There are several query execution methods. I'll deal with each in turn, starting with `all`. + +```swift +try! moc.from(Department).filter({$0.name == "Engineering"}).all() +``` + +Very simply, this returns all managed object contexts of the appropriate type—`Department` in this case—which satisfy the query. -While there are other ways, the simplest way to start a query is to begin with an instance of `NSManagedObjectContext`, e.g., +For a query that did not start with a managed object context, it can be passed as the first parameter to `all`. ```swift -managedObjectContext.from(Employee) +try! EntityQuery.from(Department).order({$0.name}).all(moc) ``` -The parameter passed to `from` is a type, specifically an `NSManagedObject` subclass that implements the `EntityType` protocol. +As shown in the example above, all of the query execution methods take an optional `NSManagedObjectContext` as a parameter (though it isn't always the first parameter), so I won't show further examples of that. -#### Filtering +Next up is `count`, which works as advertised: + +```swift +let departmentCount = try! moc.from(Department).filter({$0.employees.count > 10}).count() +``` + +The `first` method returns either the first matching result or `nil`. + +```swift +let department = try! moc.from(Department).first()! +``` + +Obviously if there are no departments, we'll get a runtime exception, so I don't recommend force-unwrapping the optional returned by `first` unless you're very certain. + +The `array` query execution method is best demonstrated by example: ```swift -managedObjectContext.from(Employee).filter({ $0.department.name == "Engineering" && $0.salary < 70000 }) +let names: [String] = try! moc.from(Department).array({$0.name}) ``` -The parameter passed to the filter block `{ $0.department.name == "Engineering" && $0.salary < 70000 }` is an instance of `EmployeeAttribute`. +This returns an array of department names. Because it can only return a _single_ attribute of an entity, any previous `select`s are ignored. You must give the compiler enough type evidence to know what to cast its result to, which is why `[String]` was specified explicitly. -If multiple filters are chained together, the effect is as if `&&` was placed between them, e.g., +`value` is to `array` what `first` is to `all`. In other words, it returns the first matching attribute value. ```swift -managedObjectContext.from(Employee).filter({ $0.department.name == "Engineering" }).filter({ $0.salary < 70000 }) +let name: String = try! moc.from(Department).filter({none($0.employees.salary < 40000)}).value({$0.name})! ``` -#### Ordering +Like `first`, `value` returns `nil` if there was no matching value. Like `array`, it supercedes any prior `select`s and requires type evidence to know what to cast its value to. + +#### Limits ```swift -managedObjectContext.from(Department).order({ $0.name }) -managedObjectContext.from(Employee).order(descending: { $0.salary }).order({ $0.lastName }) +moc.from(Department).filter({$0.name.contains("e")}).limit(2) ``` -As with filtering, the parameter passed to the block is an instance of `EmployeeAttribute`. Chained `order` methods work exactly as you would expect. There are several overloads that allow you to express the same thing in different ways, whatever is most convenient. +Pretty obvious. + +#### Fetch Requests + +Sometimes we don't want to execute a query, but instead get the `NSFetchRequest` that would be generated. This is especially useful with `NSFetchedResultsController`. Doing so is very straightforward in CDQI: ```swift -managedObjectContext.from(Employee).order({ $0.lastName }, { $0.firstName }) -managedObjectContext.from(Employee).order({ [ $0.lastName, $0.firstName ] }) -let employee = EmployeeAttribute() -managedObjectContext.from(Employee).order(employee.lastName, employee.firstName) +let request = moc.from(Department).filter({$0.manager.lastName == "Smith"}).request() ``` +We can then use this fetch request however we like. + +#### Query Reuse + +Under the hood, CDQI uses value types to avoid side effects. This means that queries can be reused: + +```swift +let departmentQuery = moc.from(Department) // This is an instance of EntityQuery +let filteredDepartmentQuery = departmentQuery.filter() { $0.manager.salary > 100000 } +let sortedFilteredDepartmentQuery = filteredDepartmentQuery.order(descending: {$0.name}) +``` + +Each of these queries was built from the one before it, but each subsequent query has no side effects on the previous ones. The only drawback here is that we are also storing a reference to our managed object context. This can be avoided by starting with `EntityQuery` instead of the context. + +```swift +let departmentQuery = EntityQuery.from(Department) // This is an instance of EntityQuery +let filteredDepartmentQuery = departmentQuery.filter() { $0.manager.salary > 100000 } +let sortedFilteredDepartmentQuery = filteredDepartmentQuery.order(descending: {$0.name}) +``` + +Since no managed object context is associated with these queries, we have to specify one when they are executed, either by passing an instance of `NSManagedObjectContext` to the query execution method or using the `context` method: + +```swift +sortedFilteredDepartmentQuery.all(moc) +sortedFilteredDepartmentQuery.context(moc).all() +``` + +This capability allows us to store frequently used queries for reuse, perhaps as static variables on a class, e.g., + +```swift +class DepartmentQueries { + static let AllDepartments = EntityQuery.from(Department) +} +``` diff --git a/README.v2.4.md b/README.v2.4.md new file mode 100644 index 0000000..39959c1 --- /dev/null +++ b/README.v2.4.md @@ -0,0 +1,198 @@ +![CoreDataQueryInterface](CoreDataQueryInterface.png) + +[![Build Status](https://travis-ci.org/Prosumma/CoreDataQueryInterface.svg)](https://travis-ci.org/Prosumma/CoreDataQueryInterface) +[![CocoaPods compatible](https://img.shields.io/cocoapods/v/CoreDataQueryInterface.svg)](https://cocoapods.org) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) + +Core Data Query Interface (CDQI) is a type-safe, fluent, intuitive library for working with Core Data in Swift. CDQI tremendously reduces the amount of code needed to do Core Data, and dramatically improves readability by allowing method chaining and by eliminating magic strings. If you've worked with LINQ in the C# world, CDQI is a bit like LINQ. + +## Advantages + +The best way to understand the advantages of CDQI is to see an example. (Be sure to scroll right to see the entire example). + +```swift +for employee in try! managedObjectContext.from(Employee).filter({$0.salary > 70000 && $0.department.name == "Engineering"}).order(descending: {$0.lastName}, {$0.firstName}).all() { + debugPrintln("\(employee.lastName), \(employee.firstName)") +} +``` + +Now compare this to vanilla Core Data: + +```swift +let fetchRequest = NSFetchRequest(entityName: "Employee") +fetchRequest.predicate = NSPredicate(format: "salary > %ld && department.name == %@", 70000, "Engineering") +fetchRequest.sortDescriptors = [NSSortDescriptor(key: "lastName", ascending: false), NSSortDescriptor(key: "firstName", ascending: false)] +for employee in managedObjectContext.executeFetchRequest(fetchRequest, error: nil)! as! [Employee] { + debugPrintln("\(employee.lastName), \(employee.firstName)") +} +``` + +Compare the quantity and readability of the code in the two examples. Which would you rather write? + +## Features + +- [x] [Fluent interface](http://en.wikipedia.org/wiki/Fluent_interface), i.e., chainable methods +- [x] Large number of useful overloads +- [x] Type-safety +- [x] Lazy execution +- [x] Three main query types: Entity, ManagedObjectID, and Dictionary (called "Expression" in CDQI) +- [x] Grouping, sorting, counts, etc. +- [x] Optionally eliminates the use of magic strings so common in Core Data +- [x] Query reuse, i.e., no side-effects from chaining +- [x] Support for iOS and OS X + +## Requirements + +- iOS 8.1+ / Mac OS X 10.9+ +- Xcode 7.0+ +- Swift 2.0+ + +### Integration + +In order to use CoreDataQueryInterface with your models, they must implement the `EntityType` protocol: + +```swift +class Department : NSManagedObject, EntityType { + @NSManaged var name: String +} + +class Employee : NSManagedObject, EntityType { + @NSManaged var department: Department + @NSManaged var firstName: String + @NSManaged var lastName: String + @NSManaged var salary: Int +} +``` + +#### Attributes + +In order to use CoreDataQueryInterface's support for sorting, filtering, and the like without magic strings, you have to do a little more work. _This is completely optional but highly recommended._ (Don't despair. You can use the `mocdqi` tool detailed below to generate the attribute classes automatically.) + +```swift +class Department : NSManagedObject, EntityType { + // IMPORTANT: This is where you tell CDQI about the corresponding attribute type generated by the mocdqi tool. + // Without this, queries without magic strings won't work. + typealias EntityAttributeType = DepartmentAttribute + @NSManaged var name: String +} + +class DepartmentAttribute : Attribute { + let name: AttributeType + required init(name: String? = nil, parent: AttributeType? = nil) { + super.init(name, parent: parent) + name = Attribute("name", parent: self) + } +} + +class Employee : NSManagedObject, EntityType { + // IMPORTANT: This is where you tell CDQI about the corresponding attribute type generated by the mocdqi tool. + // Without this, queries without magic strings won't work. + typealias EntityAttributeType = EmployeeAttribute + @NSManaged var department: Department + @NSManaged var firstName: String + @NSManaged var lastName: String + @NSManaged var salary: Int +} + +class EmployeeAttribute : Attribute { + let department: AttributeType + let firstName: AttributeType + let lastName: AttributeType + let salary: AttributeType + required init(name: String? = nil, parent: AttributeType? = nil) { + super.init(name: parent: parent) + department = DepartmentAttribute("department", parent: self) + firstName = Attribute("firstName", parent: self) + lastName = Attribute("lastName", parent: self) + salary = Attribute("salary", parent: self) + } +} +``` + +##### MOCDQI + +A tool called `mocdqi` is available in the `bin` directory of the project to generate attribute classes. By default, it starts in the current directory and searches for the data model with the name specified. If it finds this data model, it generates a file for each model it finds and writes these files next to the data model package. For example: + +```bash +cd MyAwesomeProject +/path/to/mocdqi MyAwesomeData +``` + +If `MyAwesomeData` contains two models called `Order` and `OrderItem`, `mocdqi` will generate `OrderAttribute.swift` and `OrderItemAttribute.swift`. If either of these files exist, `mocdqi` will refuse to overwrite them unless the `--force` option is passed. You can tell `mocdqi` to start in a diferent folder from the current one by using the `-i` or `--in` option, e.g., + +```bash +/path/to/mocdqi --in=~/Projects/MyAwesomeProject MyAwesomeData +``` + +If you want `mocdqi` to put the generated files into a different folder, specify `-o` or `--out`, e.g., + +```bash +/path/to/mocdqi --out=~/Desktop/ --in=~/Projects/MyAwesomeProject MyAwesomeData +``` + +You can create a single file with all the attributes using the `-m` or `--merged` option: + +```bash +/path/to/mocdqi --out=~/Desktop/ --in=~/Projects/MyAwesomeProject --merged MyAwesomeData +``` + +This will generate a file called `MyAwesomeDataAttribute.swift`. By default, the generated class are not marked public. If you want to make them so, use `-p` or `--public`: + +```bash +/path/to/mocdqi --public --merged MyAwesomeData +``` + +Lastly, you can exclude a particular entity by using the `-x` or `--exclude=` option. This option can be used multiple times. + +```bash +/path/to/mocdqi --public --exclude=BadModel --exclude=TerribleEntity MyAwesomeData +``` + +If you do this, not only will these models be excluded, but also all relationship properties in other entities that reference them. + +There are a few other minor options. Examine the source code to see them. The code is pretty straightforward. + +### Usage + +All of the examples in this section assume that the `mocdqi` tool has been used to generate attribute classes to eliminate the use of magic strings. To see more extensive examples, take a look at the unit tests. + +#### Starting a Query + +While there are other ways, the simplest way to start a query is to begin with an instance of `NSManagedObjectContext`, e.g., + +```swift +managedObjectContext.from(Employee) +``` + +The parameter passed to `from` is a type, specifically an `NSManagedObject` subclass that implements the `EntityType` protocol. + +#### Filtering + +```swift +managedObjectContext.from(Employee).filter({ $0.department.name == "Engineering" && $0.salary < 70000 }) +``` + +The parameter passed to the filter block `{ $0.department.name == "Engineering" && $0.salary < 70000 }` is an instance of `EmployeeAttribute`. + +If multiple filters are chained together, the effect is as if `&&` was placed between them, e.g., + +```swift +managedObjectContext.from(Employee).filter({ $0.department.name == "Engineering" }).filter({ $0.salary < 70000 }) +``` + +#### Ordering + +```swift +managedObjectContext.from(Department).order({ $0.name }) +managedObjectContext.from(Employee).order(descending: { $0.salary }).order({ $0.lastName }) +``` + +As with filtering, the parameter passed to the block is an instance of `EmployeeAttribute`. Chained `order` methods work exactly as you would expect. There are several overloads that allow you to express the same thing in different ways, whatever is most convenient. + +```swift +managedObjectContext.from(Employee).order({ $0.lastName }, { $0.firstName }) +managedObjectContext.from(Employee).order({ [ $0.lastName, $0.firstName ] }) +let employee = EmployeeAttribute() +managedObjectContext.from(Employee).order(employee.lastName, employee.firstName) +``` + diff --git a/bin/cdqi b/bin/cdqi new file mode 100755 index 0000000..eb2b13b --- /dev/null +++ b/bin/cdqi @@ -0,0 +1,213 @@ +#!/usr/bin/env ruby + +require 'date' +require 'find' +require 'optparse' +require 'ostruct' + +RESERVED_ATTRIBUTE_NAMES = %w{count sum average max min} + +$options = OpenStruct::new +$options.force = false +$options.input_path = "." +$options.output_path = nil +$options.merged = false +$options.excluded_entities = [] +$options.included_entities = [] +$options.access_modifier = "" +$options.suffix = "Attribute" +$options.indent = 4 + +option_parser = OptionParser::new do |opts| + opts.on("-f", "--force", "Force overwrite existing files") do |opt| + $options.force = opt + end + opts.on("-i", "--in=PATH", "Where to start searching for the data model (defaults to current directory)") do |opt| + $options.input_path = File::expand_path(opt) + end + opts.on("-o", "--out=PATH", "Where to put the resulting files (defaults to same directory as data model)") do |opt| + $options.output_path = File::expand_path(opt) + end + opts.on("-m", "--merged", "Creates a single file using the name of the data model") do |opt| + $options.merged = opt + end + opts.on("-x", "--exclude=ENTITY", "Exclude an entity") do |opt| + $options.excluded_entities << opt + end + opts.on("-w", "--write=ENTITY", "Write only this entity to the output") do |opt| + $options.included_entities << opt + end + opts.on("-p", "--public", "Make generated classes public") do |opt| + $options.access_modifier = opt ? 'public ' : '' + end + opts.on("-s", "--suffix=SUFFIX", "Suffix to use on generated classes and filenames (defaults to Attribute)") do |opt| + $options.suffix = opt + end + opts.on("-n", "--indent=COUNT", OptionParser::DecimalInteger, "Number of spaces to indent (defaults to 2)") do |opt| + $options.indent = opt + end +end + +models = option_parser.parse(ARGV) +if models.length == 0 + abort "ERROR: No data model specified. Quitting." +elsif models.length > 1 + abort "ERROR: Multiple data models specified. Please specify only one data model. Quitting." +end + +model = models[0] +model_path = nil + +Find::find($options.input_path) do |path| + if path =~ /#{Regexp.quote(model)}\.xcdatamodeld$/ + model_path = path + $options.output_path ||= File::dirname(path) + break + end +end + +abort "Could not find #{model} data model in #{$options.input_path}. Quitting." if model_path.nil? + +require 'rexml/document' +require 'rexml/xpath' + +class IndentWriter + def initialize(io) + @io = io + @level = 0 + end + def puts(s) + @io.puts "#{''.ljust($options.indent * @level, ' ')}#{s}" + end + def indent(level = 1) + @level += level + yield + @level -= level + end +end + +class Attribute + attr_reader :name, :entity + def initialize(name, entity: nil) + @name = name + @entity = entity + end + def render_property(io) + attribute = "#{@entity || 'Key'}#{$options.suffix}" + io.puts "#{$options.access_modifier}private(set) lazy var #{@name}: #{attribute} = { #{attribute}(\"#{@name}\", parent: self) }()" + end +end + +class Entity + attr_reader :name + def initialize(name) + @name = name + @attributes = [] + end + def <<(attribute) + @attributes << attribute + end + def render(io) + io.puts "#{$options.access_modifier}class #{@name}#{$options.suffix}: Attribute, Aggregable {" + io.indent do + @attributes.each do |attribute| + attribute.render_property(io) + end + end + io.puts "}" + io.puts "" + io.puts "extension #{@name}: EntityType {" + io.indent do + io.puts "#{$options.access_modifier}typealias EntityAttributeType = #{@name}#{$options.suffix}" + end + io.puts "}" + io.puts "" + end +end + +entities = [] + +File::open(File::join(model_path, ".xccurrentversion")) do |fd| + xml = REXML::Document::new(fd) + model_path = File::join(model_path, REXML::XPath::first(xml, "//key[.='_XCCurrentVersionName']/following-sibling::string").text) + $stderr.puts "NOTICE: Using data model at #{model_path}." + model_path = File::join(model_path, 'contents') +end + +File::open(model_path) do |fd| + xml = REXML::Document::new(fd) + xml.root.elements.each('entity') do |entity_node| + represented_class = entity_node.attributes['representedClassName'] + entity_name = entity_node.attributes['name'] + if represented_class.nil? + $stderr.puts "WARNING: The '#{entity_name}' entity was skipped because it has no represented class." + next + end + if represented_class == 'Key' && $options.suffix == 'Attribute' + $stderr.puts "WARNING: The entity '#{entity_name}' represented by the class 'Key' was skipped because its name would conflict with CDQI's 'KeyAttribute'. You can avoid this by using a different represented class name or a suffix other than 'Attribute'." + $options.excluded_entities << entity_name + end + if !$options.excluded_entities.include?(entity_name) && ($options.included_entities.empty? || $options.included_entities.include?(entity_name)) + entity_name = represented_class.split('.')[-1] + entity = Entity::new(entity_name) + entity_node.elements.each('attribute') do |attribute_node| + attribute_name = attribute_node.attributes['name'] + if !RESERVED_ATTRIBUTE_NAMES.include?(attribute_name) + entity << Attribute::new(attribute_name) + else + $stderr.puts "WARNING: The attribute '#{attribute_name}' in the '#{entity_node.attributes['name']}' entity has been skipped because it conflicts with the name of an aggregate property." + end + end + entity_node.elements.each('relationship') do |relationship_node| + destination_entity_name = relationship_node.attributes['destinationEntity'] + destination_class = REXML::XPath::first(xml, "//entity[@name='#{destination_entity_name}']").attributes['representedClassName'] + unless $options.excluded_entities.include?(destination_entity_name) || destination_class.nil? + destination_entity_name = destination_class.split('.')[-1] + attribute_name = relationship_node.attributes['name'] + entity << Attribute::new(attribute_name, entity: destination_entity_name) + end + end + entities << entity + end + end +end + +def write_file_header(io) + io.puts "//" + io.puts "// Generated by CDQI on #{DateTime::now.strftime('%Y-%m-%d')}." + io.puts "//" + io.puts "// This file was generated by a tool. Further invocations of this tool will overwrite this file." + io.puts "// Edit it at your own risk." + io.puts "//" + io.puts "" +end + +if $options.merged + output_file = File.join($options.output_path, "#{model}#{$options.suffix}.swift") + if $options.force || !File.exists?(output_file) + File::open(output_file, 'w') do |io| + write_file_header io + iw = IndentWriter::new(io) + entities.each do |entity| + entity.render iw + end + end + $stderr.puts "NOTICE: Wrote file #{output_file}." + else + abort "ERROR: The file '#{output_file}' exists and --force was not specified. Quitting." + end +else + entities.each do |entity| + output_file = File.join($options.output_path, "#{entity.name}#{$options.suffix}.swift") + if $options.force || !File.exists?(output_file) + File::open(output_file, 'w') do |io| + write_file_header io + entity.render IndentWriter::new(io) + end + $stderr.puts "NOTICE: Wrote file #{output_file}." + else + $stderr.puts "WARNING: The file '#{output_file}' exists and --force was not specified. Skipping." + end + end +end +