├── .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 |
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 |
--------------------------------------------------------------------------------