├── .gitignore ├── .swift-version ├── .travis.yml ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── querykit-cli │ ├── QueryKit.swift │ └── main.swift └── share └── querykit └── template.swift /.gitignore: -------------------------------------------------------------------------------- 1 | /.build/ 2 | /Packages/ 3 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode11.3 3 | install: 4 | - eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" 5 | script: 6 | - make build 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR := /usr/local 2 | 3 | build: 4 | @echo "DEPRECATED: QueryKit CLI is no longer needed, QueryKit 0.14.0 works with KeyPath, see https://github.com/QueryKit/QueryKit/pull/55." 5 | @swift build --configuration release 6 | 7 | install: build 8 | install -d "$(DESTDIR)/bin" 9 | install -d "$(DESTDIR)/share/querykit" 10 | install -C -m 755 ".build/release/querykit" "$(DESTDIR)/bin/querykit" 11 | install -C -m 644 "share/querykit/template.swift" "$(DESTDIR)/share/querykit" 12 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Commander", 6 | "repositoryURL": "https://github.com/kylef/Commander.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "4b6133c3071d521489a80c38fb92d7983f19d438", 10 | "version": "0.9.1" 11 | } 12 | }, 13 | { 14 | "package": "PathKit", 15 | "repositoryURL": "https://github.com/kylef/PathKit.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "e2f5be30e4c8f531c9c1e8765aa7b71c0a45d7a0", 19 | "version": "0.9.2" 20 | } 21 | }, 22 | { 23 | "package": "Spectre", 24 | "repositoryURL": "https://github.com/kylef/Spectre.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", 28 | "version": "0.9.0" 29 | } 30 | }, 31 | { 32 | "package": "Stencil", 33 | "repositoryURL": "https://github.com/stencilproject/Stencil.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "0e9a78d6584e3812cd9c09494d5c7b483e8f533c", 37 | "version": "0.13.1" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | import PackageDescription 3 | 4 | 5 | let package = Package( 6 | name: "querykit", 7 | products: [ 8 | .executable(name: "querykit", targets: ["querykit-cli"]), 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/stencilproject/Stencil.git", from: "0.13.1"), 12 | .package(url: "https://github.com/kylef/Commander.git", from: "0.9.1"), 13 | ], 14 | targets: [ 15 | .target(name: "querykit-cli", dependencies: ["Stencil", "Commander"]), 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | QueryKit Logo 2 | 3 | __DEPRECATED__: _QueryKit CLI is no longer needed, QueryKit 0.14.0 works with 4 | KeyPath, see https://github.com/QueryKit/QueryKit/pull/55._ 5 | 6 | # QueryKit CLI 7 | 8 | A command line tool to generate extensions for your Core Data models 9 | including QueryKit attributes for type-safe Core Data querying. 10 | Allowing you to use Xcode generated managed object classes with QueryKit. 11 | 12 | ## Installation 13 | 14 | Installation can be done via [Homebrew](http://brew.sh) as follows: 15 | 16 | ```shell 17 | $ brew install querykit/formulae/querykit-cli 18 | ``` 19 | 20 | Alternatively, you can build it yourself with the Swift package manager. 21 | 22 | ```shell 23 | $ git clone https://github.com/QueryKit/querykit-cli 24 | $ cd querykit-cli 25 | $ make install 26 | ``` 27 | 28 | ## Usage 29 | 30 | You can run QueryKit against your model providing the directory to save the 31 | extensions. 32 | 33 | ```bash 34 | $ querykit 35 | ``` 36 | 37 | ### Example 38 | 39 | We've provided an [example application](http://github.com/QueryKit/TodoExample) using QueryKit's CLI tool. 40 | 41 | ``` 42 | $ querykit Todo/Model.xcdatamodeld Todo/Model 43 | -> Generated 'Task' 'Todo/Model/Task+QueryKit.swift' 44 | -> Generated 'User' 'Todo/Model/User+QueryKit.swift' 45 | ``` 46 | 47 | QueryKit CLI will generate a `QueryKit` extension for each of your managed 48 | object subclasses that you can add to Xcode, in this case `Task` and `User`. 49 | 50 | These extensions provide you with properties on your model for type-safe filtering and ordering. 51 | 52 | #### Task+QueryKit.swift 53 | 54 | ```swift 55 | import QueryKit 56 | 57 | extension Task { 58 | static var createdAt:Attribute { return Attribute("createdAt") } 59 | static var creator:Attribute { return Attribute("creator") } 60 | static var name:Attribute { return Attribute("name") } 61 | static var complete:Attribute { return Attribute("complete") } 62 | } 63 | 64 | extension Attribute where AttributeType: Task { 65 | var creator:Attribute { return attribute(AttributeType.creator) } 66 | var createdAt:Attribute { return attribute(AttributeType.createdAt) } 67 | var name:Attribute { return attribute(AttributeType.name) } 68 | var complete:Attribute { return attribute(AttributeType.complete) } 69 | } 70 | ``` 71 | 72 | #### User+QueryKit.swift 73 | 74 | ```swift 75 | import QueryKit 76 | 77 | extension User { 78 | static var name:Attribute { return Attribute("name") } 79 | } 80 | 81 | extension Attribute where AttributeType: User { 82 | var name:Attribute { return attribute(AttributeType.name) } 83 | } 84 | ``` 85 | 86 | We can use these properties in conjunction with QueryKit to build type-safe 87 | queries. For example, here we are querying for all the Tasks which are 88 | created by a user with the name `Kyle` which are completed, ordered 89 | by their created date. 90 | 91 | ```swift 92 | Task.queryset(context) 93 | .filter { $0.user.name == "Kyle" } 94 | .exclude { $0.completed == true } 95 | .orderBy { $0.createdAt.ascending } 96 | ``` 97 | 98 | ### Customising 99 | 100 | You may pass a custom template to the QueryKit tool, please see the standard 101 | template for more information on the syntax. 102 | 103 | ```shell 104 | $ querykit Todo/Model.xcdatamodeld Todo/Model --template share/querykit/template.swift 105 | ``` 106 | 107 | -------------------------------------------------------------------------------- /Sources/querykit-cli/QueryKit.swift: -------------------------------------------------------------------------------- 1 | import Darwin.libc 2 | import CoreData 3 | import Commander 4 | import PathKit 5 | import Stencil 6 | 7 | 8 | extension Path { 9 | static var processPath: Path { 10 | if ProcessInfo.processInfo.arguments[0].components(separatedBy: Path.separator).count > 1 { 11 | return Path.current + ProcessInfo.processInfo.arguments[0] 12 | } 13 | 14 | let PATH = ProcessInfo.processInfo.environment["PATH"]! 15 | let paths = PATH.components(separatedBy: ":").map { 16 | Path($0) + ProcessInfo.processInfo.arguments[0] 17 | }.filter { $0.exists } 18 | 19 | return paths.first! 20 | } 21 | 22 | public static var defaultTemplatePath: Path { 23 | return processPath + "../../share/querykit/template.swift" 24 | } 25 | } 26 | 27 | 28 | func compileCoreDataModel(_ source: Path) -> Path { 29 | let destinationExtension = source.`extension`!.hasSuffix("d") ? ".momd" : ".mom" 30 | let filename = source.lastComponentWithoutExtension + destinationExtension 31 | let destination = try! Path.uniqueTemporary() + Path(filename) 32 | 33 | Process.launchedProcess( 34 | launchPath: "/usr/bin/xcrun", 35 | arguments: ["momc", source.absolute().string, destination.absolute().string] 36 | ).waitUntilExit() 37 | 38 | return destination 39 | } 40 | 41 | class AttributeDescription : NSObject { 42 | let name: String 43 | let type: String 44 | 45 | init(name: String, type: String) { 46 | self.name = name 47 | self.type = type 48 | } 49 | } 50 | 51 | extension NSAttributeDescription { 52 | var qkClassName: String? { 53 | switch attributeType { 54 | case .booleanAttributeType: 55 | return "Bool" 56 | case .stringAttributeType: 57 | return "String" 58 | default: 59 | return attributeValueClassName 60 | } 61 | } 62 | 63 | var qkAttributeDescription: AttributeDescription? { 64 | if let className = qkClassName { 65 | return AttributeDescription(name: name, type: className) 66 | } 67 | 68 | return nil 69 | } 70 | } 71 | 72 | extension NSRelationshipDescription { 73 | var qkAttributeDescription: AttributeDescription? { 74 | if let destinationEntity = destinationEntity { 75 | var type = destinationEntity.qk_className 76 | 77 | if isToMany { 78 | type = "Set<\(type)>" 79 | 80 | if isOrdered { 81 | type = "NSOrderedSet" 82 | } 83 | } 84 | 85 | return AttributeDescription(name: name, type: type) 86 | } 87 | 88 | return nil 89 | } 90 | } 91 | 92 | extension NSEntityDescription { 93 | var qk_className: String { 94 | if managedObjectClassName.hasPrefix(".") { 95 | // "Current Module" 96 | return managedObjectClassName.substring(from: managedObjectClassName.index(after: managedObjectClassName.startIndex)) 97 | } 98 | 99 | return managedObjectClassName 100 | } 101 | 102 | func qk_hasSuperProperty(_ name: String) -> Bool { 103 | if let superentity = superentity { 104 | if superentity.qk_className != "NSManagedObject" && superentity.propertiesByName[name] != nil { 105 | return true 106 | } 107 | 108 | return superentity.qk_hasSuperProperty(name) 109 | } 110 | 111 | return false 112 | } 113 | } 114 | 115 | class CommandError : Error { 116 | let description: String 117 | 118 | init(description: String) { 119 | self.description = description 120 | } 121 | } 122 | 123 | func render(entity: NSEntityDescription, destination: Path, template: Template) throws { 124 | let attributes = entity.properties.flatMap { property -> AttributeDescription? in 125 | if entity.qk_hasSuperProperty(property.name) { 126 | return nil 127 | } 128 | 129 | if let attribute = property as? NSAttributeDescription { 130 | return attribute.qkAttributeDescription 131 | } else if let relationship = property as? NSRelationshipDescription { 132 | return relationship.qkAttributeDescription 133 | } 134 | 135 | return nil 136 | } 137 | 138 | let context: [String: Any] = [ 139 | "className": entity.qk_className, 140 | "attributes": attributes, 141 | "entityName": entity.name ?? "Unknown", 142 | ] 143 | 144 | try destination.write(try template.render(context)) 145 | } 146 | 147 | func render(model: NSManagedObjectModel, destination: Path, templatePath: Path) throws { 148 | if !destination.exists { 149 | try destination.mkpath() 150 | } 151 | 152 | for entity in model.entities { 153 | let loader = FileSystemLoader(paths: [templatePath.parent().absolute()]) 154 | let environment = Environment(loader: loader) 155 | let template = try environment.loadTemplate(name: templatePath.lastComponent) 156 | let className = entity.qk_className 157 | 158 | if className == "NSManagedObject" { 159 | let name = entity.name ?? "Unknown" 160 | print("-> Skipping entity '\(name)', doesn't use a custom class.") 161 | continue 162 | } 163 | 164 | let destinationFile = destination + (className + "+QueryKit.swift") 165 | 166 | do { 167 | try render(entity: entity, destination: destinationFile, template: template) 168 | print("-> Generated '\(className)' '\(destinationFile)'") 169 | } catch { 170 | print(error) 171 | } 172 | } 173 | } 174 | 175 | public func generate(model: Path, output: Path, template: Path) throws { 176 | let compiledModel = compileCoreDataModel(model) 177 | let modelURL = URL(fileURLWithPath: compiledModel.description) 178 | let model = NSManagedObjectModel(contentsOf: modelURL)! 179 | try render(model: model, destination: output, templatePath: template) 180 | } 181 | 182 | extension Path: ArgumentConvertible { 183 | public init(parser: ArgumentParser) throws { 184 | if let path = parser.shift() { 185 | self.init(path) 186 | } else { 187 | throw ArgumentError.missingValue(argument: nil) 188 | } 189 | } 190 | } 191 | 192 | public func isReadable(_ path: Path) -> Path { 193 | if !path.isReadable { 194 | print("'\(path)' does not exist or is not readable.") 195 | exit(1) 196 | } 197 | 198 | return path 199 | } 200 | 201 | public func isCoreDataModel(_ path: Path) -> Path { 202 | let ext = path.`extension` 203 | if ext == "xcdatamodel" || ext == "xcdatamodeld" { 204 | return isReadable(path) 205 | } 206 | 207 | print("'\(path)' is not a Core Data model.") 208 | exit(1) 209 | } 210 | -------------------------------------------------------------------------------- /Sources/querykit-cli/main.swift: -------------------------------------------------------------------------------- 1 | import Darwin.libc 2 | import PathKit 3 | import Commander 4 | 5 | 6 | let version = "0.2.0" 7 | 8 | command( 9 | Option("template", default: Path.defaultTemplatePath, description: "Path to a custom template file", validator: isReadable), 10 | Argument("model", validator: isCoreDataModel), 11 | Argument("output", validator: isReadable) 12 | ) { template, model, output in 13 | do { 14 | try generate(model: model, output: output, template: template) 15 | } catch { 16 | print(error) 17 | exit(1) 18 | } 19 | }.run(version) 20 | 21 | -------------------------------------------------------------------------------- /share/querykit/template.swift: -------------------------------------------------------------------------------- 1 | // This is a generated file from QueryKit. 2 | // https://github.com/QueryKit/querykit-cli 3 | 4 | import QueryKit 5 | 6 | /// Extension to {{ className }} providing an QueryKit attribute descriptions. 7 | extension {{ className }} {{ "{" }}{% for attribute in attributes %} 8 | static var {{ attribute.name }}:Attribute<{{ attribute.type }}> { return Attribute("{{ attribute.name }}") }{% endfor %} 9 | 10 | class func queryset(context:NSManagedObjectContext) -> QuerySet<{{ className }}> { 11 | return QuerySet(context, "{{ entityName }}") 12 | } 13 | } 14 | 15 | extension Attribute where AttributeType: {{ className }} {{ "{" }}{% for attribute in attributes %} 16 | var {{ attribute.name }}:Attribute<{{ attribute.type }}> { return attribute(AttributeType.{{ attribute.name }}) }{% endfor %} 17 | } 18 | 19 | --------------------------------------------------------------------------------