├── .gitignore ├── test.sh ├── templates └── entry.swift ├── LICENSE ├── README.md └── bin └── contentful-generator /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . /usr/local/opt/chswift/share/chswift/chswift.sh 4 | chswift 2.1 5 | 6 | FRAMEWORKS=$HOME/.📦/contentful-generator/Rome 7 | 8 | mkdir -p out 9 | rm -f out/* 10 | 11 | ./bin/contentful-generator cfexampleapi b4c0n73n7fu1 --output out 12 | 13 | cd out 14 | xcrun --sdk macosx swiftc -F$FRAMEWORKS -framework Contentful \ 15 | -module-name YOLO -c *.swift 16 | rm -f *.o 17 | 18 | -------------------------------------------------------------------------------- /templates/entry.swift: -------------------------------------------------------------------------------- 1 | // This is a generated file. 2 | 3 | import CoreLocation 4 | 5 | struct {{ className }} {{% for field in fields %} 6 | let {{ field.name }}: {{ field.type }}?{% endfor %} 7 | } 8 | 9 | import Contentful 10 | 11 | extension {{ className }} { 12 | static func fromEntry(entry: Entry) throws -> {{ className }} { 13 | return {{ className }}({% for field in fields %} 14 | {{ field.name }}: entry.fields["{{ field.name }}"] as? {{ field.type }},{% endfor %}) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Contentful GmbH - https://www.contentful.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contentful Swift Generator 2 | 3 | Script for generating structs with concretely typed properties from a [Contentful][1] content 4 | model. This is meant to be used in conjunction with our [Swift SDK][3] to increase the 5 | compile-time safety of your applications. 6 | 7 | Using the generated structs will ensure that you only access properties which are actually 8 | defined in your content model and that the correct types are used for them. It will also decouple 9 | your application from Contentful, as the model structs only depend on the SDK for their 10 | conversion method. 11 | 12 | [Contentful][1] is a content management platform for web applications, mobile apps and connected devices. It allows you to create, edit & manage content in the cloud and publish it anywhere via powerful API. Contentful offers tools for managing editorial teams and enabling cooperation between organizations. 13 | 14 | ## Usage 15 | 16 | Executing the script will generate one Swift file per content type in your space. You have to 17 | specify the space ID and a CDA access token. Optionally, an output directory for the generated 18 | source files can be provided: 19 | 20 | ```bash 21 | $ ./bin/contentful-generator cfexampleapi b4c0n73n7fu1 --output out 22 | ``` 23 | 24 | The output will look like this: 25 | 26 | ```swift 27 | // This is a generated file. 28 | 29 | import Contentful 30 | import CoreLocation 31 | 32 | struct Dog { 33 | let name: String? 34 | let description: String? 35 | let image: Asset? 36 | 37 | static func fromEntry(entry: Entry) throws -> Dog { 38 | return Dog( 39 | name: entry.fields["name"] as? String, 40 | description: entry.fields["description"] as? String, 41 | image: entry.fields["image"] as? Asset) 42 | } 43 | } 44 | ``` 45 | 46 | For mapping from an `Entry` object received via the SDK to this struct, you can use the 47 | `fromEntry()` function: 48 | 49 | ```swift 50 | client.fetchEntry("doge").1.next { let doge = Doge.fromEntry($0) } 51 | ``` 52 | 53 | ## Installation 54 | 55 | The script requires [`cato`][4] for assembling its dependencies, please install it using: 56 | 57 | ```bash 58 | $ gem install cocoapods cocoapods-rome 59 | $ brew tap neonichu/formulae 60 | $ brew install cato 61 | ``` 62 | 63 | The script can be invoked from anywhere, but it will load files from the `templates` directory 64 | at runtime, so you will need to keep the full Git checkout around. 65 | 66 | ## Limitations 67 | 68 | - Currently, `Date` and `Location` values are unsupported. You will receive warnings for each unsupported field that the generator encounters. 69 | - All properties are declared optional at the moment. 70 | - The script only runs on OS X. 71 | 72 | ## License 73 | 74 | Copyright (c) 2015 Contentful GmbH. See LICENSE for further details. 75 | 76 | [1]: https://www.contentful.com 77 | [2]: http://cocoapods.org 78 | [3]: https://github.com/contentful/contentful.swift 79 | [4]: https://github.com/neonichu/cato 80 | -------------------------------------------------------------------------------- /bin/contentful-generator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env cato 2.1 2 | 3 | import AppKit 4 | import Chores 5 | import Commander 6 | import Contentful 7 | import CoreLocation 8 | import PathKit 9 | import Stencil 10 | 11 | // TODO: Query validations using CMA 12 | 13 | extension FieldType { 14 | func toSwiftType() -> String? { 15 | switch self { 16 | case .Asset: 17 | return "\(Asset.self)" 18 | case .Boolean: 19 | return "\(Bool.self)" 20 | case .Date: 21 | return "\(NSDate.self)" 22 | case .Entry: 23 | return "\(Entry.self)" 24 | case .Integer: 25 | return "\(Int.self)" 26 | case .Location: 27 | return "\(CLLocationCoordinate2D.self)" 28 | case .Number: 29 | return "\(Float.self)" 30 | case .Symbol, .Text: 31 | return "\(String.self)" 32 | default: 33 | return nil 34 | } 35 | } 36 | } 37 | 38 | extension Field { 39 | var fieldError: String { return "Unhandled field `\(identifier)` of" } 40 | var itemTypeError: String { return "\(fieldError) item type `\(itemType)`" } 41 | 42 | func toSwiftType() -> String { 43 | switch type { 44 | case .Array: 45 | if let itemType = itemType.toSwiftType() { 46 | return "[\(itemType)]" 47 | } 48 | 49 | fatalError(itemTypeError) 50 | case .Link: 51 | if let swiftType = itemType.toSwiftType() { 52 | return swiftType 53 | } 54 | 55 | fatalError(itemTypeError) 56 | default: 57 | if let swiftType = type.toSwiftType() { 58 | return swiftType 59 | } 60 | } 61 | 62 | fatalError("\(fieldError) type `\(type)`") 63 | } 64 | } 65 | 66 | extension Process { 67 | static var directory: Path { 68 | if let executablePath = Process.arguments.first { 69 | return Path(executablePath).parent().absolute() 70 | } 71 | 72 | return Path.current 73 | } 74 | } 75 | 76 | private func sanitize(var string: String) -> String { 77 | string = string.stringByReplacingOccurrencesOfString("-", withString: "_") 78 | return string.stringByReplacingOccurrencesOfString(" ", withString: "_") 79 | } 80 | 81 | func generateModelStructs(spaceId: String, _ token: String, output: String) { 82 | let client = ContentfulClient(spaceIdentifier: spaceId, accessToken: token) 83 | client.fetchContentTypes { (result) in 84 | switch result { 85 | case let .Success(types): 86 | for item in types.items { 87 | var fields = [[String:String]]() 88 | 89 | for field in item.fields { 90 | if field.type == .Date || field.type == .Location { 91 | print("\(field.type) values are not yet supported (\(item.name).\(field.identifier)).") 92 | continue 93 | } 94 | 95 | fields.append([ 96 | "name": field.identifier, 97 | "type": field.toSwiftType() 98 | ]) 99 | } 100 | 101 | do { 102 | try render(sanitize(item.name), fields, output: output) 103 | } catch let error { 104 | print("Could not generate template for \(item.name): \(error)") 105 | } 106 | } 107 | case let .Error(error): 108 | print(error) 109 | } 110 | 111 | exit(0) 112 | } 113 | } 114 | 115 | func replaceInFile(sedExpression: String, _ destinationFile: Path) { 116 | // FIXME: Shouldn't shell out to `sed` here 117 | let result = >["sed", "-i", "", sedExpression, destinationFile.description] 118 | if result.stdout.characters.count > 0 || result.stderr.characters.count > 0 { 119 | print("\(result.stdout) \(result.stderr)") 120 | } 121 | } 122 | 123 | func render(name: String, _ fields: [[String:String]], output: String) throws { 124 | let template = try Template(path: Process.directory + "../templates/entry.swift") 125 | let context = Context(dictionary: [ 126 | "className": name, 127 | "fields": fields 128 | ]) 129 | 130 | let destinationFile = Path(output) + Path("\(name).swift") 131 | try destinationFile.write(try template.render(context)) 132 | replaceInFile("s/,)/)/", destinationFile) 133 | replaceInFile("s/: \\(.*\\) as? \\[\\(.*\\)\\]/: (\\1 as? NSArray)?.map { $0 as? \\2 }.flatMap { $0 }/", destinationFile) 134 | } 135 | 136 | command(Argument("Space ID"), 137 | Argument("Access Token"), 138 | Option("output", ".", description: "Output directory for files.")) { (spaceId, token, out) in 139 | NSApplicationLoad() 140 | generateModelStructs(spaceId, token, output: out) 141 | NSApp.run() 142 | }.run() 143 | --------------------------------------------------------------------------------