├── .gitignore ├── .travis.yml ├── Example ├── testModel.json └── testSchema.json ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── ReverseJson.xcodeproj ├── Configs │ └── Project.xcconfig ├── CoreJSONConvenience_Info.plist ├── CoreJSONFoundation_Info.plist ├── CoreJSONLiterals_Info.plist ├── CoreJSONPointer_Info.plist ├── CoreJSONSubscript_Info.plist ├── CoreJSON_Info.plist ├── JSONTests_Info.plist ├── JSON_Info.plist ├── ReverseJsonCommandLineTests_Info.plist ├── ReverseJsonCommandLine_Info.plist ├── ReverseJsonCoreTests_Info.plist ├── ReverseJsonCore_Info.plist ├── ReverseJsonFoundationTests_Info.plist ├── ReverseJsonFoundation_Info.plist ├── ReverseJsonLibTests_Info.plist ├── ReverseJsonLib_Info.plist ├── ReverseJsonModelExportTests_Info.plist ├── ReverseJsonModelExport_Info.plist ├── ReverseJsonObjcTests_Info.plist ├── ReverseJsonObjc_Info.plist ├── ReverseJsonSchema_Info.plist ├── ReverseJsonSwagger_Info.plist ├── ReverseJsonSwiftTests_Info.plist ├── ReverseJsonSwift_Info.plist ├── ReverseJsonTests_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ ├── ReverseJson args.xcscheme │ ├── ReverseJson-Package.xcscheme │ ├── ReverseJson.xcscheme │ └── xcschememanagement.plist ├── Sources ├── ReverseJson │ └── main.swift ├── ReverseJsonCommandLine │ ├── CommandLineArgumentsConvertible.swift │ └── ReverseJson.swift ├── ReverseJsonCore │ ├── ModelGenerator.swift │ ├── ModelTranslator.swift │ └── Utils.swift ├── ReverseJsonModelExport │ └── ModelExportTranslator.swift ├── ReverseJsonObjc │ └── ObjcModelTranslator.swift ├── ReverseJsonSwagger │ └── SwaggerReader.swift └── ReverseJsonSwift │ └── SwiftModelTranslator.swift ├── Tests ├── LinuxMain.swift ├── ReverseJsonCommandLineTests │ ├── Inputs │ │ ├── invalid.json │ │ └── valid.json │ ├── ModelGeneratorCommandLineTest.swift │ ├── ObjcModelCreatorCommandLineTest.swift │ ├── ReverseJsonTest.swift │ ├── SwiftTranslatorCommandLineTest.swift │ └── XCTestManifests.swift ├── ReverseJsonCoreTests │ ├── ModelGeneratorTest.swift │ └── XCTestManifests.swift ├── ReverseJsonModelExportTests │ ├── JsonToModelTest.swift │ ├── ModelExportTranslatorTest.swift │ ├── ModelToJsonTest.swift │ └── XCTestManifests.swift ├── ReverseJsonObjcTests │ ├── ObjcModelTranslatorTest.swift │ └── XCTestManifests.swift └── ReverseJsonSwiftTests │ ├── SwiftTranslatorTest.swift │ └── XCTestManifests.swift ├── codecov.yml └── test /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode11 2 | language: generic 3 | matrix: 4 | include: 5 | - os: osx 6 | env: TYPE=xcode 7 | language: objective-c 8 | after_success: bash <(curl -s https://codecov.io/bash) 9 | - os: osx 10 | env: 11 | - TYPE=spm 12 | - SWIFT_VERSION=5.1 13 | - os: linux 14 | dist: trusty 15 | sudo: required 16 | env: 17 | - TYPE=spm 18 | - SWIFT_VERSION=5.1 19 | install: if [[ "$TYPE" == "spm" ]]; then eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/02090c7ede5a637b76e6df1710e83cd0bbe7dcdf/swiftenv-install.sh)"; fi 20 | script: ./test $TYPE 21 | -------------------------------------------------------------------------------- /Example/testModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "height" : 10, 3 | "private" : false, 4 | "mixed" : [ 5 | 10, 6 | true, 7 | "hallo", 8 | { 9 | "field" : "value" 10 | }, 11 | { 12 | "signed" : 10 13 | }, 14 | 1.2 15 | ], 16 | "numbers" : [ 17 | 10, 18 | 11, 19 | 12 20 | ], 21 | "let" : false, 22 | "locations" : [ 23 | { 24 | "lat" : 10.5, 25 | "lon" : null, 26 | "enum test" : "bla" 27 | }, 28 | { 29 | "lon" : "abc", 30 | "enum test" : { 31 | 32 | }, 33 | "lat" : 10.5, 34 | "bla" : [ 35 | [ 36 | [ 37 | [ 38 | { 39 | "test" : "a" 40 | } 41 | ] 42 | ] 43 | ] 44 | ] 45 | }, 46 | { 47 | "lon" : 49.1, 48 | "enum test" : "bla", 49 | "lat" : 10, 50 | "bla" : [ 51 | [ 52 | [ 53 | [ 54 | { 55 | "test" : 123 56 | } 57 | ] 58 | ] 59 | ] 60 | ] 61 | } 62 | ], 63 | "is_true" : true, 64 | "internal" : false, 65 | "public" : false, 66 | "name" : null, 67 | "blup" : [ 68 | { 69 | "obj1" : 10 70 | }, 71 | { 72 | "obj1" : "test" 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /Example/testSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "$schema": "https://github.com/tomquist/ReverseJson/tree/1.2.0", 4 | "properties": { 5 | "locations": { 6 | "type": "list", 7 | "content": { 8 | "type": "object", 9 | "properties": { 10 | "lon": { 11 | "type": "any", 12 | "isOptional": true, 13 | "of": [ 14 | "double", 15 | "text" 16 | ] 17 | }, 18 | "lat": "double", 19 | "bla": { 20 | "type": "list", 21 | "isOptional": true, 22 | "content": { 23 | "type": "list", 24 | "content": { 25 | "type": "list", 26 | "content": { 27 | "type": "list", 28 | "content": { 29 | "type": "object", 30 | "properties": { 31 | "test": { 32 | "type": "any", 33 | "of": [ 34 | "int", 35 | "text" 36 | ] 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | }, 44 | "enum test": { 45 | "type": "any", 46 | "of": [ 47 | "object", 48 | "text" 49 | ] 50 | } 51 | } 52 | } 53 | }, 54 | "height": "int", 55 | "numbers": { 56 | "type": "list", 57 | "content": "int" 58 | }, 59 | "name": "any?", 60 | "blup": { 61 | "type": "list", 62 | "content": { 63 | "type": "object", 64 | "properties": { 65 | "obj1": { 66 | "type": "any", 67 | "of": [ 68 | "int", 69 | "text" 70 | ] 71 | } 72 | } 73 | } 74 | }, 75 | "let": "bool", 76 | "internal": "bool", 77 | "private": "bool", 78 | "is_true": "bool", 79 | "public": "bool", 80 | "mixed": { 81 | "type": "list", 82 | "content": { 83 | "type": "any", 84 | "of": [ 85 | "bool", 86 | "double", 87 | { 88 | "type": "object", 89 | "properties": { 90 | "signed": "int?", 91 | "field": "text?" 92 | } 93 | }, 94 | "text" 95 | ] 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CoreJSON", 6 | "repositoryURL": "https://github.com/tomquist/CoreJSON.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "109124ee37960204152f6c7dfea2c4c7b690daf5", 10 | "version": "2.0.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ReverseJson", 7 | products: [ 8 | .executable(name: "ReverseJson", targets: ["ReverseJson"]), 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/tomquist/CoreJSON.git", from: "2.0.0") 12 | ], 13 | targets: [ 14 | .target(name: "ReverseJsonCore", dependencies: ["CoreJSON"]), 15 | .target(name: "ReverseJsonObjc", dependencies: ["ReverseJsonCore"]), 16 | .target(name: "ReverseJsonModelExport", dependencies: ["ReverseJsonCore"]), 17 | .target(name: "ReverseJsonSwift", dependencies: ["ReverseJsonCore"]), 18 | .target(name: "ReverseJsonCommandLine", dependencies: ["ReverseJsonCore", "ReverseJsonObjc", "ReverseJsonModelExport", "ReverseJsonSwift"]), 19 | .target(name: "ReverseJsonSwagger", dependencies: ["ReverseJsonCore"]), 20 | .target(name: "ReverseJson", dependencies: ["ReverseJsonCommandLine"]), 21 | .testTarget(name: "ReverseJsonCoreTests", dependencies: ["ReverseJsonCore"]), 22 | .testTarget(name: "ReverseJsonObjcTests", dependencies: ["ReverseJsonObjc"]), 23 | .testTarget(name: "ReverseJsonModelExportTests", dependencies: ["ReverseJsonModelExport"]), 24 | .testTarget(name: "ReverseJsonSwiftTests", dependencies: ["ReverseJsonSwift"]), 25 | .testTarget(name: "ReverseJsonCommandLineTests", dependencies: ["ReverseJsonCommandLine"]), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/tomquist/ReverseJson.svg)](https://travis-ci.org/tomquist/ReverseJson) 2 | [![codecov.io](https://codecov.io/github/tomquist/ReverseJson/coverage.svg)](https://codecov.io/github/tomquist/ReverseJson) 3 | 4 | # ReverseJson 5 | 6 | ## Introduction 7 | Generate data model code and JSON-parser code from JSON-files. Currently you can generate Swift and Objective-C code. 8 | 9 | ## Features 10 | - Scans the whole JSON-file to get most information out of it 11 | - Detects variadic types within arrays, even in sub structures 12 | - Detects nullability attributes, e.g. when a single occurrence of a property is null or a property is missing, the property is declared as Optional/Nullable 13 | - Generates parsing instructions for Swift and Objective-C 14 | - Converts any JSON data-structure into a simple schema which then can be modified/adjusted and be used to generate Swift/Objective-C code 15 | 16 | ## Usage 17 | 18 | ### Prerequisites 19 | 20 | * Swift 5.0 21 | * Any Swift 5.0 compatible platform (e.g. macOS or Linux) 22 | 23 | ### Build 24 | 25 | swift build --configuration release 26 | 27 | By default, you'll find the executable in ```.build/release/ReverseJson``` 28 | 29 | ### Test 30 | 31 | swift test 32 | 33 | ### General usage: 34 | 35 | ``` 36 | Usage: ReverseJson (swift|objc|export) NAME FILE 37 | e.g. ReverseJson swift User testModel.json 38 | Options: 39 | -v, --verbose Print result instead of creating files 40 | -o, --out Output directory (default is current directory) 41 | -c, --class (Swift) Use classes instead of structs for objects 42 | -ca, --contiguousarray (Swift) Use ContiguousArray for lists 43 | -pt, --publictypes (Swift) Make type declarations public instead of internal 44 | -pf, --publicfields (Swift) Make field declarations public instead of internal 45 | -n, --nullable (Swift and Objective-C) Make all field declarations optional (nullable in Objective-C) 46 | -m, --mutable (Swift and Objective-C) All object fields are mutable (var instead of 47 | let in Swift and 'readwrite' instead of 'readonly' in Objective-C) 48 | -a, --atomic (Objective-C) Make properties 'atomic' 49 | -p, --prefix (Objective-C) Class-prefix to use for type declarations 50 | -r, --reversemapping (Objective-C) Create method for reverse mapping (toJson) 51 | ``` 52 | 53 | ### To create a Swift data model: 54 | 55 | ./ReverseJson swift User testModel.json 56 | 57 | ### To create an Objective-C data model: 58 | 59 | ./ReverseJson objc User testModel.json 60 | 61 | ### To export a ReverseJson schema: 62 | 63 | ./ReverseJson export User testModel.json 64 | 65 | ## Demo 66 | Turns this: 67 | 68 | ```json 69 | { 70 | "name": "Tom", 71 | "is_private": false, 72 | "mixed": [10, "Some text"], 73 | "numbers": [10, 11, 12], 74 | "locations": [ 75 | {"lat": 10.5, "lon": 49}, 76 | { 77 | "lat": -74.1184284, 78 | "lon": 40.7055647, 79 | "address": { 80 | "street": "Some Street", 81 | "city": "New York" 82 | } 83 | } 84 | ], 85 | "internal": false 86 | } 87 | ``` 88 | 89 | ...into this: 90 | 91 | ```swift 92 | // JsonModel.swift 93 | struct User { 94 | enum MixedItem { 95 | case number(Int) 96 | case text(String) 97 | } 98 | struct LocationsItem { 99 | struct Address { 100 | let city: String 101 | let street: String 102 | } 103 | let address: Address? 104 | let lat: Double 105 | let lon: Double 106 | } 107 | let `internal`: Bool 108 | let isPrivate: Bool 109 | let locations: [LocationsItem] 110 | let mixed: [MixedItem] 111 | let name: String 112 | let numbers: [Int] 113 | } 114 | // JsonModelMapping.swift 115 | enum JsonParsingError: Error { 116 | case unsupportedTypeError 117 | } 118 | 119 | extension Array { 120 | init(jsonValue: Any?, map: (Any) throws -> Element) throws { 121 | guard let items = jsonValue as? [Any] else { 122 | throw JsonParsingError.unsupportedTypeError 123 | } 124 | self = try items.map(map) 125 | } 126 | } 127 | 128 | extension Bool { 129 | init(jsonValue: Any?) throws { 130 | if let number = jsonValue as? NSNumber { 131 | guard String(cString: number.objCType) == String(cString: NSNumber(value: true).objCType) else { 132 | throw JsonParsingError.unsupportedTypeError 133 | } 134 | self = number.boolValue 135 | } else if let number = jsonValue as? Bool { 136 | self = number 137 | } else { 138 | throw JsonParsingError.unsupportedTypeError 139 | } 140 | } 141 | } 142 | 143 | extension Double { 144 | init(jsonValue: Any?) throws { 145 | if let number = jsonValue as? NSNumber { 146 | self = number.doubleValue 147 | } else if let number = jsonValue as? Int { 148 | self = Double(number) 149 | } else if let number = jsonValue as? Double { 150 | self = number 151 | } else if let number = jsonValue as? Float { 152 | self = Double(number) 153 | } else { 154 | throw JsonParsingError.unsupportedTypeError 155 | } 156 | } 157 | } 158 | 159 | extension Int { 160 | init(jsonValue: Any?) throws { 161 | if let number = jsonValue as? NSNumber { 162 | self = number.intValue 163 | } else if let number = jsonValue as? Int { 164 | self = number 165 | } else if let number = jsonValue as? Double { 166 | self = Int(number) 167 | } else if let number = jsonValue as? Float { 168 | self = Int(number) 169 | } else { 170 | throw JsonParsingError.unsupportedTypeError 171 | } 172 | } 173 | } 174 | 175 | extension Optional { 176 | init(jsonValue: Any?, map: (Any) throws -> Wrapped) throws { 177 | if let jsonValue = jsonValue, !(jsonValue is NSNull) { 178 | self = try map(jsonValue) 179 | } else { 180 | self = nil 181 | } 182 | } 183 | } 184 | 185 | extension String { 186 | init(jsonValue: Any?) throws { 187 | guard let string = jsonValue as? String else { 188 | throw JsonParsingError.unsupportedTypeError 189 | } 190 | self = string 191 | } 192 | } 193 | 194 | extension User { 195 | init(jsonValue: Any?) throws { 196 | guard let dict = jsonValue as? [String: Any] else { 197 | throw JsonParsingError.unsupportedTypeError 198 | } 199 | self.mixed = try Array(jsonValue: dict["mixed"]) { try User.MixedItem(jsonValue: $0) } 200 | self.numbers = try Array(jsonValue: dict["numbers"]) { try Int(jsonValue: $0) } 201 | self.`internal` = try Bool(jsonValue: dict["internal"]) 202 | self.isPrivate = try Bool(jsonValue: dict["is_private"]) 203 | self.locations = try Array(jsonValue: dict["locations"]) { try User.LocationsItem(jsonValue: $0) } 204 | self.name = try String(jsonValue: dict["name"]) 205 | } 206 | } 207 | 208 | extension User.LocationsItem { 209 | init(jsonValue: Any?) throws { 210 | guard let dict = jsonValue as? [String: Any] else { 211 | throw JsonParsingError.unsupportedTypeError 212 | } 213 | self.lat = try Double(jsonValue: dict["lat"]) 214 | self.lon = try Double(jsonValue: dict["lon"]) 215 | self.address = try Optional(jsonValue: dict["address"]) { try User.LocationsItem.Address(jsonValue: $0) } 216 | } 217 | } 218 | 219 | extension User.LocationsItem.Address { 220 | init(jsonValue: Any?) throws { 221 | guard let dict = jsonValue as? [String: Any] else { 222 | throw JsonParsingError.unsupportedTypeError 223 | } 224 | self.street = try String(jsonValue: dict["street"]) 225 | self.city = try String(jsonValue: dict["city"]) 226 | } 227 | } 228 | 229 | extension User.MixedItem { 230 | init(jsonValue: Any?) throws { 231 | if let value = try? Int(jsonValue: jsonValue) { 232 | self = .number(value) 233 | } else if let value = try? String(jsonValue: jsonValue) { 234 | self = .text(value) 235 | } else { 236 | throw JsonParsingError.unsupportedTypeError 237 | } 238 | } 239 | } 240 | 241 | func parseUser(jsonValue: Any?) throws -> User { 242 | return try User(jsonValue: jsonValue) 243 | } 244 | ``` 245 | 246 | ...or into this... 247 | 248 | ```objective-c 249 | // User.h 250 | #import 251 | @class UserLocationsItem, UserMixedItem; 252 | NS_ASSUME_NONNULL_BEGIN 253 | @interface User : NSObject 254 | - (instancetype)initWithJsonDictionary:(NSDictionary> *)dictionary; 255 | - (nullable instancetype)initWithJsonValue:(nullable id)jsonValue; 256 | - (NSDictionary> *)toJson; 257 | @property (nonatomic, assign, readonly) BOOL internal; 258 | @property (nonatomic, assign, readonly) BOOL isPrivate; 259 | @property (nonatomic, strong, readonly) NSArray *numbers; 260 | @property (nonatomic, strong, readonly) NSArray *locations; 261 | @property (nonatomic, strong, readonly) NSArray *mixed; 262 | @property (nonatomic, copy, readonly) NSString *name; 263 | @end 264 | NS_ASSUME_NONNULL_END 265 | // UserLocationsItem.h 266 | #import 267 | @class UserLocationsItemAddress; 268 | NS_ASSUME_NONNULL_BEGIN 269 | @interface UserLocationsItem : NSObject 270 | - (instancetype)initWithJsonDictionary:(NSDictionary> *)dictionary; 271 | - (nullable instancetype)initWithJsonValue:(nullable id)jsonValue; 272 | - (NSDictionary> *)toJson; 273 | @property (nonatomic, strong, readonly, nullable) UserLocationsItemAddress *address; 274 | @property (nonatomic, assign, readonly) double lat; 275 | @property (nonatomic, assign, readonly) double lon; 276 | @end 277 | NS_ASSUME_NONNULL_END 278 | // UserLocationsItemAddress.h 279 | #import 280 | NS_ASSUME_NONNULL_BEGIN 281 | @interface UserLocationsItemAddress : NSObject 282 | - (instancetype)initWithJsonDictionary:(NSDictionary> *)dictionary; 283 | - (nullable instancetype)initWithJsonValue:(nullable id)jsonValue; 284 | - (NSDictionary> *)toJson; 285 | @property (nonatomic, copy, readonly) NSString *city; 286 | @property (nonatomic, copy, readonly) NSString *street; 287 | @end 288 | NS_ASSUME_NONNULL_END 289 | // UserMixedItem.h 290 | #import 291 | NS_ASSUME_NONNULL_BEGIN 292 | @interface UserMixedItem : NSObject 293 | - (instancetype)initWithJsonValue:(nullable id)jsonValue; 294 | - (id)toJson; 295 | @property (nonatomic, copy, readonly, nullable) NSNumber/*NSInteger*/ *number; 296 | @property (nonatomic, strong, readonly, nullable) NSString *text; 297 | @end 298 | NS_ASSUME_NONNULL_END 299 | // User.m 300 | #import "User.h" 301 | #import "UserLocationsItem.h" 302 | #import "UserMixedItem.h" 303 | @implementation User 304 | - (instancetype)initWithJsonDictionary:(NSDictionary *)dict { 305 | self = [super init]; 306 | if (self) { 307 | _internal = [dict[@"internal"] isKindOfClass:[NSNumber class]] ? [dict[@"internal"] boolValue] : 0; 308 | _isPrivate = [dict[@"is_private"] isKindOfClass:[NSNumber class]] ? [dict[@"is_private"] boolValue] : 0; 309 | _numbers = ({ 310 | id value = dict[@"numbers"]; 311 | NSMutableArray *values = nil; 312 | if ([value isKindOfClass:[NSArray class]]) { 313 | NSArray *array = value; 314 | values = [NSMutableArray arrayWithCapacity:array.count]; 315 | for (id item in array) { 316 | NSNumber/*NSInteger*/ *parsedItem = [item isKindOfClass:[NSNumber class]] ? item : nil; 317 | [values addObject:parsedItem ?: (id)[NSNull null]]; 318 | } 319 | } 320 | [values copy] ?: @[]; 321 | }); 322 | _locations = ({ 323 | id value = dict[@"locations"]; 324 | NSMutableArray *values = nil; 325 | if ([value isKindOfClass:[NSArray class]]) { 326 | NSArray *array = value; 327 | values = [NSMutableArray arrayWithCapacity:array.count]; 328 | for (id item in array) { 329 | UserLocationsItem *parsedItem = ([[UserLocationsItem alloc] initWithJsonValue:item] ?: [[UserLocationsItem alloc] initWithJsonDictionary:@{}]); 330 | [values addObject:parsedItem ?: (id)[NSNull null]]; 331 | } 332 | } 333 | [values copy] ?: @[]; 334 | }); 335 | _mixed = ({ 336 | id value = dict[@"mixed"]; 337 | NSMutableArray *values = nil; 338 | if ([value isKindOfClass:[NSArray class]]) { 339 | NSArray *array = value; 340 | values = [NSMutableArray arrayWithCapacity:array.count]; 341 | for (id item in array) { 342 | UserMixedItem *parsedItem = [[UserMixedItem alloc] initWithJsonValue:item]; 343 | [values addObject:parsedItem ?: (id)[NSNull null]]; 344 | } 345 | } 346 | [values copy] ?: @[]; 347 | }); 348 | _name = [dict[@"name"] isKindOfClass:[NSString class]] ? dict[@"name"] : @""; 349 | } 350 | return self; 351 | } 352 | - (instancetype)initWithJsonValue:(id)jsonValue { 353 | if ([jsonValue isKindOfClass:[NSDictionary class]]) { 354 | self = [self initWithJsonDictionary:jsonValue]; 355 | } else { 356 | self = nil; 357 | } 358 | return self; 359 | } 360 | - (NSDictionary> *)toJson { 361 | NSMutableDictionary *ret = [NSMutableDictionary dictionaryWithCapacity:6]; 362 | ret[@"internal"] = @(_internal); 363 | ret[@"is_private"] = @(_isPrivate); 364 | ret[@"numbers"] = ({ 365 | NSMutableArray> *values = nil; 366 | NSArray *array = _numbers; 367 | if (array) { 368 | values = [NSMutableArray arrayWithCapacity:array.count]; 369 | for (id item in array) { 370 | if (item == [NSNull null]) { 371 | [values addObject:item]; 372 | } else { 373 | id json = item; 374 | [values addObject:json ?: (id)[NSNull null]]; 375 | } 376 | } 377 | } 378 | [values copy] ?: @[]; 379 | }); 380 | ret[@"locations"] = ({ 381 | NSMutableArray> *values = nil; 382 | NSArray *array = _locations; 383 | if (array) { 384 | values = [NSMutableArray arrayWithCapacity:array.count]; 385 | for (id item in array) { 386 | if (item == [NSNull null]) { 387 | [values addObject:item]; 388 | } else { 389 | id json = [item toJson]; 390 | [values addObject:json ?: (id)[NSNull null]]; 391 | } 392 | } 393 | } 394 | [values copy] ?: @[]; 395 | }); 396 | ret[@"mixed"] = ({ 397 | NSMutableArray> *values = nil; 398 | NSArray *array = _mixed; 399 | if (array) { 400 | values = [NSMutableArray arrayWithCapacity:array.count]; 401 | for (id item in array) { 402 | if (item == [NSNull null]) { 403 | [values addObject:item]; 404 | } else { 405 | id json = [item toJson]; 406 | [values addObject:json ?: (id)[NSNull null]]; 407 | } 408 | } 409 | } 410 | [values copy] ?: @[]; 411 | }); 412 | ret[@"name"] = _name; 413 | return [ret copy]; 414 | } 415 | @end 416 | // UserLocationsItem.m 417 | #import "UserLocationsItem.h" 418 | #import "UserLocationsItemAddress.h" 419 | @implementation UserLocationsItem 420 | - (instancetype)initWithJsonDictionary:(NSDictionary *)dict { 421 | self = [super init]; 422 | if (self) { 423 | _address = [[UserLocationsItemAddress alloc] initWithJsonValue:dict[@"address"]]; 424 | _lat = [dict[@"lat"] isKindOfClass:[NSNumber class]] ? [dict[@"lat"] doubleValue] : 0; 425 | _lon = [dict[@"lon"] isKindOfClass:[NSNumber class]] ? [dict[@"lon"] doubleValue] : 0; 426 | } 427 | return self; 428 | } 429 | - (instancetype)initWithJsonValue:(id)jsonValue { 430 | if ([jsonValue isKindOfClass:[NSDictionary class]]) { 431 | self = [self initWithJsonDictionary:jsonValue]; 432 | } else { 433 | self = nil; 434 | } 435 | return self; 436 | } 437 | - (NSDictionary> *)toJson { 438 | NSMutableDictionary *ret = [NSMutableDictionary dictionaryWithCapacity:3]; 439 | ret[@"address"] = [_address toJson]; 440 | ret[@"lat"] = @(_lat); 441 | ret[@"lon"] = @(_lon); 442 | return [ret copy]; 443 | } 444 | @end 445 | // UserLocationsItemAddress.m 446 | #import "UserLocationsItemAddress.h" 447 | @implementation UserLocationsItemAddress 448 | - (instancetype)initWithJsonDictionary:(NSDictionary *)dict { 449 | self = [super init]; 450 | if (self) { 451 | _city = [dict[@"city"] isKindOfClass:[NSString class]] ? dict[@"city"] : @""; 452 | _street = [dict[@"street"] isKindOfClass:[NSString class]] ? dict[@"street"] : @""; 453 | } 454 | return self; 455 | } 456 | - (instancetype)initWithJsonValue:(id)jsonValue { 457 | if ([jsonValue isKindOfClass:[NSDictionary class]]) { 458 | self = [self initWithJsonDictionary:jsonValue]; 459 | } else { 460 | self = nil; 461 | } 462 | return self; 463 | } 464 | - (NSDictionary> *)toJson { 465 | NSMutableDictionary *ret = [NSMutableDictionary dictionaryWithCapacity:2]; 466 | ret[@"city"] = _city; 467 | ret[@"street"] = _street; 468 | return [ret copy]; 469 | } 470 | @end 471 | // UserMixedItem.m 472 | #import "UserMixedItem.h" 473 | @implementation UserMixedItem 474 | - (instancetype)initWithJsonValue:(id)jsonValue { 475 | self = [super init]; 476 | if (self) { 477 | _number = [jsonValue isKindOfClass:[NSNumber class]] ? jsonValue : nil; 478 | _text = [jsonValue isKindOfClass:[NSString class]] ? jsonValue : nil; 479 | } 480 | return self; 481 | } 482 | - (id)toJson { 483 | if (_number) { 484 | return _number; 485 | } else if (_text) { 486 | return _text; 487 | } 488 | return nil; 489 | } 490 | @end 491 | ``` 492 | 493 | ## ReverseJson Schema 494 | Sometimes the inferred model requires some small adjustments, e.g. a field which has been inferred as non-optional should be optional. 495 | Therefore ReverseJson allows to export a simple schema which then can be adjusted and be used to generate Swift or Objective-C code: 496 | 497 | ### Export ReverseJson schema 498 | 499 | ./ReverseJson export User testModel.json 500 | 501 | The example from above produces the following schema: 502 | ```json 503 | { 504 | "type" : "object", 505 | "$schema" : "https:\/\/github.com\/tomquist\/ReverseJson\/tree\/1.2.0", 506 | "properties" : { 507 | "mixed" : { 508 | "type" : "list", 509 | "content" : { 510 | "type" : "any", 511 | "of" : [ 512 | "int", 513 | "text" 514 | ] 515 | } 516 | }, 517 | "numbers" : { 518 | "type" : "list", 519 | "content" : "int" 520 | }, 521 | "name" : "text", 522 | "internal" : "bool", 523 | "locations" : { 524 | "type" : "list", 525 | "content" : { 526 | "type" : "object", 527 | "properties" : { 528 | "lat" : "double", 529 | "lon" : "double", 530 | "address" : { 531 | "type" : "object", 532 | "isOptional" : true, 533 | "properties" : { 534 | "street" : "text", 535 | "city" : "text" 536 | } 537 | } 538 | } 539 | } 540 | }, 541 | "is_private" : "bool" 542 | } 543 | } 544 | ``` 545 | 546 | ### Convert ReverseJson schema into Swift 547 | 548 | ./ReverseJson swift User mySchema.json 549 | 550 | ### General schema structure 551 | A schema is a simple type description which always has the following structure: 552 | ```json 553 | { 554 | "type": "", 555 | "isOptional": true 556 | } 557 | ``` 558 | If there is only a "type" property, it is also possible to simply use the type identifier, e.g. the following expressions 559 | ```json 560 | "string" 561 | ``` 562 | is identical to 563 | ```json 564 | { 565 | "type": "string" 566 | } 567 | ``` 568 | 569 | If the property "isOptional" is missing, the type is assumed to be non-optional. Instead of having a property "isOptional", it is also possible to just add a "?" as a suffix to the type name. E.g. this expression 570 | ```json 571 | "string?" 572 | ``` 573 | is identical to 574 | ```json 575 | { 576 | "type": "string", 577 | "isOptional": true 578 | } 579 | ``` 580 | 581 | ### Objects 582 | Objects have an additional property "properties" which is a JSON-object containing all properties, where the key is the property name and the value is a schema. Optionally you can add the "name" property to override the auto-generated name when converting the model to code. E.g. 583 | ```json 584 | { 585 | "type": "object", 586 | "name": "MyObject", 587 | "properties": { 588 | "property1": { 589 | "type": "string" 590 | }, 591 | "property2": "int?" 592 | } 593 | } 594 | ``` 595 | 596 | ### Lists 597 | Lists describe their content-type using the "content" property. E.g. 598 | ```json 599 | { 600 | "type": "list", 601 | "content": { 602 | "type": "object", 603 | "properties": { 604 | "id": "int", 605 | "name": "string", 606 | "description": "string?" 607 | } 608 | } 609 | } 610 | ``` 611 | 612 | ### Any 613 | The type "any" allows to support multiple value types at the same place. To describe which types are allowed, use the "of" property. Optionally you can add the "name" property to override the auto-generated name when converting the model to code. E.g. 614 | ```json 615 | { 616 | "type": "any", 617 | "name": "MyObject", 618 | "of": [ 619 | { 620 | "type": "object", 621 | "properties": { 622 | "id": "text", 623 | "name": "text", 624 | "description": "text?" 625 | } 626 | }, 627 | "int" 628 | ] 629 | } 630 | ``` 631 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/Configs/Project.xcconfig: -------------------------------------------------------------------------------- 1 | PRODUCT_NAME = $(TARGET_NAME) 2 | SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator 3 | MACOSX_DEPLOYMENT_TARGET = 10.10 4 | DYLIB_INSTALL_NAME_BASE = @rpath 5 | OTHER_SWIFT_FLAGS = -DXcode 6 | COMBINE_HIDPI_IMAGES = YES 7 | USE_HEADERMAP = NO 8 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/CoreJSONConvenience_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/CoreJSONFoundation_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/CoreJSONLiterals_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/CoreJSONPointer_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/CoreJSONSubscript_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/CoreJSON_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/JSONTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/JSON_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonCommandLineTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonCommandLine_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonCoreTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonCore_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonFoundationTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonFoundation_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonLibTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonLib_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonModelExportTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonModelExport_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonObjcTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonObjc_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonSchema_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonSwagger_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonSwiftTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonSwift_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/ReverseJsonTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/xcshareddata/xcschemes/ReverseJson args.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 71 | 77 | 78 | 79 | 85 | 91 | 92 | 93 | 99 | 105 | 106 | 107 | 113 | 119 | 120 | 121 | 127 | 133 | 134 | 135 | 141 | 147 | 148 | 149 | 155 | 161 | 162 | 163 | 169 | 175 | 176 | 177 | 183 | 189 | 190 | 191 | 192 | 193 | 199 | 200 | 202 | 208 | 209 | 210 | 212 | 218 | 219 | 220 | 222 | 228 | 229 | 230 | 232 | 238 | 239 | 240 | 242 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 264 | 266 | 272 | 273 | 274 | 275 | 278 | 279 | 282 | 283 | 286 | 287 | 290 | 291 | 294 | 295 | 298 | 299 | 302 | 303 | 306 | 307 | 308 | 309 | 310 | 311 | 317 | 318 | 320 | 321 | 324 | 325 | 326 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/xcshareddata/xcschemes/ReverseJson-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | 59 | 60 | 61 | 62 | 63 | 69 | 70 | 72 | 77 | 78 | 79 | 81 | 86 | 87 | 88 | 90 | 95 | 96 | 97 | 99 | 104 | 105 | 106 | 108 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/xcshareddata/xcschemes/ReverseJson.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 24 | 29 | 30 | 31 | 33 | 38 | 39 | 40 | 42 | 47 | 48 | 49 | 51 | 56 | 57 | 58 | 60 | 65 | 66 | 67 | 68 | 69 | 79 | 81 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ReverseJson.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SchemeUserState 5 | 6 | ReverseJson.xcscheme 7 | 8 | 9 | SuppressBuildableAutocreation 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Sources/ReverseJson/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // ReverseJson 4 | // 5 | // Created by Tom Quist on 07.02.16. 6 | // Copyright © 2016 Tom Quist. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReverseJsonCommandLine 11 | 12 | do { 13 | let reverseJson = try ReverseJson(args: CommandLine.arguments) 14 | print(try reverseJson.main()) 15 | exit(0) 16 | } catch let error as ReverseJsonError { 17 | print(error) 18 | print(ReverseJson.usage()) 19 | exit(1) 20 | } catch { 21 | print(error) 22 | exit(1) 23 | } 24 | -------------------------------------------------------------------------------- /Sources/ReverseJsonCommandLine/CommandLineArgumentsConvertible.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReverseJsonCore 3 | import ReverseJsonSwift 4 | import ReverseJsonObjc 5 | import ReverseJsonModelExport 6 | 7 | public protocol CommandLineArgumentsConvertible { 8 | init(consumableArgs args: inout [String]) throws 9 | init(args: [String]) throws 10 | } 11 | 12 | extension CommandLineArgumentsConvertible { 13 | public init(args: [String]) throws { 14 | var args = args 15 | self = try Self(consumableArgs: &args) 16 | } 17 | } 18 | 19 | extension Array where Element: Equatable { 20 | mutating func consume(flag: Element) -> Bool { 21 | if let idx = firstIndex(of: flag) { 22 | remove(at: idx) 23 | return true 24 | } 25 | return false 26 | } 27 | 28 | mutating func consume(value: Element) -> Element? { 29 | if let idx = firstIndex(of: value) , count > idx + 1 { 30 | remove(at: idx) 31 | return remove(at: idx) 32 | } 33 | return nil 34 | } 35 | } 36 | 37 | extension ReverseJson { 38 | 39 | public init(consumableArgs args: inout [String]) throws { 40 | guard args.count >= 4 else { 41 | throw ReverseJsonError.wrongArgumentCount 42 | } 43 | let language = args[1] 44 | modelName = args[2] 45 | let file = args[3] 46 | args = args.suffix(from: 4).map {$0} 47 | typealias CommandLineArgumentsConvertibleTranslator = (ModelTranslator & CommandLineArgumentsConvertible) 48 | var translatorType: CommandLineArgumentsConvertibleTranslator.Type 49 | switch language { 50 | case "swift": 51 | translatorType = SwiftTranslator.self 52 | case "objc": 53 | translatorType = ObjcModelCreator.self 54 | case "export": 55 | translatorType = ModelExportTranslator.self 56 | default: 57 | throw ReverseJsonError.unsupportedLanguage(language) 58 | } 59 | self.writeToConsole = args.consume(flag: "-v") || args.consume(flag: "--verbose") 60 | self.outputDirectory = args.consume(value: "-o") ?? args.consume(value: "--out") ?? "" 61 | 62 | let data: Data 63 | do { 64 | data = try Data(contentsOf: URL(fileURLWithPath: file)) 65 | } catch { 66 | throw ReverseJsonError.unableToRead(file: file, error) 67 | } 68 | 69 | do { 70 | json = try JSONSerialization.jsonObject(with: data, options: []) 71 | } catch { 72 | throw ReverseJsonError.unableToParseFile(error: error) 73 | } 74 | 75 | modelGenerator = ModelGenerator(consumableArgs: &args) 76 | translator = try translatorType.init(consumableArgs: &args) 77 | } 78 | 79 | } 80 | 81 | extension SwiftTranslator: CommandLineArgumentsConvertible { 82 | public init(consumableArgs args: inout [String]) { 83 | self.init() 84 | objectType = args.consume(flag: "-c") || args.consume(flag: "--class") ? .classType : .structType 85 | listType = args.consume(flag: "-ca") || args.consume(flag: "--contiguousarray") ? .contiguousArray : .array 86 | mutableFields = args.consume(flag: "-m") || args.consume(flag: "--mutable") 87 | fieldVisibility = args.consume(flag: "-pf") || args.consume(flag: "--publicfields") ? .publicVisibility : .internalVisibility 88 | typeVisibility = args.consume(flag: "-pt") || args.consume(flag: "--publictypes") ? .publicVisibility : .internalVisibility 89 | } 90 | } 91 | 92 | extension ObjcModelCreator: CommandLineArgumentsConvertible { 93 | public init(consumableArgs args: inout [String]) { 94 | self.init() 95 | atomic = args.consume(flag: "-a") || args.consume(flag: "--atomic") 96 | readonly = !(args.consume(flag: "-m") || args.contains("--mutable")) 97 | createToJson = args.consume(flag: "-r") || args.contains("--reversemapping") 98 | if let prefix = (args.consume(value: "-p") ?? args.consume(value: "--prefix")) { 99 | typePrefix = prefix 100 | } 101 | } 102 | } 103 | 104 | extension ModelExportTranslator: CommandLineArgumentsConvertible { 105 | public init(consumableArgs args: inout [String]) throws { 106 | self.init() 107 | } 108 | } 109 | 110 | extension ModelGenerator: CommandLineArgumentsConvertible { 111 | public init(consumableArgs args: inout [String]) { 112 | self.init() 113 | allFieldsOptional = args.consume(flag: "-n") || args.consume(flag: "--nullable") 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /Sources/ReverseJsonCommandLine/ReverseJson.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReverseJsonCore 3 | import ReverseJsonSwift 4 | import ReverseJsonObjc 5 | import ReverseJsonModelExport 6 | import CoreJSON 7 | import CoreJSONFoundation 8 | 9 | public enum ReverseJsonError: Error { 10 | case wrongArgumentCount 11 | case outputDirectoryDoesNotExist(String) 12 | case outputPathIsNoDirectory(String) 13 | case unsupportedLanguage(String) 14 | case unableToRead(file: String, Error) 15 | case unableToParseFile(error: Error) 16 | } 17 | 18 | extension Bool { 19 | fileprivate var boolValue: Bool { 20 | return self 21 | } 22 | } 23 | 24 | public struct ReverseJson: CommandLineArgumentsConvertible { 25 | 26 | public var translator: ModelTranslator 27 | public var json: Any 28 | public var modelName: String 29 | public var modelGenerator: ModelGenerator 30 | public var writeToConsole: Bool 31 | public var outputDirectory: String 32 | 33 | public init(json: Any, modelName: String, modelGenerator: ModelGenerator, translator: ModelTranslator, writeToConsole: Bool, outputDirectory: String) { 34 | self.json = json 35 | self.modelName = modelName 36 | self.modelGenerator = modelGenerator 37 | self.translator = translator 38 | self.writeToConsole = writeToConsole 39 | self.outputDirectory = outputDirectory 40 | } 41 | 42 | public static func usage(command: String = CommandLine.arguments[0]) -> String { 43 | let pathComponents = command.split(separator: "/") 44 | let exec = pathComponents.last.map(String.init)! 45 | return [ 46 | "Usage: \(exec) (swift|objc|export) NAME FILE ", 47 | "e.g. \(exec) swift User testModel.json ", 48 | "Options:", 49 | " -v, --verbose Print result instead of creating files", 50 | " -o, --out Output directory (default is current directory)", 51 | " -c, --class (Swift) Use classes instead of structs for objects", 52 | " -ca, --contiguousarray (Swift) Use ContiguousArray for lists", 53 | " -pt, --publictypes (Swift) Make type declarations public instead of internal", 54 | " -pf, --publicfields (Swift) Make field declarations public instead of internal", 55 | " -n, --nullable (Swift and Objective-C) Make all field declarations optional (nullable in Objective-C)", 56 | " -m, --mutable (Swift and Objective-C) All object fields are mutable (var instead of", 57 | " let in Swift and 'readwrite' instead of 'readonly' in Objective-C)", 58 | " -a, --atomic (Objective-C) Make properties 'atomic'", 59 | " -p, --prefix (Objective-C) Class-prefix to use for type declarations", 60 | " -r, --reversemapping (Objective-C) Create method for reverse mapping (toJson)", 61 | ].joined(separator: "\n") 62 | } 63 | 64 | public func main() throws -> String { 65 | let model = try JSON(foundation: json) 66 | let rootType: FieldType 67 | if ModelExportTranslator.isSchema(model) { 68 | rootType = try FieldType(json: model) 69 | } else { 70 | rootType = modelGenerator.decode(model) 71 | } 72 | if writeToConsole { 73 | return translator.translate(rootType, name: modelName) 74 | } 75 | 76 | 77 | let files: [TranslatorOutput] = translator.translate(rootType, name: modelName) 78 | let baseUrl = URL(fileURLWithPath: outputDirectory, isDirectory: true) 79 | var isDir: ObjCBool = false 80 | if !FileManager.default.fileExists(atPath: baseUrl.path, isDirectory: &isDir) { 81 | throw ReverseJsonError.outputDirectoryDoesNotExist(outputDirectory) 82 | } 83 | if !isDir.boolValue { 84 | throw ReverseJsonError.outputPathIsNoDirectory(outputDirectory) 85 | } 86 | var output = "" 87 | let count = files.reduce(0) { count, file in 88 | let fileUrl = baseUrl.appendingPathComponent(file.name, isDirectory: false) 89 | output += "Writing \(fileUrl.path)" 90 | defer { output += "\n" } 91 | do { 92 | try file.content.data(using: .utf8)?.write(to: fileUrl) 93 | return count + 1 94 | } catch { 95 | output += " (FAILED: \(error))" 96 | return count 97 | } 98 | } 99 | return output + "Wrote \(count) files" 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /Sources/ReverseJsonCore/ModelGenerator.swift: -------------------------------------------------------------------------------- 1 | import CoreJSON 2 | 3 | public struct ObjectField: Hashable, Equatable { 4 | public let name: String 5 | public let type: FieldType 6 | 7 | public init(name: String, type: FieldType) { 8 | self.name = name 9 | self.type = type 10 | } 11 | } 12 | public enum NumberType: String, Hashable, Equatable { 13 | case bool = "Bool" 14 | case int = "Int" 15 | case float = "Float" 16 | case double = "Double" 17 | } 18 | 19 | public indirect enum FieldType: Hashable, Equatable { 20 | case object(name: String?, Set) 21 | case list(FieldType) 22 | case text 23 | case number(NumberType) 24 | case `enum`(name: String?, Set) 25 | case unknown(name: String?) 26 | case optional(FieldType) 27 | } 28 | 29 | extension FieldType { 30 | public static func unnamedObject(_ fields: Set) -> FieldType { 31 | return .object(name: nil, fields) 32 | } 33 | public static func unnamedEnum(_ subTypes: Set) -> FieldType { 34 | return .enum(name: nil, subTypes) 35 | } 36 | public static var unnamedUnknown: FieldType { 37 | return .unknown(name: nil) 38 | } 39 | } 40 | 41 | extension FieldType { 42 | public var isOptional: Bool { 43 | switch self { 44 | case .optional: return true 45 | default: return false 46 | } 47 | } 48 | 49 | public var unwrapOptional: FieldType { 50 | switch self { 51 | case let .optional(type): 52 | return type.unwrapOptional 53 | default: 54 | return self 55 | } 56 | } 57 | } 58 | 59 | public struct ModelGenerator { 60 | 61 | public var allFieldsOptional = false 62 | 63 | public init() {} 64 | 65 | public func decode(_ value: JSON) -> FieldType { 66 | let fieldType = internalDecode(value) 67 | if allFieldsOptional { 68 | return ModelGenerator.transformAllFieldsToOptional(fieldType) 69 | } 70 | return fieldType 71 | } 72 | 73 | public func internalDecode(_ value: JSON) -> FieldType { 74 | switch value { 75 | case .string: return .text 76 | case let .number(number): 77 | switch number { 78 | case .int: return .number(.int) 79 | case .int64: return .number(.int) 80 | case .float: return .number(.float) 81 | case .uint: return .number(.int) 82 | case .uint64: return .number(.int) 83 | case .double: return .number(.double) 84 | } 85 | case .bool: return .number(.bool) 86 | case .null: return .optional(.unnamedUnknown) 87 | case let .object(object): 88 | return decode(object) 89 | case let .array(array): 90 | if let subType = decode(array) { 91 | return .list(subType) 92 | } else { 93 | return .list(.unnamedUnknown) 94 | } 95 | } 96 | } 97 | 98 | private func decode(_ dict: [String: JSON]) -> FieldType { 99 | let fields = dict.map { (name: String, value: JSON) in 100 | return ObjectField(name: name, type: internalDecode(value)) 101 | } 102 | return .object(name: nil, Set(fields)) 103 | } 104 | 105 | private func decode(_ list: [JSON]) -> FieldType? { 106 | let types = list.compactMap(internalDecode) 107 | return types.reduce(nil) { (type1, type2) -> FieldType? in 108 | if let type1 = type1 { 109 | return type1.mergeWith(type2) 110 | } 111 | return type2 112 | } 113 | } 114 | 115 | private static func transformAllFieldsToOptionalImpl(_ rootField: FieldType) -> FieldType { 116 | switch rootField { 117 | case let .object(name, fields): 118 | let mappedFields = fields.map { 119 | ObjectField(name: $0.name, type: transformAllFieldsToOptionalImpl($0.type)) 120 | } 121 | return .optional(.object(name: name, Set(mappedFields))) 122 | case let .list(fieldType): 123 | return .optional(.list(transformAllFieldsToOptional(fieldType))) 124 | case .text: 125 | return .optional(.text) 126 | case let .number(numberType): 127 | return .optional(.number(numberType)) 128 | case let .enum(name, fields): 129 | let mappedFields = fields.map { 130 | transformAllFieldsToOptional($0) 131 | } 132 | return .optional(.enum(name: name, Set(mappedFields))) 133 | case .unknown: 134 | return .optional(.unnamedUnknown) 135 | case let .optional(fieldType): 136 | return .optional(transformAllFieldsToOptional(fieldType)) 137 | } 138 | } 139 | 140 | static func transformAllFieldsToOptional(_ rootField: FieldType) -> FieldType { 141 | switch rootField { 142 | case let .object(name, fields): 143 | let mappedFields = fields.map { 144 | ObjectField(name: $0.name, type: transformAllFieldsToOptionalImpl($0.type)) 145 | } 146 | return .object(name: name, Set(mappedFields)) 147 | case let .list(fieldType): 148 | return .list(transformAllFieldsToOptional(fieldType)) 149 | case let .enum(name, fields): 150 | let mappedFields = fields.map { transformAllFieldsToOptional($0) } 151 | return .enum(name: name, Set(mappedFields)) 152 | default: 153 | return rootField 154 | } 155 | } 156 | 157 | } 158 | 159 | extension NumberType { 160 | fileprivate func mergeWith(_ numberType: NumberType) -> NumberType? { 161 | switch (self, numberType) { 162 | case let (numberType1, numberType2) where numberType1 == numberType2: return numberType1 163 | case (.bool, _), (_, .bool): return nil 164 | case (.double, _), (_, .double): return .double 165 | case (.float, _), (_, .float): return .float 166 | default: return self // Can't be reached 167 | } 168 | } 169 | } 170 | 171 | extension FieldType { 172 | fileprivate func mergeWith(_ type: FieldType) -> FieldType { 173 | func mergeEnumTypes(_ enumTypes: Set, otherType: FieldType) -> Set { 174 | if enumTypes.contains(otherType) { 175 | return enumTypes 176 | } 177 | var merged = false 178 | let newEnumTypes: [FieldType] = enumTypes.map { enumType in 179 | switch (enumType, otherType) { 180 | case let (.optional(type1), type2), let (type2, .optional(type1)): 181 | merged = true 182 | if case let .optional(type2) = type2 { 183 | return .optional(type1.mergeWith(type2)) 184 | } else { 185 | return .optional(type1.mergeWith(type2)) 186 | } 187 | case let (.unknown, knownType), let (knownType, .unknown): 188 | merged = true 189 | return knownType 190 | case (.object, .object): 191 | merged = true 192 | return enumType.mergeWith(otherType) 193 | case let (.number(numberType1), .number(numberType2)) where numberType1.mergeWith(numberType2) != nil: 194 | merged = true 195 | let mergedNumberType = numberType1.mergeWith(numberType2)! 196 | return .number(mergedNumberType) 197 | case let (.list(listType1), .list(listType2)): 198 | merged = true 199 | return .list(listType1.mergeWith(listType2)) 200 | default: 201 | return enumType 202 | } 203 | } 204 | return Set(newEnumTypes + (merged ? [] : [otherType])) 205 | } 206 | 207 | switch (self, type) { 208 | case let (type1, type2) where type1 == type2: 209 | return type1 210 | case let (.optional(type1), type2), let (type2, .optional(type1)): 211 | return .optional(type1.mergeWith(type2)) 212 | case let (.unknown, knownType), let (knownType, .unknown): 213 | return knownType 214 | case let (.number(numberType1), .number(numberType2)) where numberType1.mergeWith(numberType2) != nil: 215 | let mergedNumberType = numberType1.mergeWith(numberType2)! 216 | return .number(mergedNumberType) 217 | case let (.object(name1, fields1), .object(name2, fields2)) where name1 == name2: 218 | var resultFields: Set = [] 219 | var remainingFields = fields2 220 | for f1 in fields1 { 221 | let foundItemIndex = remainingFields.firstIndex { f -> Bool in 222 | return f1.name == f.name 223 | } 224 | let field: ObjectField 225 | if let foundItemIndex = foundItemIndex { 226 | let foundItem = remainingFields.remove(at: foundItemIndex) 227 | let mergedType = f1.type.mergeWith(foundItem.type) 228 | field = ObjectField(name: f1.name, type: mergedType) 229 | } else if case .optional = f1.type { 230 | field = f1 231 | } else { 232 | field = ObjectField(name: f1.name, type: .optional(f1.type)) 233 | } 234 | resultFields.insert(field) 235 | } 236 | for field in remainingFields { 237 | if case .optional = field.type { 238 | resultFields.insert(field) 239 | } else { 240 | resultFields.insert(ObjectField(name: field.name, type: .optional(field.type))) 241 | } 242 | } 243 | return .object(name: name1, resultFields) 244 | case let (.list(listType1), .list(listType2)): 245 | return .list(listType1.mergeWith(listType2)) 246 | case let (.enum(name, enumTypes), type), let (type, .enum(name, enumTypes)): 247 | return .enum(name: name, mergeEnumTypes(enumTypes, otherType: type)) 248 | default: 249 | return .enum(name: nil, [self, type]) 250 | } 251 | } 252 | } 253 | 254 | 255 | -------------------------------------------------------------------------------- /Sources/ReverseJsonCore/ModelTranslator.swift: -------------------------------------------------------------------------------- 1 | 2 | public struct TranslatorOutput: Hashable, Equatable { 3 | public var name: String 4 | public var content: String 5 | 6 | public init(name: String, content: String) { 7 | self.name = name 8 | self.content = content 9 | } 10 | } 11 | 12 | public protocol ModelTranslator { 13 | func translate(_ type: FieldType, name: String) -> [TranslatorOutput] 14 | } 15 | 16 | extension ModelTranslator { 17 | public func translate(_ type: FieldType, name: String, outputFormatter: (TranslatorOutput) -> String = {"// \($0.name)\n\($0.content)"}) -> String { 18 | let files: [TranslatorOutput] = translate(type, name: name) 19 | return files.map(outputFormatter).joined(separator: "\n") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ReverseJsonCore/Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension FieldType { 4 | public var enumCaseName: String { 5 | switch self { 6 | case .object: return "object" 7 | case .list: return "list" 8 | case .text: return "text" 9 | case .number(.bool): return "boolean" 10 | case .number: return "number" 11 | case .enum: return "enum" 12 | case .unknown: return "unknownType" 13 | case let .optional(type): return type.enumCaseName 14 | } 15 | } 16 | } 17 | 18 | 19 | extension String { 20 | 21 | public init(lines: String...) { 22 | self = lines.joined(separator: "\n") 23 | } 24 | 25 | public init(joined parts: [String], separator: String = "\n") { 26 | self = parts.joined(separator: separator) 27 | } 28 | 29 | public func times(_ times: Int) -> String { 30 | return String(joined: (0.. String in 31 | return self 32 | }, separator: "") 33 | } 34 | 35 | public func indent(_ level: Int, spaces: Int = 4) -> String { 36 | let suffix = self.hasSuffix("\n") ? "\n" : "" 37 | let indented = String(joined: self.split(separator: "\n").lazy.map { " ".times(level * spaces) + String($0) }) 38 | return indented + suffix 39 | } 40 | public func firstCapitalized() -> String { 41 | if isEmpty { return "" } 42 | var result = self 43 | result.replaceSubrange(startIndex...startIndex, with: String(self[startIndex]).uppercased()) 44 | return result 45 | } 46 | public func firstLowercased() -> String { 47 | if isEmpty { return "" } 48 | var result = self 49 | result.replaceSubrange(startIndex...startIndex, with: String(self[startIndex]).lowercased()) 50 | return result 51 | } 52 | 53 | public func pascalCased() -> String { 54 | let slices = self.split { $0 == "_" || $0 == " " } 55 | if let first = slices.first { 56 | return slices.dropFirst().reduce(String(first)) { (string, subSequence) in 57 | return string + String(subSequence[subSequence.startIndex]).uppercased() + String(subSequence.suffix(from: subSequence.index(subSequence.startIndex, offsetBy: 1))) 58 | }.firstLowercased() 59 | } 60 | return self 61 | } 62 | public var camelCasedString: String { 63 | return self.pascalCased().firstCapitalized() 64 | } 65 | 66 | 67 | static let swiftKeywords: Set = [ 68 | "associatedtype", "class", "deinit", "enum", "extension", "func", "import", "init", "inout", "internal", "let", "operator", "private", "protocol", "public", "static", "struct", "subscript", "typealias", "var", "break", "case", "continue", "default", "defer", "do", "else", "fallthrough", "for", "guard", "if", "in", "repeat", "return", "switch", "where", "while", "as", "Any", "catch", "dynamicType", "false", "is", "nil", "rethrows", "super", "self", "Self", "throw", "throws", "true", "try", "associativity", "convenience", "dynamic", "didSet", "final", "get", "infix", "indirect", "lazy", "left", "mutating", "none", "nonmutating", "optional", "override", "postfix", "precedence", "prefix", "Protocol", "required", "right", "set", "Type", "unowned", "weak", "willSet" 69 | ] 70 | 71 | static let objcKeywords: Set = [ 72 | "description", "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register", "restrict", "return", "short", "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "_Bool", "_Complex", "_Imaginery" 73 | ] 74 | 75 | public var swiftKeywordEscaped: String { 76 | if String.swiftKeywords.contains(self) { 77 | return "`\(self)`" 78 | } else { 79 | return self 80 | } 81 | } 82 | 83 | 84 | public var asValidSwiftIdentifier: String { 85 | if let identifierHead = first { 86 | let head: String 87 | let identifierTail = suffix(from: index(after: startIndex)) 88 | let tail = String(joined: identifierTail.split(whereSeparator: { !$0.isValidSwiftIdentifierTailCharacter}).map(String.init), separator: "_").pascalCased() 89 | if identifierHead.isValidSwiftIdentifierHeadCharacter { 90 | head = String(identifierHead) 91 | } else { 92 | head = tail.isEmpty || !tail.first!.isValidSwiftIdentifierHeadCharacter ? "_" : "" 93 | } 94 | return "\(head)\(tail)" 95 | } else { 96 | return "_" 97 | } 98 | } 99 | 100 | public var asValidObjcIdentifier: String { 101 | guard !String.objcKeywords.contains(self) && self != "signed" && !self.hasPrefix("new") && !self.hasPrefix("copy") && !self.hasPrefix("init") else { 102 | return "$\(self)" 103 | } 104 | return self 105 | } 106 | } 107 | 108 | extension Character { 109 | 110 | private static let tailCharRanges = Character.headCharRanges + [ 111 | 0x30...0x39, // decimal 112 | 0x300...0x36F, 113 | 0x1DC0...0x1DFF, 114 | 0x20D0...0x20FF, 115 | 0xFE20...0xFE2F, 116 | ] 117 | 118 | private static let headCharRanges: [ClosedRange] = [ 119 | 0x41...0x5A as ClosedRange, // uppercase 120 | 0x61...0x7A as ClosedRange, // lowercase 121 | 0xA8...0xA8 as ClosedRange, 122 | 0xAA...0xAA as ClosedRange, 123 | 0xAD...0xAD as ClosedRange, 124 | 0xAF...0xAF as ClosedRange, 125 | 0xB2...0xB5 as ClosedRange, 126 | 0xB7...0xBA as ClosedRange, 127 | 0xBC...0xBE as ClosedRange, 128 | 0xC0...0xD6 as ClosedRange, 129 | 0xD8...0xF6 as ClosedRange, 130 | 0xF8...0xFF as ClosedRange, 131 | 0x100...0x2FF as ClosedRange, 132 | 0x370...0x167F as ClosedRange, 133 | 0x1681...0x180D as ClosedRange, 134 | 0x180F...0x1DBF as ClosedRange, 135 | 0x1E00...0x1FFF as ClosedRange, 136 | 0x200B...0x200D as ClosedRange, 137 | 0x202A...0x202E as ClosedRange, 138 | 0x203F...0x2040 as ClosedRange, 139 | 0x2054...0x2054 as ClosedRange, 140 | 0x2060...0x206F as ClosedRange, 141 | 0x2070...0x20CF as ClosedRange, 142 | 0x2100...0x218F as ClosedRange, 143 | 0x2460...0x24FF as ClosedRange, 144 | 0x2776...0x2793 as ClosedRange, 145 | 0x2C00...0x2DFF as ClosedRange, 146 | 0x2E80...0x2FFF as ClosedRange, 147 | 0x3004...0x3007 as ClosedRange, 148 | 0x3021...0x302F as ClosedRange, 149 | 0x3031...0x303F as ClosedRange, 150 | 0x3040...0xD7FF as ClosedRange, 151 | 0xF900...0xFD3D as ClosedRange, 152 | 0xFD40...0xFDCF as ClosedRange, 153 | 0xFDF0...0xFE1F as ClosedRange, 154 | 0xFE30...0xFE44 as ClosedRange, 155 | 0xFE47...0xFFFD as ClosedRange, 156 | 0x10000...0x1FFFD as ClosedRange, 157 | 0x20000...0x2FFFD as ClosedRange, 158 | 0x30000...0x3FFFD as ClosedRange, 159 | 0x40000...0x4FFFD as ClosedRange, 160 | 0x50000...0x5FFFD as ClosedRange, 161 | 0x60000...0x6FFFD as ClosedRange, 162 | 0x70000...0x7FFFD as ClosedRange, 163 | 0x80000...0x8FFFD as ClosedRange, 164 | 0x90000...0x9FFFD as ClosedRange, 165 | 0xA0000...0xAFFFD as ClosedRange, 166 | 0xB0000...0xBFFFD as ClosedRange, 167 | 0xC0000...0xCFFFD as ClosedRange, 168 | 0xD0000...0xDFFFD as ClosedRange, 169 | 0xE0000...0xEFFFD as ClosedRange 170 | ] 171 | 172 | var isValidSwiftIdentifierHeadCharacter: Bool { 173 | return !String(self).utf16.compactMap(UnicodeScalar.init).map { scalar -> Bool in 174 | return Character.headCharRanges.contains { range in 175 | range.contains(scalar.value) 176 | } 177 | }.contains(false) 178 | } 179 | 180 | var isValidSwiftIdentifierTailCharacter: Bool { 181 | return !String(self).utf16.compactMap(UnicodeScalar.init).map { scalar -> Bool in 182 | return Character.tailCharRanges.contains { range in 183 | range.contains(scalar.value) 184 | } 185 | }.contains(false) 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /Sources/ReverseJsonModelExport/ModelExportTranslator.swift: -------------------------------------------------------------------------------- 1 | import ReverseJsonCore 2 | import CoreJSON 3 | import CoreJSONConvenience 4 | import CoreJSONPointer 5 | import CoreJSONFoundation 6 | import Foundation 7 | 8 | public struct ModelExportTranslator: ModelTranslator { 9 | 10 | public static let schemaIdentifier = "https://github.com/tomquist/ReverseJson/tree/1.2.0" 11 | 12 | public var isPrettyPrinted: Bool = true 13 | 14 | public init() {} 15 | 16 | public static func isSchema(_ json: JSON) -> Bool { 17 | guard case let .object(props) = json else { return false } 18 | guard case let .string(schemaIdentifier)? = props["$schema"] else { return false } 19 | return schemaIdentifier == ModelExportTranslator.schemaIdentifier 20 | } 21 | 22 | public func translate(_ type: FieldType, name: String) -> [TranslatorOutput] { 23 | let json = type.toJSON(isRoot: true) 24 | let foundationJson = json.toFoundation() 25 | do { 26 | var options: JSONSerialization.WritingOptions = [] 27 | if isPrettyPrinted { 28 | options.insert(.prettyPrinted) 29 | } 30 | let data = try JSONSerialization.data(withJSONObject: foundationJson, options: options) 31 | guard let jsonString = String(data: data, encoding: .utf8) else { return [] } 32 | return [TranslatorOutput(name: "\(name)-model.json", content: jsonString)] 33 | } catch { 34 | return [] 35 | } 36 | } 37 | 38 | } 39 | 40 | extension NumberType { 41 | fileprivate var typeName: String { 42 | return self.rawValue.lowercased() 43 | } 44 | } 45 | 46 | extension FieldType { 47 | 48 | private var typeName: String { 49 | switch self { 50 | case .text: return "string" 51 | case let .number(numberType): return numberType.typeName 52 | case .unknown: return "any" 53 | case let .optional(type): return type.typeName 54 | case .list: return "list" 55 | case .enum: return "any" 56 | case .object: return "object" 57 | } 58 | } 59 | 60 | 61 | public func toJSON(isRoot: Bool = false) -> JSON { 62 | var ret = [String: JSON]() 63 | if isRoot { 64 | ret["$schema"] = .string(ModelExportTranslator.schemaIdentifier) 65 | } 66 | ret["type"] = .string(typeName) 67 | if isOptional { 68 | ret["isOptional"] = .bool(true) 69 | } 70 | switch self.unwrapOptional { 71 | case .text, .number, .unknown, .optional: break 72 | case let .object(name, fields): 73 | var properties: [String: JSON] = [:] 74 | fields.forEach { 75 | properties[$0.name] = $0.type.toJSON() 76 | } 77 | if let name = name { 78 | ret["name"] = .string(name) 79 | } 80 | if properties.count > 0 { 81 | ret["properties"] = .object(properties) 82 | } 83 | case let .enum(name, cases): 84 | if let name = name { 85 | ret["name"] = .string(name) 86 | } 87 | if cases.count > 0 { 88 | ret["of"] = .array(cases.sorted { $0.enumCaseName < $1.enumCaseName }.map { $0.toJSON() }) 89 | } 90 | case let .list(type): 91 | ret["content"] = type.toJSON() 92 | } 93 | // Simplification for type-only models 94 | if ret.count == 1, let type = ret["type"] { 95 | return type 96 | } 97 | // Simplification for optional type-only models 98 | if ret.count == 2, case let .string(type)? = ret["type"], 99 | case let .bool(isOptional)? = ret["isOptional"], isOptional { 100 | return .string("\(type)?") 101 | } 102 | var result = JSON.object(ret) 103 | if isRoot { 104 | result.flattenSchema(isRoot: true) 105 | } 106 | return result 107 | } 108 | 109 | } 110 | 111 | extension JSON { 112 | 113 | mutating func flattenSchema(isRoot: Bool = false) { 114 | var collectedTypes: [String: JSON] = [:] 115 | var prevCollectedTypes = collectedTypes 116 | flattenSchema(types: &collectedTypes) 117 | while collectedTypes != prevCollectedTypes { 118 | prevCollectedTypes = collectedTypes 119 | for key in collectedTypes.keys { 120 | collectedTypes[key]?.flattenSchema(types: &prevCollectedTypes, isDefinitions: true) 121 | } 122 | } 123 | if !collectedTypes.isEmpty { 124 | self.objectValue?["definitions"] = .object(collectedTypes) 125 | } 126 | } 127 | 128 | mutating func flattenSchema(types: inout [String: JSON], isDefinitions: Bool = false) { 129 | if let type = objectValue?["type"]?.stringValue { 130 | if type == "any", var subTypes = objectValue?["of"]?.arrayValue { 131 | for i in subTypes.indices { 132 | subTypes[i].flattenSchema(types: &types) 133 | } 134 | objectValue?["of"] = .array(subTypes) 135 | } else if type == "object", let name = objectValue?["name"]?.stringValue { 136 | let isOptional = objectValue?["isOptional"]?.boolValue ?? false 137 | objectValue?["isOptional"] = nil 138 | types[name] = self 139 | if var properties = objectValue?["properties"]?.objectValue { 140 | for key in properties.keys { 141 | properties[key]?.flattenSchema(types: &types) 142 | } 143 | objectValue?["properties"] = .object(properties) 144 | } 145 | if !isDefinitions { 146 | self = .object(["$ref": .string("#/definitions/\(name)")]) 147 | if isOptional { 148 | objectValue?["isOptional"] = .bool(true) 149 | } 150 | } 151 | } else if type == "list", var content = objectValue?["content"] { 152 | content.flattenSchema(types: &types) 153 | objectValue?["content"] = content 154 | } 155 | } 156 | } 157 | 158 | } 159 | 160 | extension FieldType { 161 | 162 | public enum JSONConvertionError: Error { 163 | case missingParameter(String) 164 | case invalidType(String) 165 | case unexpectedPropertyType(JSON) 166 | } 167 | 168 | public init(json: JSON) throws { 169 | self = try FieldType(json: json, rootJson: json) 170 | } 171 | 172 | private init(json: JSON, rootJson: JSON) throws { 173 | func from(typeString: String, props: [String: JSON]) throws -> FieldType { 174 | var typeString = typeString 175 | if typeString.hasSuffix("?") { 176 | typeString = String(typeString[.. [TranslatorOutput] { 17 | let a = declarationsFor(type, name: name, valueToParse: "jsonValue") 18 | var result: [TranslatorOutput] = Array(a.interfaces).sorted(by: { $0.name < $1.name }) 19 | let implementations: [TranslatorOutput] = a.implementations.sorted(by: { $0.name < $1.name }) 20 | result.append(contentsOf: implementations) 21 | return result 22 | } 23 | 24 | public func isNullable(_ type: FieldType) -> Bool { 25 | switch type { 26 | case .optional: 27 | return true 28 | default: 29 | return false 30 | } 31 | } 32 | 33 | func memoryManagementModifier(_ type: FieldType) -> String { 34 | switch type { 35 | case .optional(.number), .text: 36 | return "copy" 37 | case .number: 38 | return "assign" 39 | default: 40 | return "strong" 41 | } 42 | } 43 | 44 | struct ObjectiveCDeclaration { 45 | let interfaces: Set 46 | let implementations: Set 47 | let parseExpression: String 48 | let fieldRequiredTypeNames: Set 49 | let fullTypeName: String 50 | let toJson: (String) -> String 51 | } 52 | 53 | struct ObjectFieldDeclaration { 54 | let property: String 55 | let initialization: String 56 | let requiredTypeNames: Set 57 | let fieldTypeName: String 58 | let interfaces: Set 59 | let implementations: Set 60 | let toJson: String 61 | } 62 | 63 | private func fieldDeclarationFor(_ type: FieldType, variableNameBase: String? = nil, parentName: String, forceNullable: Bool, valueToParse: String) -> ObjectFieldDeclaration { 64 | let typeIsNullable = isNullable(type) 65 | let nullable = forceNullable || typeIsNullable 66 | let type = forceNullable && !typeIsNullable ? .optional(type) : type 67 | 68 | let nonOptionalVariableNameBase = variableNameBase ?? type.objectTypeName 69 | let variableName = nonOptionalVariableNameBase.pascalCased().asValidObjcIdentifier 70 | let subDeclaration = declarationsFor(type, name: "\(parentName)\(nonOptionalVariableNameBase.camelCasedString)", valueToParse: valueToParse) 71 | 72 | var modifiers = [atomicyModifier, memoryManagementModifier(type)] 73 | if (readonly) { 74 | modifiers.append("readonly") 75 | } 76 | if nullable { 77 | modifiers.append("nullable") 78 | } 79 | let modifierList = String(joined: modifiers, separator: ", ") 80 | let propertyName: String 81 | if subDeclaration.fullTypeName.hasSuffix("*") { 82 | propertyName = variableName 83 | } else { 84 | propertyName = " \(variableName)" 85 | } 86 | let property = "@property (\(modifierList)) \(subDeclaration.fullTypeName)\(propertyName);" 87 | let initialization = "_\(variableName) = \(subDeclaration.parseExpression);" 88 | let jsonReturnValue = subDeclaration.toJson("_\(variableName)") 89 | let toJson: String 90 | if let variableNameBase = variableNameBase { 91 | toJson = "ret[@\"\(variableNameBase)\"] = \(jsonReturnValue);" 92 | } else { 93 | toJson = "if (_\(variableName)) {\n return \(jsonReturnValue);\n}" 94 | } 95 | return ObjectFieldDeclaration(property: property, 96 | initialization: initialization, 97 | requiredTypeNames: subDeclaration.fieldRequiredTypeNames, 98 | fieldTypeName: subDeclaration.fullTypeName, 99 | interfaces: subDeclaration.interfaces, 100 | implementations: subDeclaration.implementations, 101 | toJson: toJson) 102 | } 103 | 104 | private func declarationsFor(_ type: FieldType, name: String, valueToParse: String, isOptional: Bool = false) -> ObjectiveCDeclaration { 105 | switch type { 106 | case let .enum(enumTypeName, enumTypes): 107 | let className = "\(typePrefix)\(enumTypeName ?? name.camelCasedString)" 108 | let fieldValues = enumTypes.sorted{$0.enumCaseName < $1.enumCaseName}.map { 109 | fieldDeclarationFor($0, 110 | parentName: (enumTypeName ?? name.camelCasedString), 111 | forceNullable: enumTypes.count > 1, 112 | valueToParse: "jsonValue") 113 | } 114 | let requiredTypeNames = Set(fieldValues.flatMap{$0.requiredTypeNames}) 115 | let forwardDeclarations = requiredTypeNames.sorted(by: <) 116 | let sortedFieldValues = fieldValues.sorted{$0.fieldTypeName < $1.fieldTypeName} 117 | let properties = sortedFieldValues.map {$0.property} 118 | let initializations = sortedFieldValues.map {$0.initialization.indent(2)} 119 | 120 | var interface = "#import \n" 121 | if !forwardDeclarations.isEmpty { 122 | let forwardDeclarationList = String(joined: forwardDeclarations, separator: ", ") 123 | interface += "@class \(forwardDeclarationList);\n" 124 | } 125 | interface += String(joined: [ 126 | "NS_ASSUME_NONNULL_BEGIN", 127 | "@interface \(className) : NSObject", 128 | "- (instancetype)initWithJsonValue:(nullable id)jsonValue;", 129 | ] + (createToJson ? ["- (id)toJson;"] : []) + properties + [ 130 | "@end", 131 | "NS_ASSUME_NONNULL_END", 132 | ]) 133 | 134 | let imports = forwardDeclarations.map {"#import \"\($0).h\""} 135 | var implementationLines = [ 136 | "#import \"\(className).h\"", 137 | ] + imports + [ 138 | "@implementation \(className)", 139 | "- (instancetype)initWithJsonValue:(id)jsonValue {", 140 | " self = [super init];", 141 | " if (self) {", 142 | ] 143 | implementationLines += initializations 144 | implementationLines += [ 145 | " }", 146 | " return self;", 147 | "}", 148 | ] 149 | if createToJson { 150 | let toJsonImplementation = sortedFieldValues.map {$0.toJson}.joined(separator: " else ") 151 | implementationLines += [ 152 | "- (id)toJson {", 153 | toJsonImplementation.indent(1), 154 | " return nil;", 155 | "}" 156 | ] 157 | } 158 | implementationLines += [ 159 | "@end" 160 | ] 161 | let implementation = String(joined: implementationLines) 162 | 163 | let interfaces = fieldValues.lazy.map {$0.interfaces}.reduce(Set([TranslatorOutput(name: "\(className).h", content: interface)])) { $0.union($1) } 164 | let implementations = fieldValues.lazy.map{$0.implementations}.reduce(Set([TranslatorOutput(name: "\(className).m", content: implementation)])) { $0.union($1) } 165 | let parseExpression = "[[\(className) alloc] initWithJsonValue:\(valueToParse)]" 166 | return ObjectiveCDeclaration(interfaces: interfaces, 167 | implementations: implementations, 168 | parseExpression: parseExpression, 169 | fieldRequiredTypeNames: [className], 170 | fullTypeName: "\(className) *", 171 | toJson: { (name: String) in "[\(name) toJson]"}) 172 | case let .object(objectTypeName, fields): 173 | let className = "\(typePrefix)\(objectTypeName ?? name.camelCasedString)" 174 | let fieldValues = fields.sorted{$0.name < $1.name}.map { 175 | fieldDeclarationFor($0.type, 176 | variableNameBase: $0.name, 177 | parentName: objectTypeName ?? name.camelCasedString, 178 | forceNullable: false, 179 | valueToParse: "dict[@\"\($0.name)\"]") } 180 | let requiredTypeNames = Set(fieldValues.flatMap{$0.requiredTypeNames}) 181 | let forwardDeclarations = requiredTypeNames.sorted(by: <) 182 | let sortedFieldValues = fieldValues.sorted{$0.fieldTypeName < $1.fieldTypeName} 183 | let properties = sortedFieldValues.map {$0.property} 184 | let initializations = sortedFieldValues.map {$0.initialization.indent(2)} 185 | 186 | var interface = "#import \n" 187 | if !forwardDeclarations.isEmpty { 188 | let forwardDeclarationList = String(joined: forwardDeclarations, separator: ", ") 189 | interface += "@class \(forwardDeclarationList);\n" 190 | } 191 | interface += String(joined: [ 192 | "NS_ASSUME_NONNULL_BEGIN", 193 | "@interface \(className) : NSObject", 194 | "- (instancetype)initWithJsonDictionary:(NSDictionary> *)dictionary;", 195 | "- (nullable instancetype)initWithJsonValue:(nullable id)jsonValue;", 196 | ] + (createToJson ? ["- (NSDictionary> *)toJson;"] : []) + properties + [ 197 | "@end", 198 | "NS_ASSUME_NONNULL_END" 199 | ]) 200 | 201 | let imports = forwardDeclarations.map {"#import \"\($0).h\""} 202 | var implementationLines = [ 203 | "#import \"\(className).h\"" 204 | ] 205 | implementationLines += imports 206 | implementationLines += [ 207 | "@implementation \(className)", 208 | "- (instancetype)initWithJsonDictionary:(NSDictionary *)dict {", 209 | " self = [super init];", 210 | " if (self) {", 211 | ] 212 | implementationLines += initializations 213 | implementationLines += [ 214 | " }", 215 | " return self;", 216 | "}", 217 | "- (instancetype)initWithJsonValue:(id)jsonValue {", 218 | " if ([jsonValue isKindOfClass:[NSDictionary class]]) {", 219 | " self = [self initWithJsonDictionary:jsonValue];", 220 | " } else {", 221 | " self = nil;", 222 | " }", 223 | " return self;", 224 | "}", 225 | ] 226 | if createToJson { 227 | implementationLines += [ 228 | "- (NSDictionary> *)toJson {", 229 | " NSMutableDictionary *ret = [NSMutableDictionary dictionaryWithCapacity:\(sortedFieldValues.count)];", 230 | ] 231 | implementationLines += sortedFieldValues.map {$0.toJson.indent(1)} 232 | implementationLines += [ 233 | " return [ret copy];", 234 | "}", 235 | ] 236 | } 237 | implementationLines += [ 238 | "@end", 239 | ] 240 | let implementation = String(joined: implementationLines) 241 | 242 | let interfaces = fieldValues.lazy.map {$0.interfaces}.reduce(Set([TranslatorOutput(name: "\(className).h", content: interface)])) { $0.union($1) } 243 | let implementations = fieldValues.lazy.map{$0.implementations}.reduce(Set([TranslatorOutput(name: "\(className).m", content: implementation)])) { $0.union($1) } 244 | var parseExpression = "[[\(className) alloc] initWithJsonValue:\(valueToParse)]" 245 | if !isOptional { 246 | parseExpression = "(\(parseExpression) ?: [[\(className) alloc] initWithJsonDictionary:@{}])" 247 | } 248 | return ObjectiveCDeclaration(interfaces: interfaces, 249 | implementations: implementations, 250 | parseExpression: parseExpression, 251 | fieldRequiredTypeNames: [className], 252 | fullTypeName: "\(className) *", 253 | toJson: { (name: String) in "[\(name) toJson]"}) 254 | case .text: 255 | let fallback = isOptional ? "nil" : "@\"\"" 256 | return ObjectiveCDeclaration(interfaces: [], 257 | implementations: [], 258 | parseExpression: "[\(valueToParse) isKindOfClass:[NSString class]] ? \(valueToParse) : \(fallback)", 259 | fieldRequiredTypeNames: [], 260 | fullTypeName: "NSString *", 261 | toJson: { $0 }) 262 | case let .number(numberType): 263 | return ObjectiveCDeclaration(interfaces: [], 264 | implementations: [], 265 | parseExpression: "[\(valueToParse) isKindOfClass:[NSNumber class]] ? [\(valueToParse) \(numberType.objcNSNumberMethod)] : 0", 266 | fieldRequiredTypeNames: [], 267 | fullTypeName: numberType.objcNumberType, 268 | toJson: { (name: String) in "@(\(name))" }) 269 | case let .list(origListType): 270 | var listType = origListType 271 | if case let .number(numberType) = listType { 272 | listType = .optional(.number(numberType)) 273 | } 274 | let subName: String 275 | if case .list = listType { 276 | subName = name 277 | } else { 278 | subName = "\(name)Item" 279 | } 280 | let listTypeValues = declarationsFor(listType, name: subName, valueToParse: "item") 281 | let subParseExpression: String 282 | if let lineBreakRange = listTypeValues.parseExpression.range(of: "\n") { 283 | let firstLine = listTypeValues.parseExpression[.. *values = nil;", 300 | " if ([value isKindOfClass:[NSArray class]]) {", 301 | " NSArray *array = value;", 302 | " values = [NSMutableArray arrayWithCapacity:array.count];", 303 | " for (id item in array) {", 304 | " \(listTypeName)parsedItem = \(subParseExpression);", 305 | " [values addObject:parsedItem ?: (id)[NSNull null]];", 306 | " }", 307 | " }", 308 | " [values copy]\(fallback);", 309 | "})" 310 | ) 311 | let valueToJson = listTypeValues.toJson("item") 312 | let subValueToJson: String 313 | if let lineBreakRange = valueToJson.range(of: "\n") { 314 | let firstLine = valueToJson[.. String in 322 | return String(lines: 323 | "({", 324 | " NSMutableArray> *values = nil;", 325 | " NSArray *array = \(variableName);", 326 | " if (array) {", 327 | " values = [NSMutableArray arrayWithCapacity:array.count];", 328 | " for (id item in array) {", 329 | " if (item == [NSNull null]) {", 330 | " [values addObject:item];", 331 | " } else {", 332 | " id json = \(subValueToJson);", 333 | " [values addObject:json ?: (id)[NSNull null]];", 334 | " }", 335 | " }", 336 | " }", 337 | " [values copy]\(fallback);", 338 | "})") 339 | } 340 | return ObjectiveCDeclaration(interfaces: listTypeValues.interfaces, 341 | implementations: listTypeValues.implementations, 342 | parseExpression: parseExpression, 343 | fieldRequiredTypeNames: listTypeValues.fieldRequiredTypeNames, 344 | fullTypeName: "NSArray<\(listTypeValues.fullTypeName)> *", 345 | toJson: toJson) 346 | case let .optional(.number(numberType)): 347 | return ObjectiveCDeclaration(interfaces: [], 348 | implementations: [], 349 | parseExpression: "[\(valueToParse) isKindOfClass:[NSNumber class]] ? \(valueToParse) : nil", 350 | fieldRequiredTypeNames: [], 351 | fullTypeName: "NSNumber/*\(numberType.objcNumberType)*/ *", 352 | toJson: {$0}) 353 | case .optional(let optionalType): 354 | return declarationsFor(optionalType, name: name, valueToParse: valueToParse, isOptional: true) 355 | case .unknown: 356 | return ObjectiveCDeclaration(interfaces: [], 357 | implementations: [], 358 | parseExpression: valueToParse, 359 | fieldRequiredTypeNames: [], 360 | fullTypeName: "id", 361 | toJson: {$0}) 362 | } 363 | } 364 | 365 | } 366 | 367 | extension FieldType { 368 | 369 | fileprivate var objectTypeName: String { 370 | switch self { 371 | case let .object(name?, _): 372 | return name 373 | case let .enum(name?, _): 374 | return name 375 | case let .optional(type): 376 | return type.objectTypeName 377 | default: 378 | return enumCaseName 379 | } 380 | } 381 | 382 | } 383 | 384 | extension NumberType { 385 | fileprivate var objcNumberType: String { 386 | switch self { 387 | case .bool: return "BOOL" 388 | case .int: return "NSInteger" 389 | case .float: return "float" 390 | case .double: return "double" 391 | } 392 | } 393 | fileprivate var objcNSNumberMethod: String { 394 | switch self { 395 | case .bool: return "boolValue" 396 | case .int: return "integerValue" 397 | case .float: return "floatValue" 398 | case .double: return "doubleValue" 399 | } 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /Sources/ReverseJsonSwagger/SwaggerReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemaImport.swift 3 | // ReverseJson 4 | // 5 | // Created by Tom Quist on 07.02.16. 6 | // Copyright © 2016 Tom Quist. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReverseJsonCore 11 | import CoreJSON 12 | import CoreJSONConvenience 13 | import CoreJSONPointer 14 | 15 | public enum SwaggerReadingError: Error { 16 | case missingParameter(String) 17 | case invalidType(String) 18 | case unexpectedPropertyType(JSON) 19 | } 20 | 21 | public struct SwaggerReader { 22 | 23 | private let rootJSON: JSON 24 | 25 | public init(json: JSON) { 26 | rootJSON = json 27 | } 28 | 29 | private func resolve(_ json: JSON) -> (JSON, String?) { 30 | let json = json 31 | if case let .string(ref)? = json.objectValue?["$ref"], 32 | let refUrl = URL(string: ref), 33 | let fragment = refUrl.fragment, 34 | let jsonPointer = JSONPointer(string: fragment), 35 | let refObject = rootJSON[jsonPointer] { 36 | return (refObject, jsonPointer.lastPathComponent) 37 | } 38 | return (json, nil) 39 | } 40 | 41 | private func from(responses json: JSON, path: String, method: String) throws -> [FieldType] { 42 | guard let properties = json.objectValue else { 43 | return [] 44 | } 45 | return properties.compactMap { statusCode, object in 46 | let (object, refName) = resolve(object) 47 | let name: String 48 | if let refName = refName { 49 | name = refName 50 | } else { 51 | let cleanedPath = path.replacingOccurrences(of: "/", with: " ").trimmingCharacters(in: .whitespaces) 52 | name = "\(method.firstCapitalized())\(cleanedPath.camelCasedString)" 53 | } 54 | if let schema = object.objectValue?["schema"] { 55 | let type = try? FieldType(swaggerSchema: schema, rootJson: rootJSON) 56 | switch type { 57 | case let .enum(name: nil, subTypes)?: 58 | return .enum(name: name, subTypes) 59 | case let .object(name: nil, fields)?: 60 | return .object(name: name, fields) 61 | default: 62 | return type 63 | } 64 | } 65 | return nil 66 | } 67 | } 68 | 69 | private func from(operation json: JSON, path: String, method: String) throws -> [FieldType] { 70 | guard let properties = json.objectValue else { 71 | return [] 72 | } 73 | guard let responses = properties["responses"] else { 74 | return [] 75 | } 76 | return try from(responses: responses, path: path, method: method) 77 | } 78 | 79 | private func from(pathItem json: JSON, path: String) throws -> [FieldType] { 80 | guard let properties = json.objectValue else { 81 | return [] 82 | } 83 | return try ["get", "put", "post", "delete", "options", "head", "patch"].compactMap { method in 84 | properties[method].map { (method, $0) } 85 | }.flatMap { method, operation in 86 | try from(operation: operation, path: path, method: method) 87 | } 88 | } 89 | 90 | public func allTypes() throws -> Set { 91 | guard case let .object(paths)? = rootJSON.objectValue?["paths"] else { 92 | throw SwaggerReadingError.missingParameter("paths") 93 | } 94 | let types = try paths.flatMap { path, value in 95 | try from(pathItem: value, path: path) 96 | } 97 | return Set(types) 98 | } 99 | 100 | } 101 | 102 | extension FieldType { 103 | 104 | fileprivate init(swaggerSchema json: JSON, rootJson: JSON) throws { 105 | func from(typeString: String, props: [String: JSON]) throws -> FieldType { 106 | switch typeString { 107 | case "string": return .text 108 | case "boolean": return .number(.bool) 109 | case "integer": return .number(.int) 110 | case "number": 111 | switch props["format"] { 112 | case .string("float")?: 113 | return .number(.float) 114 | case .string("double")?: 115 | fallthrough 116 | default: 117 | return .number(.double) 118 | } 119 | case "object": 120 | let properties: [String: JSON] 121 | if case let .object(f)? = props["properties"] { 122 | properties = f 123 | } else { 124 | properties = [:] 125 | } 126 | let required: Set 127 | if case let .array(jsonRequired)? = props["required"] { 128 | required = Set(jsonRequired.compactMap { $0.stringValue }) 129 | } else { 130 | required = [] 131 | } 132 | let fields = try properties.map { name, propertyModel -> ObjectField in 133 | var type = try FieldType(swaggerSchema: propertyModel, rootJson: rootJson) 134 | if !required.contains(name) { 135 | type = .optional(type) 136 | } 137 | return ObjectField(name: name, type: type) 138 | } 139 | let objectName: String? 140 | if case let .string(name)? = props["name"] { 141 | objectName = name 142 | } else { 143 | objectName = nil 144 | } 145 | if properties.count == 0 { 146 | return .unknown(name: objectName) 147 | } else { 148 | return .object(name: objectName, Set(fields)) 149 | } 150 | case "array": 151 | let content = try props["items"].map { try FieldType(swaggerSchema: $0, rootJson: rootJson) } ?? .unnamedUnknown 152 | return .list(content) 153 | default: 154 | throw SwaggerReadingError.invalidType(typeString) 155 | } 156 | } 157 | switch json { 158 | case let .string(string): 159 | self = try from(typeString: string, props: [:]) 160 | case var .object(props): 161 | if case let .string(reference)? = props["$ref"], 162 | let refUri = URL(string: reference), 163 | let fragment = refUri.fragment, 164 | let jsonPointer = JSONPointer(string: fragment), 165 | let declaration = rootJson[jsonPointer] { 166 | var fieldType = try FieldType(swaggerSchema: declaration, rootJson: rootJson) 167 | switch fieldType { 168 | case let .enum(name: nil, subtypes): 169 | fieldType = .enum(name: jsonPointer.lastPathComponent, subtypes) 170 | case let .object(name: nil, fields): 171 | fieldType = .object(name: jsonPointer.lastPathComponent, fields) 172 | default: 173 | break 174 | } 175 | self = fieldType 176 | return 177 | } 178 | guard case let .string(typeString)? = props["type"] else { 179 | throw SwaggerReadingError.missingParameter("type") 180 | } 181 | props["type"] = nil 182 | self = try from(typeString: typeString, props: props) 183 | default: 184 | throw SwaggerReadingError.unexpectedPropertyType(json) 185 | } 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ReverseJsonCommandLineTests 4 | import ReverseJsonCoreTests 5 | import ReverseJsonModelExportTests 6 | import ReverseJsonObjcTests 7 | import ReverseJsonSwiftTests 8 | 9 | var tests = [XCTestCaseEntry]() 10 | tests += ReverseJsonCommandLineTests.__allTests() 11 | tests += ReverseJsonCoreTests.__allTests() 12 | tests += ReverseJsonModelExportTests.__allTests() 13 | tests += ReverseJsonObjcTests.__allTests() 14 | tests += ReverseJsonSwiftTests.__allTests() 15 | 16 | XCTMain(tests) 17 | -------------------------------------------------------------------------------- /Tests/ReverseJsonCommandLineTests/Inputs/invalid.json: -------------------------------------------------------------------------------- 1 | {] -------------------------------------------------------------------------------- /Tests/ReverseJsonCommandLineTests/Inputs/valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "height" : 10 3 | } 4 | -------------------------------------------------------------------------------- /Tests/ReverseJsonCommandLineTests/ModelGeneratorCommandLineTest.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import ReverseJsonCore 4 | @testable import ReverseJsonCommandLine 5 | 6 | class ModelGeneratorCommandLineTest: XCTestCase { 7 | 8 | func testAllFieldsOptionalFlag() { 9 | let generator1 = try! ModelGenerator(args: ["-n"]) 10 | let generator2 = try! ModelGenerator(args: ["--nullable"]) 11 | let generator3 = try! ModelGenerator(args: []) 12 | XCTAssertEqual(generator1.allFieldsOptional, true) 13 | XCTAssertEqual(generator2.allFieldsOptional, true) 14 | XCTAssertEqual(generator3.allFieldsOptional, false) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Tests/ReverseJsonCommandLineTests/ObjcModelCreatorCommandLineTest.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import ReverseJsonCore 4 | import ReverseJsonObjc 5 | @testable import ReverseJsonCommandLine 6 | 7 | class ObjcModelCreatorCommandLineTest: XCTestCase { 8 | 9 | func testAtomicFieldsFlag() { 10 | let modelCreator1 = try! ObjcModelCreator(args: ["-a"]) 11 | let modelCreator2 = try! ObjcModelCreator(args: ["--atomic"]) 12 | let modelCreator3 = try! ObjcModelCreator(args: []) 13 | XCTAssertEqual(modelCreator1.atomic, true) 14 | XCTAssertEqual(modelCreator2.atomic, true) 15 | XCTAssertEqual(modelCreator3.atomic, false) 16 | } 17 | 18 | func testMutableFieldsFlag() { 19 | let modelCreator1 = try! ObjcModelCreator(args: ["-m"]) 20 | let modelCreator2 = try! ObjcModelCreator(args: ["--mutable"]) 21 | let modelCreator3 = try! ObjcModelCreator(args: []) 22 | XCTAssertEqual(modelCreator1.readonly, false) 23 | XCTAssertEqual(modelCreator2.readonly, false) 24 | XCTAssertEqual(modelCreator3.readonly, true) 25 | } 26 | 27 | func testReverseMappingFlag() { 28 | let modelCreator1 = try! ObjcModelCreator(args: ["-r"]) 29 | let modelCreator2 = try! ObjcModelCreator(args: ["--reversemapping"]) 30 | let modelCreator3 = try! ObjcModelCreator(args: []) 31 | XCTAssertEqual(modelCreator1.createToJson, true) 32 | XCTAssertEqual(modelCreator2.createToJson, true) 33 | XCTAssertEqual(modelCreator3.createToJson, false) 34 | } 35 | 36 | func testPrefixOption() { 37 | let modelCreator1 = try! ObjcModelCreator(args: ["-p", "ABC"]) 38 | let modelCreator2 = try! ObjcModelCreator(args: ["--prefix", "ABC"]) 39 | let modelCreator3 = try! ObjcModelCreator(args: []) 40 | XCTAssertEqual(modelCreator1.typePrefix, "ABC") 41 | XCTAssertEqual(modelCreator2.typePrefix, "ABC") 42 | XCTAssertEqual(modelCreator3.typePrefix, "") 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Tests/ReverseJsonCommandLineTests/ReverseJsonTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | import ReverseJsonCore 4 | import ReverseJsonSwift 5 | import ReverseJsonObjc 6 | @testable import ReverseJsonCommandLine 7 | 8 | struct DummyTranslator: ModelTranslator { 9 | let fileName: String 10 | 11 | func translate(_ type: FieldType, name: String) -> [TranslatorOutput] { 12 | return [TranslatorOutput(name: fileName, content: "\(name): \(type)")] 13 | } 14 | } 15 | 16 | class ReverseJsonTest: XCTestCase { 17 | 18 | private func resourcePath(_ name: String) -> String { 19 | return URL(fileURLWithPath: #file).deletingLastPathComponent().appendingPathComponent("Inputs").appendingPathComponent(name).path 20 | } 21 | 22 | func testConsoleOutput() { 23 | let modelGenerator = ModelGenerator() 24 | let reverseJson = ReverseJson(json: "Test", modelName: "Name", modelGenerator: modelGenerator, translator: DummyTranslator(fileName: "fileName"), writeToConsole: true, outputDirectory: "") 25 | XCTAssertEqual(try? reverseJson.main(), "// fileName\nName: \(modelGenerator.decode(.string("Test")))") 26 | } 27 | 28 | func testExistingOutputButNotADir() { 29 | let fileName = "ReverseJsonTestExistingFile" 30 | let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory()) 31 | let outUrl = tmpDir.appendingPathComponent(fileName, isDirectory: false) 32 | try? FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) 33 | if FileManager.default.fileExists(atPath: outUrl.path, isDirectory: nil) { 34 | try? FileManager.default.removeItem(at: outUrl) 35 | } 36 | let _ = FileManager.default.createFile(atPath: outUrl.path, contents: nil, attributes: nil) 37 | defer { 38 | try? FileManager.default.removeItem(at: outUrl) 39 | } 40 | let modelGenerator = ModelGenerator() 41 | let reverseJson = ReverseJson(json: "Test", modelName: "Name", modelGenerator: modelGenerator, translator: DummyTranslator(fileName: fileName), writeToConsole: false, outputDirectory: outUrl.path) 42 | do { 43 | let _ = try reverseJson.main() 44 | XCTFail() 45 | } catch ReverseJsonError.outputPathIsNoDirectory(let dir) { 46 | XCTAssertEqual(dir, outUrl.path) 47 | } catch { 48 | XCTFail("\(error)") 49 | } 50 | } 51 | 52 | func testNonExistingOutputDir() { 53 | let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory()) 54 | let outUrl = tmpDir.appendingPathComponent("ReverseJsonTestNonExistingDir", isDirectory: true) 55 | if FileManager.default.fileExists(atPath: outUrl.path, isDirectory: nil) { 56 | try? FileManager.default.removeItem(at: outUrl) 57 | } 58 | let modelGenerator = ModelGenerator() 59 | let reverseJson = ReverseJson(json: "Test", modelName: "Name", modelGenerator: modelGenerator, translator: DummyTranslator(fileName: "fileName"), writeToConsole: false, outputDirectory: outUrl.path) 60 | do { 61 | let _ = try reverseJson.main() 62 | XCTFail() 63 | } catch ReverseJsonError.outputDirectoryDoesNotExist(let dir) { 64 | XCTAssertEqual(dir, outUrl.path) 65 | } catch { 66 | XCTFail(error.localizedDescription) 67 | } 68 | } 69 | 70 | func testFileOutput() { 71 | let outUrl = URL(fileURLWithPath: NSTemporaryDirectory()) 72 | let fileName = "ReverseJsonTestDummyOutput" 73 | defer { 74 | let outFile = outUrl.appendingPathComponent(fileName) 75 | try? FileManager.default.removeItem(at: outFile) 76 | } 77 | let modelGenerator = ModelGenerator() 78 | let reverseJson = ReverseJson(json: "Test", modelName: "Name", modelGenerator: modelGenerator, translator: DummyTranslator(fileName: fileName), writeToConsole: false, outputDirectory: outUrl.path) 79 | do { 80 | let _ = try reverseJson.main() 81 | } catch { 82 | XCTFail(error.localizedDescription) 83 | } 84 | } 85 | 86 | func testHasUsage() { 87 | let usage = ReverseJson.usage(command: "__IGNORE__/__XYZ__") 88 | XCTAssertFalse(usage.isEmpty) 89 | XCTAssertTrue(usage.contains("__XYZ__")) 90 | XCTAssertFalse(usage.contains("__IGNORE__")) 91 | } 92 | 93 | func testNoArguments() { 94 | do { 95 | let _ = try ReverseJson(args: []) 96 | XCTFail() 97 | } catch ReverseJsonError.wrongArgumentCount { 98 | return 99 | } catch { 100 | XCTFail() 101 | } 102 | } 103 | 104 | func testWrongArgumentCount() { 105 | do { 106 | let _ = try ReverseJson(args: ["1", "2", "3"]) 107 | XCTFail() 108 | } catch ReverseJsonError.wrongArgumentCount { 109 | return 110 | } catch { 111 | XCTFail() 112 | } 113 | } 114 | 115 | func testUnknownLanguage() { 116 | do { 117 | let _ = try ReverseJson(args: ["ReverseJson", "python", "Test", resourcePath("valid.json")]) 118 | XCTFail() 119 | } catch ReverseJsonError.unsupportedLanguage("python") { 120 | return 121 | } catch { 122 | XCTFail() 123 | } 124 | } 125 | 126 | func testNonExistingFile() { 127 | do { 128 | let _ = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("nonExisting.json")]) 129 | XCTFail() 130 | } catch ReverseJsonError.unableToRead(file: resourcePath("nonExisting.json"), _) { 131 | return 132 | } catch { 133 | XCTFail() 134 | } 135 | } 136 | 137 | func testInvalidJson() { 138 | do { 139 | let _ = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("invalid.json")]) 140 | XCTFail() 141 | } catch ReverseJsonError.unableToParseFile { 142 | return 143 | } catch { 144 | XCTFail() 145 | } 146 | } 147 | 148 | func testSwift() { 149 | do { 150 | let reverseJson = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("valid.json")]) 151 | XCTAssertTrue(reverseJson.translator is SwiftTranslator) 152 | } catch { 153 | XCTFail() 154 | } 155 | } 156 | 157 | func testObjc() { 158 | do { 159 | let reverseJson = try ReverseJson(args: ["ReverseJson", "objc", "Test", resourcePath("valid.json")]) 160 | XCTAssertTrue(reverseJson.translator is ObjcModelCreator) 161 | } catch { 162 | XCTFail() 163 | } 164 | } 165 | 166 | func testPassArgumentsToTranslator() { 167 | do { 168 | let reverseJson = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("valid.json"), "--mutable"]) 169 | guard let translator = reverseJson.translator as? SwiftTranslator else { 170 | XCTFail() 171 | return 172 | } 173 | XCTAssertTrue(translator.mutableFields) 174 | } catch { 175 | XCTFail() 176 | } 177 | } 178 | 179 | func testPassArgumentsToModelGenerator() { 180 | do { 181 | let reverseJson = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("valid.json"), "--nullable"]) 182 | XCTAssertTrue(reverseJson.modelGenerator.allFieldsOptional) 183 | } catch { 184 | XCTFail() 185 | } 186 | } 187 | 188 | func testModelName() { 189 | do { 190 | let reverseJson = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("valid.json")]) 191 | XCTAssertEqual(reverseJson.modelName, "Test") 192 | } catch { 193 | XCTFail() 194 | } 195 | } 196 | 197 | func testParsedJson() { 198 | do { 199 | let reverseJson = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("valid.json")]) 200 | let dict = reverseJson.json as? [String: Any] 201 | XCTAssertNotNil(dict) 202 | XCTAssertEqual(dict?["height"] as? Int, 10) 203 | } catch { 204 | XCTFail() 205 | } 206 | } 207 | 208 | func testVerbose() { 209 | do { 210 | let reverseJson0 = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("valid.json")]) 211 | let reverseJson1 = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("valid.json"), "-v"]) 212 | let reverseJson2 = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("valid.json"), "--verbose"]) 213 | XCTAssertFalse(reverseJson0.writeToConsole) 214 | XCTAssertTrue(reverseJson1.writeToConsole) 215 | XCTAssertTrue(reverseJson2.writeToConsole) 216 | } catch { 217 | XCTFail() 218 | } 219 | } 220 | 221 | func testOutDir() { 222 | do { 223 | let reverseJson0 = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("valid.json")]) 224 | let reverseJson1 = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("valid.json"), "-o", "bla"]) 225 | let reverseJson2 = try ReverseJson(args: ["ReverseJson", "swift", "Test", resourcePath("valid.json"), "--out", "bla"]) 226 | XCTAssertEqual("", reverseJson0.outputDirectory) 227 | XCTAssertEqual("bla", reverseJson1.outputDirectory) 228 | XCTAssertEqual("bla", reverseJson2.outputDirectory) 229 | } catch { 230 | XCTFail() 231 | } 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /Tests/ReverseJsonCommandLineTests/SwiftTranslatorCommandLineTest.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import ReverseJsonCore 4 | import ReverseJsonSwift 5 | @testable import ReverseJsonCommandLine 6 | 7 | class SwiftTranslatorCommandLineTest: XCTestCase { 8 | 9 | func testPublicTypeFlagWithObject() { 10 | let translator1 = try! SwiftTranslator(args: ["-pt"]) 11 | let translator2 = try! SwiftTranslator(args: ["--publictypes"]) 12 | let translator3 = try! SwiftTranslator(args: []) 13 | XCTAssertEqual(translator1.typeVisibility, Visibility.publicVisibility) 14 | XCTAssertEqual(translator2.typeVisibility, Visibility.publicVisibility) 15 | XCTAssertEqual(translator3.typeVisibility, Visibility.internalVisibility) 16 | } 17 | 18 | func testClassFlag() { 19 | let translator1 = try! SwiftTranslator(args: ["-c"]) 20 | let translator2 = try! SwiftTranslator(args: ["--class"]) 21 | let translator3 = try! SwiftTranslator(args: []) 22 | XCTAssertEqual(translator1.objectType, ObjectType.classType) 23 | XCTAssertEqual(translator2.objectType, ObjectType.classType) 24 | XCTAssertEqual(translator3.objectType, ObjectType.structType) 25 | } 26 | 27 | func testPublicFieldsFlag() { 28 | let translator1 = try! SwiftTranslator(args: ["-pf"]) 29 | let translator2 = try! SwiftTranslator(args: ["--publicfields"]) 30 | let translator3 = try! SwiftTranslator(args: []) 31 | XCTAssertEqual(translator1.fieldVisibility, Visibility.publicVisibility) 32 | XCTAssertEqual(translator2.fieldVisibility, Visibility.publicVisibility) 33 | XCTAssertEqual(translator3.fieldVisibility, Visibility.internalVisibility) 34 | } 35 | 36 | func testMutableFieldsFlag() { 37 | let translator1 = try! SwiftTranslator(args: ["-m"]) 38 | let translator2 = try! SwiftTranslator(args: ["--mutable"]) 39 | let translator3 = try! SwiftTranslator(args: []) 40 | XCTAssertEqual(translator1.mutableFields, true) 41 | XCTAssertEqual(translator2.mutableFields, true) 42 | XCTAssertEqual(translator3.mutableFields, false) 43 | } 44 | 45 | func testContiguousArrayFlag() { 46 | let translator1 = try! SwiftTranslator(args: ["-ca"]) 47 | let translator2 = try! SwiftTranslator(args: ["--contiguousarray"]) 48 | let translator3 = try! SwiftTranslator(args: []) 49 | XCTAssertEqual(translator1.listType, ListType.contiguousArray) 50 | XCTAssertEqual(translator2.listType, ListType.contiguousArray) 51 | XCTAssertEqual(translator3.listType, ListType.array) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Tests/ReverseJsonCommandLineTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension ModelGeneratorCommandLineTest { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__ModelGeneratorCommandLineTest = [ 9 | ("testAllFieldsOptionalFlag", testAllFieldsOptionalFlag), 10 | ] 11 | } 12 | 13 | extension ObjcModelCreatorCommandLineTest { 14 | // DO NOT MODIFY: This is autogenerated, use: 15 | // `swift test --generate-linuxmain` 16 | // to regenerate. 17 | static let __allTests__ObjcModelCreatorCommandLineTest = [ 18 | ("testAtomicFieldsFlag", testAtomicFieldsFlag), 19 | ("testMutableFieldsFlag", testMutableFieldsFlag), 20 | ("testPrefixOption", testPrefixOption), 21 | ("testReverseMappingFlag", testReverseMappingFlag), 22 | ] 23 | } 24 | 25 | extension ReverseJsonTest { 26 | // DO NOT MODIFY: This is autogenerated, use: 27 | // `swift test --generate-linuxmain` 28 | // to regenerate. 29 | static let __allTests__ReverseJsonTest = [ 30 | ("testConsoleOutput", testConsoleOutput), 31 | ("testExistingOutputButNotADir", testExistingOutputButNotADir), 32 | ("testFileOutput", testFileOutput), 33 | ("testHasUsage", testHasUsage), 34 | ("testInvalidJson", testInvalidJson), 35 | ("testModelName", testModelName), 36 | ("testNoArguments", testNoArguments), 37 | ("testNonExistingFile", testNonExistingFile), 38 | ("testNonExistingOutputDir", testNonExistingOutputDir), 39 | ("testObjc", testObjc), 40 | ("testOutDir", testOutDir), 41 | ("testParsedJson", testParsedJson), 42 | ("testPassArgumentsToModelGenerator", testPassArgumentsToModelGenerator), 43 | ("testPassArgumentsToTranslator", testPassArgumentsToTranslator), 44 | ("testSwift", testSwift), 45 | ("testUnknownLanguage", testUnknownLanguage), 46 | ("testVerbose", testVerbose), 47 | ("testWrongArgumentCount", testWrongArgumentCount), 48 | ] 49 | } 50 | 51 | extension SwiftTranslatorCommandLineTest { 52 | // DO NOT MODIFY: This is autogenerated, use: 53 | // `swift test --generate-linuxmain` 54 | // to regenerate. 55 | static let __allTests__SwiftTranslatorCommandLineTest = [ 56 | ("testClassFlag", testClassFlag), 57 | ("testContiguousArrayFlag", testContiguousArrayFlag), 58 | ("testMutableFieldsFlag", testMutableFieldsFlag), 59 | ("testPublicFieldsFlag", testPublicFieldsFlag), 60 | ("testPublicTypeFlagWithObject", testPublicTypeFlagWithObject), 61 | ] 62 | } 63 | 64 | public func __allTests() -> [XCTestCaseEntry] { 65 | return [ 66 | testCase(ModelGeneratorCommandLineTest.__allTests__ModelGeneratorCommandLineTest), 67 | testCase(ObjcModelCreatorCommandLineTest.__allTests__ObjcModelCreatorCommandLineTest), 68 | testCase(ReverseJsonTest.__allTests__ReverseJsonTest), 69 | testCase(SwiftTranslatorCommandLineTest.__allTests__SwiftTranslatorCommandLineTest), 70 | ] 71 | } 72 | #endif 73 | -------------------------------------------------------------------------------- /Tests/ReverseJsonCoreTests/ModelGeneratorTest.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import Foundation 4 | import CoreJSON 5 | import CoreJSONLiterals 6 | @testable import ReverseJsonCore 7 | 8 | class ModelGeneratorTest: XCTestCase { 9 | 10 | var parser = ModelGenerator() 11 | 12 | func XCTAsserEqualFieldType(_ fieldType1: FieldType, _ fieldType2: FieldType) { 13 | XCTAssertEqual(fieldType1, fieldType2) 14 | } 15 | func XCTAsserNotEqualFieldType(_ fieldType1: FieldType, _ fieldType2: FieldType) { 16 | XCTAssertNotEqual(fieldType1, fieldType2) 17 | } 18 | 19 | func testEqualTypeUnknown() { 20 | XCTAsserEqualFieldType(.unnamedUnknown, .unnamedUnknown) 21 | XCTAsserNotEqualFieldType(.unnamedUnknown, .number(.int)) 22 | } 23 | 24 | func testEqualTypeText() { 25 | XCTAsserEqualFieldType(FieldType.text, .text) 26 | XCTAsserNotEqualFieldType(.text, .number(.int)) 27 | } 28 | 29 | func testEqualNumberTypes() { 30 | XCTAssertEqual(NumberType.int, NumberType.int) 31 | XCTAssertEqual(NumberType.float, NumberType.float) 32 | XCTAssertEqual(NumberType.bool, NumberType.bool) 33 | XCTAssertEqual(NumberType.double, NumberType.double) 34 | XCTAssertNotEqual(NumberType.int, NumberType.double) 35 | XCTAssertNotEqual(NumberType.float, NumberType.double) 36 | } 37 | 38 | func testEqualTypeNumber() { 39 | XCTAsserEqualFieldType(.number(.int), .number(.int)) 40 | XCTAsserNotEqualFieldType(.number(.int), .text) 41 | XCTAsserNotEqualFieldType(.number(.int), .number(.double)) 42 | } 43 | 44 | func testEqualTypeList() { 45 | XCTAsserEqualFieldType(.list(.text), .list(.text)) 46 | XCTAsserNotEqualFieldType(.list(.text), .text) 47 | XCTAsserNotEqualFieldType(.list(.text), .list(.number(.int))) 48 | } 49 | 50 | func testEqualTypeOptional() { 51 | XCTAsserEqualFieldType(.optional(.text), .optional(.text)) 52 | XCTAsserNotEqualFieldType(.optional(.text), .text) 53 | XCTAsserNotEqualFieldType(.optional(.text), .list(.text)) 54 | XCTAsserNotEqualFieldType(.optional(.text), .optional(.number(.int))) 55 | } 56 | 57 | func testEqualTypeObject() { 58 | XCTAsserEqualFieldType(.unnamedObject([]), .unnamedObject([])) 59 | XCTAsserEqualFieldType(.unnamedObject([.init(name: "object", type: .text)]), .unnamedObject([.init(name: "object", type: .text)])) 60 | XCTAsserEqualFieldType(.unnamedObject([ 61 | .init(name: "object", type: .text), 62 | .init(name: "int", type: .number(.int)), 63 | ]), .unnamedObject([ 64 | .init(name: "int", type: .number(.int)), 65 | .init(name: "object", type: .text), 66 | ])) 67 | XCTAsserNotEqualFieldType(.unnamedObject([ 68 | .init(name: "object", type: .text), 69 | .init(name: "int", type: .number(.int)), 70 | ]), .unnamedObject([ 71 | .init(name: "int", type: .text), 72 | .init(name: "object", type: .text), 73 | ])) 74 | XCTAsserNotEqualFieldType(.unnamedObject([ 75 | .init(name: "object", type: .text), 76 | .init(name: "integer", type: .number(.int)), 77 | ]), .unnamedObject([ 78 | .init(name: "int", type: .number(.int)), 79 | .init(name: "object", type: .text), 80 | ])) 81 | XCTAsserNotEqualFieldType(.unnamedObject([ 82 | .init(name: "object", type: .text), 83 | ]), .unnamedObject([ 84 | .init(name: "int", type: .number(.int)), 85 | .init(name: "object", type: .text), 86 | ])) 87 | XCTAsserNotEqualFieldType(.unnamedObject([.init(name: "object", type: .text)]), .unnamedObject([.init(name: "text", type: .text)])) 88 | XCTAsserNotEqualFieldType(.unnamedObject([.init(name: "object", type: .text)]), .text) 89 | } 90 | 91 | func testEqualTypeEnum() { 92 | XCTAsserEqualFieldType(.unnamedEnum([]), .unnamedEnum([])) 93 | XCTAsserEqualFieldType(.unnamedEnum([.text]), .unnamedEnum([.text])) 94 | XCTAsserEqualFieldType(.unnamedEnum([ 95 | .text, 96 | .number(.int), 97 | ]), .unnamedEnum([ 98 | .number(.int), 99 | .text, 100 | ])) 101 | XCTAsserNotEqualFieldType(.unnamedEnum([ 102 | .text, 103 | .number(.int), 104 | ]), .unnamedEnum([ 105 | .text 106 | ])) 107 | XCTAsserNotEqualFieldType(.unnamedEnum([.text]), .unnamedEnum([.number(.int)])) 108 | XCTAsserNotEqualFieldType(.unnamedEnum([.text]), .text) 109 | } 110 | 111 | func testString() throws { 112 | let type = parser.decode("Simple string") 113 | XCTAssertEqual(type, FieldType.text) 114 | } 115 | 116 | func testInt() throws { 117 | let type = parser.decode(10) 118 | XCTAssertEqual(type, FieldType.number(.int)) 119 | } 120 | 121 | func testUInt() throws { 122 | let type = parser.decode(.number(.uint(10))) 123 | XCTAssertEqual(type, FieldType.number(.int)) 124 | } 125 | 126 | func testDouble() throws { 127 | let type = parser.decode(10.0) 128 | XCTAssertEqual(type, FieldType.number(.double)) 129 | } 130 | 131 | func testFloat() throws { 132 | let type = parser.decode(.number(.float(10.0))) 133 | XCTAssertEqual(type, FieldType.number(.float)) 134 | } 135 | 136 | func testBool() throws { 137 | let type = parser.decode(true) 138 | XCTAssertEqual(type, FieldType.number(.bool)) 139 | } 140 | 141 | private func data(from jsonValue: String) throws -> Any { 142 | let data = "{\"value\":\(jsonValue)}".data(using: .utf8)! 143 | 144 | let jsonObj = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] 145 | return jsonObj["value"]! 146 | } 147 | 148 | func testEmptyObject() throws { 149 | let type = parser.decode([:]) 150 | XCTAssertEqual(type, FieldType.unnamedObject([])) 151 | } 152 | 153 | func testEmptyArray() throws { 154 | let type = parser.decode([]) 155 | XCTAssertEqual(type, FieldType.list(.unnamedUnknown)) 156 | } 157 | 158 | func testStringArray() throws { 159 | let type = parser.decode([ 160 | "Test", 161 | "123" 162 | ]) 163 | XCTAssertEqual(type, FieldType.list(.text)) 164 | } 165 | 166 | func testIntArray() throws { 167 | let type = parser.decode([ 168 | 10, 169 | 20 170 | ]) 171 | XCTAssertEqual(type, FieldType.list(.number(.int))) 172 | } 173 | 174 | func testOptionalStringArray() throws { 175 | let type = parser.decode([ 176 | "Test", 177 | nil 178 | ]) 179 | XCTAssertEqual(type, FieldType.list(.optional(.text))) 180 | } 181 | 182 | func testNullArray() throws { 183 | let type = parser.decode([ 184 | nil 185 | ]) 186 | XCTAssertEqual(type, FieldType.list(.optional(.unnamedUnknown))) 187 | } 188 | 189 | func testSingleFieldObject() throws { 190 | let type = parser.decode([ 191 | "string": "Test" 192 | ]) 193 | XCTAssertEqual(type, FieldType.unnamedObject([ObjectField(name: "string", type: .text)])) 194 | } 195 | 196 | func testThreeFieldsObject() throws { 197 | let type = parser.decode([ 198 | "string": "Test", 199 | "integer": 123, 200 | "object": [:] 201 | ]) 202 | let expectedType: FieldType = .unnamedObject([ 203 | .init(name: "string", type: .text), 204 | .init(name: "integer", type: .number(.int)), 205 | .init(name: "object", type: .unnamedObject([])) 206 | ]) 207 | XCTAssertEqual(type, expectedType) 208 | } 209 | 210 | func testArrayOfEmptyObject() throws { 211 | let type = parser.decode([ 212 | [:], 213 | [:] 214 | ]) 215 | XCTAssertEqual(type, FieldType.list(.unnamedObject([]))) 216 | } 217 | 218 | func testArrayOfEmptyOptionalObject() throws { 219 | let type = parser.decode([ 220 | [:], 221 | nil 222 | ]) 223 | XCTAssertEqual(type, FieldType.list(.optional(.unnamedObject([])))) 224 | } 225 | 226 | func testArrayOfMixedIntFloatAndDouble() throws { 227 | let type = parser.decode([ 228 | 10, 229 | 10.0, 230 | JSON.number(.float(10)) 231 | ]) 232 | XCTAssertEqual(type, FieldType.list(.number(.double)), "Mixed number types with at least one Double should be merged to Double") 233 | } 234 | 235 | func testArrayOfMixedIntAndFloat() throws { 236 | let type = parser.decode([ 237 | 10, 238 | .number(.float(10.0)) 239 | ]) 240 | let expectedResult: FieldType = .list( 241 | .number(.float) 242 | ) 243 | XCTAssertEqual(type, expectedResult, "Mixed number types with Int and Float should be merged to Float") 244 | } 245 | 246 | func testArrayOfMixedBoolAndDouble() throws { 247 | let type = parser.decode([ 248 | 10.0, 249 | true 250 | ]) 251 | let expectedResult: FieldType = .list( 252 | .unnamedEnum([ 253 | .number(.bool), 254 | .number(.double) 255 | ]) 256 | ) 257 | XCTAssertEqual(type, expectedResult, "Mixed number types with Bool and Double should be merged to enum containing Bool and Double numbers") 258 | } 259 | 260 | func testArrayOfMixedBoolIntAndDouble() throws { 261 | let type = parser.decode([ 262 | 10.0, 263 | true, 264 | 10 265 | ]) 266 | let expectedResult: FieldType = .list( 267 | .unnamedEnum([ 268 | .number(.bool), 269 | .number(.double) 270 | ]) 271 | ) 272 | XCTAssertEqual(type, expectedResult, "Mixed number types with Bool, Int and Double should be merged to enum containing Bool and Double numbers") 273 | } 274 | 275 | func testArrayOfObjectsWithMissingField() throws { 276 | let type1 = parser.decode([ 277 | [:], 278 | ["string": "Test"] 279 | ]) 280 | let expectedResult: FieldType = .list( 281 | .unnamedObject([ 282 | .init(name: "string", type: .optional(.text)) 283 | ]) 284 | ) 285 | XCTAssertEqual(type1, expectedResult, "List of objects where in one object a field is missing, should result in a object with an optional field type") 286 | 287 | let type2 = parser.decode([ 288 | ["string": "Test"], 289 | [:] 290 | ]) 291 | XCTAssertEqual(type2, expectedResult, "List of objects where in one object a field is missing, should result in a object with an optional field type") 292 | } 293 | 294 | func testArrayOfObjectsWithMixedTypesAndOptional() throws { 295 | let type = parser.decode([ 296 | ["mixed": nil], 297 | ["mixed": "string"], 298 | ["mixed": .number(.double(10))] 299 | ]) 300 | let expectedResult: FieldType = .list( 301 | .unnamedObject([ 302 | .init(name: "mixed", type: .optional( 303 | .unnamedEnum([ 304 | .text, 305 | .number(.double) 306 | ]) 307 | )) 308 | ]) 309 | ) 310 | XCTAssertEqual(type, expectedResult) 311 | } 312 | 313 | func testArrayObjectWithArrayFieldOfUnknownTypeAndStrings() throws { 314 | let type = parser.decode([ 315 | ["mixed": []], 316 | ["mixed": ["String"]] 317 | ]) 318 | let expectedResult: FieldType = .list( 319 | .unnamedObject([ 320 | .init(name: "mixed", type: .list(.text)) 321 | ]) 322 | ) 323 | XCTAssertEqual(type, expectedResult) 324 | } 325 | 326 | func testArrayObjectWithArrayFieldOfIntsStringsAndDoubles() throws { 327 | let type = parser.decode([ 328 | ["mixed": [.number(.int(10))]], 329 | ["mixed": ["String"]], 330 | ["mixed": [.number(.double(10))]] 331 | ]) 332 | let expectedResult: FieldType = .list( 333 | .unnamedObject([ 334 | .init(name: "mixed", type: .list( 335 | .unnamedEnum([ 336 | .number(.double), 337 | .text 338 | ]) 339 | )) 340 | ]) 341 | ) 342 | XCTAssertEqual(type, expectedResult) 343 | } 344 | 345 | func testArrayObjectWithMixedFieldOfMixedArraysAndInt() throws { 346 | let type = parser.decode([ 347 | ["mixed": ["String"]], 348 | ["mixed": .number(.int(10))], 349 | ["mixed": [.number(.double(10))]], 350 | ["mixed": [nil]] 351 | ]) 352 | let expectedResult: FieldType = .list( 353 | .unnamedObject([ 354 | .init(name: "mixed", type: .unnamedEnum([ 355 | .list( 356 | .optional( 357 | .unnamedEnum([ 358 | .text, 359 | .number(.double) 360 | ]) 361 | ) 362 | ), 363 | .number(.int) 364 | ])) 365 | ]) 366 | ) 367 | XCTAssertEqual(type, expectedResult) 368 | } 369 | 370 | 371 | func testTransformAllFieldsToOptional() { 372 | let type: FieldType = .unnamedObject([ 373 | .init(name: "innerObject", type: .unnamedObject([ 374 | .init(name: "innerText", type: .text), 375 | ])), 376 | .init(name: "list", type: .list( 377 | .unnamedObject([ 378 | .init(name: "insideList", type: .text) 379 | ]) 380 | ) 381 | ), 382 | .init(name: "text", type: .text), 383 | .init(name: "number", type: .number(.int)), 384 | .init(name: "enum", type: .unnamedEnum([ 385 | .unnamedObject([ 386 | .init(name: "textInEnum", type: .text) 387 | ]), 388 | .number(.float) 389 | ]) 390 | ), 391 | .init(name: "unknown", type: .unnamedUnknown), 392 | .init(name: "optionalText", type: .optional(.text)), 393 | .init(name: "optionalObject", type: .optional(.unnamedObject([ 394 | .init(name: "textInsideOptionalObject", type: .text) 395 | ]))) 396 | ]) 397 | 398 | let expectedResult: FieldType = .unnamedObject([ 399 | .init(name: "innerObject", type: .optional(.unnamedObject([ 400 | .init(name: "innerText", type: .optional(.text)), 401 | ]))), 402 | .init(name: "list", type: .optional(.list( 403 | .unnamedObject([ 404 | .init(name: "insideList", type: .optional(.text)) 405 | ]) 406 | )) 407 | ), 408 | .init(name: "text", type: .optional(.text)), 409 | .init(name: "number", type: .optional(.number(.int))), 410 | .init(name: "enum", type: .optional( 411 | .unnamedEnum([ 412 | .unnamedObject([ 413 | .init(name: "textInEnum", type: .optional(.text)) 414 | ]), 415 | .number(.float) 416 | ]) 417 | )), 418 | .init(name: "unknown", type: .optional(.unnamedUnknown)), 419 | .init(name: "optionalText", type: .optional(.text)), 420 | .init(name: "optionalObject", type: .optional(.unnamedObject([ 421 | .init(name: "textInsideOptionalObject", type: .optional(.text)) 422 | ]))) 423 | ]) 424 | 425 | let transformedResult = ModelGenerator.transformAllFieldsToOptional(type) 426 | XCTAssertEqual(expectedResult, transformedResult) 427 | } 428 | 429 | func testTransformAllFieldsToOptionalWithToplevelList() { 430 | let type: FieldType = .list(.text) 431 | let expectedResult: FieldType = .list(.text) 432 | 433 | let transformedResult = ModelGenerator.transformAllFieldsToOptional(type) 434 | XCTAssertEqual(expectedResult, transformedResult) 435 | } 436 | 437 | func testTransformAllFieldsToOptionalWithToplevelEnum() { 438 | let type: FieldType = .unnamedEnum([.text, .number(.int)]) 439 | let expectedResult: FieldType = .unnamedEnum([.text, .number(.int)]) 440 | 441 | let transformedResult = ModelGenerator.transformAllFieldsToOptional(type) 442 | XCTAssertEqual(expectedResult, transformedResult) 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /Tests/ReverseJsonCoreTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension ModelGeneratorTest { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__ModelGeneratorTest = [ 9 | ("testArrayObjectWithArrayFieldOfIntsStringsAndDoubles", testArrayObjectWithArrayFieldOfIntsStringsAndDoubles), 10 | ("testArrayObjectWithArrayFieldOfUnknownTypeAndStrings", testArrayObjectWithArrayFieldOfUnknownTypeAndStrings), 11 | ("testArrayObjectWithMixedFieldOfMixedArraysAndInt", testArrayObjectWithMixedFieldOfMixedArraysAndInt), 12 | ("testArrayOfEmptyObject", testArrayOfEmptyObject), 13 | ("testArrayOfEmptyOptionalObject", testArrayOfEmptyOptionalObject), 14 | ("testArrayOfMixedBoolAndDouble", testArrayOfMixedBoolAndDouble), 15 | ("testArrayOfMixedBoolIntAndDouble", testArrayOfMixedBoolIntAndDouble), 16 | ("testArrayOfMixedIntAndFloat", testArrayOfMixedIntAndFloat), 17 | ("testArrayOfMixedIntFloatAndDouble", testArrayOfMixedIntFloatAndDouble), 18 | ("testArrayOfObjectsWithMissingField", testArrayOfObjectsWithMissingField), 19 | ("testArrayOfObjectsWithMixedTypesAndOptional", testArrayOfObjectsWithMixedTypesAndOptional), 20 | ("testBool", testBool), 21 | ("testDouble", testDouble), 22 | ("testEmptyArray", testEmptyArray), 23 | ("testEmptyObject", testEmptyObject), 24 | ("testEqualNumberTypes", testEqualNumberTypes), 25 | ("testEqualTypeEnum", testEqualTypeEnum), 26 | ("testEqualTypeList", testEqualTypeList), 27 | ("testEqualTypeNumber", testEqualTypeNumber), 28 | ("testEqualTypeObject", testEqualTypeObject), 29 | ("testEqualTypeOptional", testEqualTypeOptional), 30 | ("testEqualTypeText", testEqualTypeText), 31 | ("testEqualTypeUnknown", testEqualTypeUnknown), 32 | ("testFloat", testFloat), 33 | ("testInt", testInt), 34 | ("testIntArray", testIntArray), 35 | ("testNullArray", testNullArray), 36 | ("testOptionalStringArray", testOptionalStringArray), 37 | ("testSingleFieldObject", testSingleFieldObject), 38 | ("testString", testString), 39 | ("testStringArray", testStringArray), 40 | ("testThreeFieldsObject", testThreeFieldsObject), 41 | ("testTransformAllFieldsToOptional", testTransformAllFieldsToOptional), 42 | ("testTransformAllFieldsToOptionalWithToplevelEnum", testTransformAllFieldsToOptionalWithToplevelEnum), 43 | ("testTransformAllFieldsToOptionalWithToplevelList", testTransformAllFieldsToOptionalWithToplevelList), 44 | ("testUInt", testUInt), 45 | ] 46 | } 47 | 48 | public func __allTests() -> [XCTestCaseEntry] { 49 | return [ 50 | testCase(ModelGeneratorTest.__allTests__ModelGeneratorTest), 51 | ] 52 | } 53 | #endif 54 | -------------------------------------------------------------------------------- /Tests/ReverseJsonModelExportTests/JsonToModelTest.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import ReverseJsonCore 4 | @testable import ReverseJsonModelExport 5 | 6 | class JsonToModelTest: XCTestCase { 7 | 8 | func testSimpleString() { 9 | let expected: FieldType = .text 10 | var type = try! FieldType(json: "string") 11 | XCTAssertEqual(type, expected) 12 | type = try! FieldType(json: [ 13 | "type": "string", 14 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 15 | ]) 16 | XCTAssertEqual(type, expected) 17 | } 18 | 19 | func testSimpleInt() { 20 | let expected: FieldType = .number(.int) 21 | var type = try! FieldType(json: "int") 22 | XCTAssertEqual(type, expected) 23 | type = try! FieldType(json: [ 24 | "type": "int", 25 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 26 | ]) 27 | XCTAssertEqual(type, expected) 28 | } 29 | 30 | func testOptionalInt() { 31 | let expected: FieldType = .optional(.number(.int)) 32 | var type = try! FieldType(json: ["type": "int?"]) 33 | XCTAssertEqual(type, expected) 34 | type = try! FieldType(json: [ 35 | "type": "int", 36 | "isOptional": true 37 | ]) 38 | XCTAssertEqual(type, expected) 39 | } 40 | 41 | func testSimpleFloat() { 42 | let expected: FieldType = .number(.float) 43 | var type = try! FieldType(json: "float") 44 | XCTAssertEqual(type, expected) 45 | type = try! FieldType(json: [ 46 | "type": "float", 47 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 48 | ]) 49 | XCTAssertEqual(type, expected) 50 | } 51 | 52 | func testSimpleDouble() { 53 | let expected: FieldType = .number(.double) 54 | var type = try! FieldType(json: "double") 55 | XCTAssertEqual(type, expected) 56 | type = try! FieldType(json: [ 57 | "type": "double", 58 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 59 | ]) 60 | XCTAssertEqual(type, expected) 61 | } 62 | 63 | func testSimpleBool() { 64 | let expected: FieldType = .number(.bool) 65 | var type = try! FieldType(json: "bool") 66 | XCTAssertEqual(type, expected) 67 | type = try! FieldType(json: [ 68 | "type": "bool", 69 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 70 | ]) 71 | XCTAssertEqual(type, expected) 72 | } 73 | 74 | func testEmptyObject() { 75 | let expected: FieldType = .unnamedObject([]) 76 | let type = try! FieldType(json: "object") 77 | XCTAssertEqual(type, expected) 78 | } 79 | 80 | func testNamedEmptyObject() { 81 | let expected: FieldType = .object(name: "Object", []) 82 | let type = try! FieldType(json: [ 83 | "type": "object", 84 | "name": "Object" 85 | ]) 86 | XCTAssertEqual(type, expected) 87 | } 88 | 89 | func testUnknown() { 90 | let expected: FieldType = .unnamedUnknown 91 | let type = try! FieldType(json: "any") 92 | XCTAssertEqual(type, expected) 93 | } 94 | 95 | func testNamedEmptyUnknown() { 96 | let expected: FieldType = .unknown(name: "Unknown") 97 | let type = try! FieldType(json: [ 98 | "type": "any", 99 | "name": "Unknown" 100 | ]) 101 | XCTAssertEqual(type, expected) 102 | } 103 | 104 | 105 | func testTextList() { 106 | let expected: FieldType = .list(.text) 107 | let type = try! FieldType(json: [ 108 | "type": "list", 109 | "content": "string" 110 | ]) 111 | XCTAssertEqual(type, expected) 112 | } 113 | 114 | func testListWithoutContent() { 115 | let expected: FieldType = .list(.unnamedUnknown) 116 | let type = try! FieldType(json: [ 117 | "type": "list", 118 | ]) 119 | XCTAssertEqual(type, expected) 120 | } 121 | 122 | 123 | func testListOfTextList() { 124 | let expected: FieldType = .list(.list(.text)) 125 | let type = try! FieldType(json: [ 126 | "type": "list", 127 | "content": [ 128 | "type": "list", 129 | "content": "string" 130 | ] 131 | ]) 132 | XCTAssertEqual(type, expected) 133 | } 134 | 135 | func testOptionalUnknown() { 136 | let expected: FieldType = .optional(.unnamedUnknown) 137 | let type = try! FieldType(json: "any?") 138 | XCTAssertEqual(type, expected) 139 | } 140 | 141 | func testListOfUnknown() { 142 | let expected: FieldType = .list(.unnamedUnknown) 143 | let type = try! FieldType(json: [ 144 | "type": "list", 145 | "content": "any" 146 | ]) 147 | XCTAssertEqual(type, expected) 148 | } 149 | 150 | func testOptionalText() { 151 | let expected: FieldType = .optional(.text) 152 | let type = try! FieldType(json: "string?") 153 | XCTAssertEqual(type, expected) 154 | } 155 | 156 | func testListOfEmptyObject() { 157 | let expected: FieldType = .list(.unnamedObject([])) 158 | let type = try! FieldType(json: [ 159 | "type": "list", 160 | "content": "object" 161 | ]) 162 | XCTAssertEqual(type, expected) 163 | } 164 | 165 | func testObjectWithSingleTextField() { 166 | let expected: FieldType = .unnamedObject([.init(name: "text", type: .text)]) 167 | let type = try! FieldType(json: [ 168 | "type": "object", 169 | "properties": [ 170 | "text": "string" 171 | ] 172 | ]) 173 | XCTAssertEqual(type, expected) 174 | } 175 | 176 | func testObjectWithFieldContainingListOfText() { 177 | let expected: FieldType = .unnamedObject([.init(name: "texts", type: .list(.text))]) 178 | let type = try! FieldType(json: [ 179 | "type": "object", 180 | "properties": [ 181 | "texts": [ 182 | "type": "list", 183 | "content": "string" 184 | ] 185 | ] 186 | ]) 187 | XCTAssertEqual(type, expected) 188 | } 189 | 190 | func testObjectWithTwoSimpleFields() { 191 | let expected: FieldType = .unnamedObject([ 192 | .init(name: "number", type: .number(.double)), 193 | .init(name: "texts", type: .list(.text)) 194 | ]) 195 | let type = try! FieldType(json: [ 196 | "type": "object", 197 | "properties": [ 198 | "number": "double", 199 | "texts": [ 200 | "type": "list", 201 | "content": "string" 202 | ] 203 | ] 204 | ]) 205 | XCTAssertEqual(type, expected) 206 | } 207 | 208 | func testObjectWithOneFieldWithSubDeclaration() { 209 | let expected: FieldType = .unnamedObject([ 210 | .init(name: "subObject", type: .unnamedObject([])) 211 | ]) 212 | let type = try! FieldType(json: [ 213 | "type": "object", 214 | "properties": [ 215 | "subObject": "object" 216 | ] 217 | ]) 218 | XCTAssertEqual(type, expected) 219 | } 220 | 221 | func testEnumWithOneCase() { 222 | let expected: FieldType = .unnamedEnum([.text]) 223 | let type = try! FieldType(json: [ 224 | "type": "any", 225 | "of": [ 226 | "string" 227 | ] 228 | ]) 229 | XCTAssertEqual(type, expected) 230 | } 231 | 232 | func testEnumWithTwoCases() { 233 | let expected: FieldType = .unnamedEnum([.text, .number(.int)]) 234 | let type = try! FieldType(json: [ 235 | "type": "any", 236 | "of": [ 237 | "int", 238 | "string" 239 | ] 240 | ]) 241 | XCTAssertEqual(type, expected) 242 | } 243 | 244 | func testEnumWithOneSubDeclarationCase() { 245 | let expected: FieldType = .unnamedEnum([.unnamedObject([])]) 246 | let type = try! FieldType(json: [ 247 | "type": "any", 248 | "of": [ 249 | "object" 250 | ] 251 | ]) 252 | XCTAssertEqual(type, expected) 253 | } 254 | 255 | func testInvalidType() { 256 | do { 257 | let _ = try FieldType(json: "invalid") 258 | XCTFail("Expected error") 259 | } catch let FieldType.JSONConvertionError.invalidType(type) { 260 | XCTAssertEqual("invalid", type) 261 | } catch { 262 | XCTFail("Wrong error") 263 | } 264 | } 265 | 266 | func testUnexpectedJSON() { 267 | do { 268 | let _ = try FieldType(json: 10) 269 | XCTFail("Expected error") 270 | } catch let FieldType.JSONConvertionError.unexpectedPropertyType(json) { 271 | XCTAssertEqual(10, json) 272 | } catch { 273 | XCTFail("Wrong error") 274 | } 275 | } 276 | 277 | func testMissingType() { 278 | do { 279 | let _ = try FieldType(json: [:]) 280 | XCTFail("Expected error") 281 | } catch let FieldType.JSONConvertionError.missingParameter(parameter) { 282 | XCTAssertEqual("type", parameter) 283 | } catch { 284 | XCTFail("Wrong error") 285 | } 286 | } 287 | 288 | func testReference() { 289 | let expected: FieldType = .object(name: "Test1", [ 290 | .init(name: "number", type: .number(.double)), 291 | .init(name: "texts", type: .list(.text)) 292 | ]) 293 | let type = try! FieldType(json: [ 294 | "definitions": [ 295 | "Test1": [ 296 | "type": "object", 297 | "properties": [ 298 | "number": "double", 299 | "texts": [ 300 | "type": "list", 301 | "content": "string" 302 | ] 303 | ] 304 | ] 305 | ], 306 | "$ref": "#/definitions/Test1" 307 | ]) 308 | XCTAssertEqual(type, expected) 309 | } 310 | 311 | } 312 | -------------------------------------------------------------------------------- /Tests/ReverseJsonModelExportTests/ModelExportTranslatorTest.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import ReverseJsonCore 4 | import CoreJSON 5 | import CoreJSONLiterals 6 | @testable import ReverseJsonModelExport 7 | 8 | class ModelExportTranslatorTest: XCTestCase { 9 | 10 | func testSuccessfullSchemaCheck() { 11 | let json: JSON = [ 12 | "type": "text", 13 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 14 | ] 15 | XCTAssertTrue(ModelExportTranslator.isSchema(json)) 16 | } 17 | 18 | func testFailedSchemaCheck1() { 19 | let json: JSON = [ 20 | "some": "other", 21 | "json": 123 22 | ] 23 | XCTAssertFalse(ModelExportTranslator.isSchema(json)) 24 | } 25 | 26 | func testFailedSchemaCheck2() { 27 | let json: JSON = 123 28 | XCTAssertFalse(ModelExportTranslator.isSchema(json)) 29 | } 30 | 31 | func testPrettyTranslation() { 32 | var translator = ModelExportTranslator() 33 | translator.isPrettyPrinted = true 34 | let result = translator.translate(.text, name: "Test") 35 | XCTAssertTrue(result.first?.content.contains("\n \"type\"") ?? false && result.first?.content.contains("\n \"$schema\"") ?? false) 36 | } 37 | 38 | func testNonPrettyTranslation() { 39 | var translator = ModelExportTranslator() 40 | translator.isPrettyPrinted = false 41 | let result = translator.translate(.text, name: "Test") 42 | XCTAssertTrue(result.first?.content.contains("\"type\"") ?? false && result.first?.content.contains("\"$schema\"") ?? false) 43 | XCTAssertFalse(result.first?.content.contains("\n \"type\"") ?? false || result.first?.content.contains("\n \"$schema\"") ?? false) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Tests/ReverseJsonModelExportTests/ModelToJsonTest.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import ReverseJsonCore 4 | @testable import ReverseJsonModelExport 5 | 6 | class ModelToJsonTest: XCTestCase { 7 | 8 | func testSimpleString() { 9 | let type: FieldType = .text 10 | let json = type.toJSON(isRoot: false) 11 | XCTAssertEqual("string", json) 12 | let rootJson = type.toJSON(isRoot: true) 13 | XCTAssertEqual([ 14 | "type": json, 15 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 16 | ], rootJson) 17 | } 18 | 19 | func testSimpleInt() { 20 | let type: FieldType = .number(.int) 21 | let json = type.toJSON(isRoot: false) 22 | XCTAssertEqual("int", json) 23 | let rootJson = type.toJSON(isRoot: true) 24 | XCTAssertEqual([ 25 | "type": json, 26 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 27 | ], rootJson) 28 | } 29 | 30 | func testSimpleFloat() { 31 | let type: FieldType = .number(.float) 32 | let json = type.toJSON(isRoot: false) 33 | XCTAssertEqual("float", json) 34 | let rootJson = type.toJSON(isRoot: true) 35 | XCTAssertEqual([ 36 | "type": json, 37 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 38 | ], rootJson) 39 | } 40 | 41 | func testSimpleDouble() { 42 | let type: FieldType = .number(.double) 43 | let json = type.toJSON(isRoot: false) 44 | XCTAssertEqual("double", json) 45 | let rootJson = type.toJSON(isRoot: true) 46 | XCTAssertEqual([ 47 | "type": json, 48 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 49 | ], rootJson) 50 | } 51 | 52 | func testSimpleBool() { 53 | let type: FieldType = .number(.bool) 54 | let json = type.toJSON(isRoot: false) 55 | XCTAssertEqual("bool", json) 56 | let rootJson = type.toJSON(isRoot: true) 57 | XCTAssertEqual([ 58 | "type": json, 59 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 60 | ], rootJson) 61 | } 62 | 63 | func testEmptyObject() { 64 | let type: FieldType = .unnamedObject([]) 65 | let json = type.toJSON(isRoot: false) 66 | XCTAssertEqual("object", json) 67 | let rootJson = type.toJSON(isRoot: true) 68 | XCTAssertEqual([ 69 | "type": json, 70 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 71 | ], rootJson) 72 | } 73 | 74 | func testNamedEmptyObject() { 75 | let type: FieldType = .object(name: "Object", []) 76 | let json = type.toJSON() 77 | XCTAssertEqual([ 78 | "type": "object", 79 | "name": "Object" 80 | ], json) 81 | } 82 | 83 | func testEmptyEnum() { 84 | let type: FieldType = .unnamedEnum([]) 85 | let json = type.toJSON(isRoot: false) 86 | XCTAssertEqual("any", json) 87 | let rootJson = type.toJSON(isRoot: true) 88 | XCTAssertEqual([ 89 | "type": json, 90 | "$schema": .string(ModelExportTranslator.schemaIdentifier) 91 | ], rootJson) 92 | } 93 | 94 | func testNamedEmptyEnum() { 95 | let type: FieldType = .enum(name: "Enum", []) 96 | let json = type.toJSON() 97 | XCTAssertEqual([ 98 | "type": "any", 99 | "name": "Enum" 100 | ], json) 101 | } 102 | 103 | 104 | func testTextList() { 105 | let type: FieldType = .list(.text) 106 | let json = type.toJSON() 107 | XCTAssertEqual([ 108 | "type": "list", 109 | "content": "string" 110 | ], json) 111 | } 112 | 113 | func testListOfTextList() { 114 | let type: FieldType = .list(.list(.text)) 115 | let json = type.toJSON() 116 | XCTAssertEqual([ 117 | "type": "list", 118 | "content": [ 119 | "type": "list", 120 | "content": "string" 121 | ] 122 | ], json) 123 | } 124 | 125 | func testUnknownType() { 126 | let type: FieldType = .unnamedUnknown 127 | let json = type.toJSON() 128 | XCTAssertEqual("any", json) 129 | } 130 | 131 | func testOptionalUnknown() { 132 | let type: FieldType = .optional(.unnamedUnknown) 133 | let json = type.toJSON() 134 | XCTAssertEqual("any?", json) 135 | } 136 | 137 | func testListOfUnknown() { 138 | let type: FieldType = .list(.unnamedUnknown) 139 | let json = type.toJSON() 140 | XCTAssertEqual([ 141 | "type": "list", 142 | "content": "any" 143 | ], json) 144 | } 145 | 146 | func testOptionalText() { 147 | let type: FieldType = .optional(.text) 148 | let json = type.toJSON() 149 | XCTAssertEqual("string?", json) 150 | } 151 | 152 | func testListOfEmptyObject() { 153 | let type: FieldType = .list(.unnamedObject([])) 154 | let json = type.toJSON() 155 | XCTAssertEqual([ 156 | "type": "list", 157 | "content": "object" 158 | ], json) 159 | } 160 | 161 | func testObjectWithSingleTextField() { 162 | let type: FieldType = .unnamedObject([.init(name: "text", type: .text)]) 163 | let json = type.toJSON() 164 | XCTAssertEqual([ 165 | "type": "object", 166 | "properties": [ 167 | "text": "string" 168 | ] 169 | ], json) 170 | } 171 | 172 | func testObjectWithFieldContainingListOfText() { 173 | let type: FieldType = .unnamedObject([.init(name: "texts", type: .list(.text))]) 174 | let json = type.toJSON() 175 | XCTAssertEqual([ 176 | "type": "object", 177 | "properties": [ 178 | "texts": [ 179 | "type": "list", 180 | "content": "string" 181 | ] 182 | ] 183 | ], json) 184 | } 185 | 186 | func testObjectWithTwoSimpleFields() { 187 | let type: FieldType = .unnamedObject([ 188 | .init(name: "number", type: .number(.double)), 189 | .init(name: "texts", type: .list(.text)) 190 | ]) 191 | let json = type.toJSON() 192 | XCTAssertEqual([ 193 | "type": "object", 194 | "properties": [ 195 | "number": "double", 196 | "texts": [ 197 | "type": "list", 198 | "content": "string" 199 | ] 200 | ] 201 | ], json) 202 | } 203 | 204 | func testObjectWithOneFieldWithSubDeclaration() { 205 | let type: FieldType = .unnamedObject([ 206 | .init(name: "subObject", type: .unnamedObject([])) 207 | ]) 208 | let json = type.toJSON() 209 | XCTAssertEqual([ 210 | "type": "object", 211 | "properties": [ 212 | "subObject": "object" 213 | ] 214 | ], json) 215 | } 216 | 217 | func testEnumWithOneCase() { 218 | let type: FieldType = .unnamedEnum([.text]) 219 | let json = type.toJSON() 220 | XCTAssertEqual([ 221 | "type": "any", 222 | "of": [ 223 | "string" 224 | ] 225 | ], json) 226 | } 227 | 228 | func testEnumWithTwoCases() { 229 | let type: FieldType = .unnamedEnum([.text, .number(.int)]) 230 | let json = type.toJSON() 231 | XCTAssertEqual([ 232 | "type": "any", 233 | "of": [ 234 | "int", 235 | "string" 236 | ] 237 | ], json) 238 | } 239 | 240 | func testEnumWithOneSubDeclarationCase() { 241 | let type: FieldType = .unnamedEnum([.unnamedObject([])]) 242 | let json = type.toJSON() 243 | XCTAssertEqual([ 244 | "type": "any", 245 | "of": [ 246 | "object" 247 | ] 248 | ], json) 249 | } 250 | 251 | } 252 | -------------------------------------------------------------------------------- /Tests/ReverseJsonModelExportTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension JsonToModelTest { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__JsonToModelTest = [ 9 | ("testEmptyObject", testEmptyObject), 10 | ("testEnumWithOneCase", testEnumWithOneCase), 11 | ("testEnumWithOneSubDeclarationCase", testEnumWithOneSubDeclarationCase), 12 | ("testEnumWithTwoCases", testEnumWithTwoCases), 13 | ("testInvalidType", testInvalidType), 14 | ("testListOfEmptyObject", testListOfEmptyObject), 15 | ("testListOfTextList", testListOfTextList), 16 | ("testListOfUnknown", testListOfUnknown), 17 | ("testListWithoutContent", testListWithoutContent), 18 | ("testMissingType", testMissingType), 19 | ("testNamedEmptyObject", testNamedEmptyObject), 20 | ("testNamedEmptyUnknown", testNamedEmptyUnknown), 21 | ("testObjectWithFieldContainingListOfText", testObjectWithFieldContainingListOfText), 22 | ("testObjectWithOneFieldWithSubDeclaration", testObjectWithOneFieldWithSubDeclaration), 23 | ("testObjectWithSingleTextField", testObjectWithSingleTextField), 24 | ("testObjectWithTwoSimpleFields", testObjectWithTwoSimpleFields), 25 | ("testOptionalInt", testOptionalInt), 26 | ("testOptionalText", testOptionalText), 27 | ("testOptionalUnknown", testOptionalUnknown), 28 | ("testReference", testReference), 29 | ("testSimpleBool", testSimpleBool), 30 | ("testSimpleDouble", testSimpleDouble), 31 | ("testSimpleFloat", testSimpleFloat), 32 | ("testSimpleInt", testSimpleInt), 33 | ("testSimpleString", testSimpleString), 34 | ("testTextList", testTextList), 35 | ("testUnexpectedJSON", testUnexpectedJSON), 36 | ("testUnknown", testUnknown), 37 | ] 38 | } 39 | 40 | extension ModelExportTranslatorTest { 41 | // DO NOT MODIFY: This is autogenerated, use: 42 | // `swift test --generate-linuxmain` 43 | // to regenerate. 44 | static let __allTests__ModelExportTranslatorTest = [ 45 | ("testFailedSchemaCheck1", testFailedSchemaCheck1), 46 | ("testFailedSchemaCheck2", testFailedSchemaCheck2), 47 | ("testNonPrettyTranslation", testNonPrettyTranslation), 48 | ("testPrettyTranslation", testPrettyTranslation), 49 | ("testSuccessfullSchemaCheck", testSuccessfullSchemaCheck), 50 | ] 51 | } 52 | 53 | extension ModelToJsonTest { 54 | // DO NOT MODIFY: This is autogenerated, use: 55 | // `swift test --generate-linuxmain` 56 | // to regenerate. 57 | static let __allTests__ModelToJsonTest = [ 58 | ("testEmptyEnum", testEmptyEnum), 59 | ("testEmptyObject", testEmptyObject), 60 | ("testEnumWithOneCase", testEnumWithOneCase), 61 | ("testEnumWithOneSubDeclarationCase", testEnumWithOneSubDeclarationCase), 62 | ("testEnumWithTwoCases", testEnumWithTwoCases), 63 | ("testListOfEmptyObject", testListOfEmptyObject), 64 | ("testListOfTextList", testListOfTextList), 65 | ("testListOfUnknown", testListOfUnknown), 66 | ("testNamedEmptyEnum", testNamedEmptyEnum), 67 | ("testNamedEmptyObject", testNamedEmptyObject), 68 | ("testObjectWithFieldContainingListOfText", testObjectWithFieldContainingListOfText), 69 | ("testObjectWithOneFieldWithSubDeclaration", testObjectWithOneFieldWithSubDeclaration), 70 | ("testObjectWithSingleTextField", testObjectWithSingleTextField), 71 | ("testObjectWithTwoSimpleFields", testObjectWithTwoSimpleFields), 72 | ("testOptionalText", testOptionalText), 73 | ("testOptionalUnknown", testOptionalUnknown), 74 | ("testSimpleBool", testSimpleBool), 75 | ("testSimpleDouble", testSimpleDouble), 76 | ("testSimpleFloat", testSimpleFloat), 77 | ("testSimpleInt", testSimpleInt), 78 | ("testSimpleString", testSimpleString), 79 | ("testTextList", testTextList), 80 | ("testUnknownType", testUnknownType), 81 | ] 82 | } 83 | 84 | public func __allTests() -> [XCTestCaseEntry] { 85 | return [ 86 | testCase(JsonToModelTest.__allTests__JsonToModelTest), 87 | testCase(ModelExportTranslatorTest.__allTests__ModelExportTranslatorTest), 88 | testCase(ModelToJsonTest.__allTests__ModelToJsonTest), 89 | ] 90 | } 91 | #endif 92 | -------------------------------------------------------------------------------- /Tests/ReverseJsonObjcTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension ObjcModelTranslatorTest { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__ObjcModelTranslatorTest = [ 9 | ("testAtomicFieldsFlag", testAtomicFieldsFlag), 10 | ("testBoolDouble", testBoolDouble), 11 | ("testEmptyEnum", testEmptyEnum), 12 | ("testEmptyObject", testEmptyObject), 13 | ("testEnumWithOneCase", testEnumWithOneCase), 14 | ("testEnumWithOneCaseAndReverseMapping", testEnumWithOneCaseAndReverseMapping), 15 | ("testEnumWithTwoCases", testEnumWithTwoCases), 16 | ("testEnumWithTwoCasesAndReverseMapping", testEnumWithTwoCasesAndReverseMapping), 17 | ("testListOfEmptyObject", testListOfEmptyObject), 18 | ("testMutableFieldsFlag", testMutableFieldsFlag), 19 | ("testNamedEmptyEnum", testNamedEmptyEnum), 20 | ("testNamedEmptyObject", testNamedEmptyObject), 21 | ("testNamedEnumWithTwoCasesAndReverseMapping", testNamedEnumWithTwoCasesAndReverseMapping), 22 | ("testObjectWithDifferentFields", testObjectWithDifferentFields), 23 | ("testObjectWithFieldContainingListOfText", testObjectWithFieldContainingListOfText), 24 | ("testObjectWithFieldContainingListOfTextWithReverseMapper", testObjectWithFieldContainingListOfTextWithReverseMapper), 25 | ("testObjectWithFieldContainingOptionalListOfText", testObjectWithFieldContainingOptionalListOfText), 26 | ("testObjectWithOneFieldWithSubDeclaration", testObjectWithOneFieldWithSubDeclaration), 27 | ("testObjectWithSingleReservedTextField", testObjectWithSingleReservedTextField), 28 | ("testObjectWithSingleTextField", testObjectWithSingleTextField), 29 | ("testObjectWithSingleTextFieldAndReverseMapper", testObjectWithSingleTextFieldAndReverseMapper), 30 | ("testPrefixOption", testPrefixOption), 31 | ("testSimpleDouble", testSimpleDouble), 32 | ("testSimpleFloat", testSimpleFloat), 33 | ("testSimpleInt", testSimpleInt), 34 | ("testSimpleString", testSimpleString), 35 | ("testTextList", testTextList), 36 | ("testUnknownType", testUnknownType), 37 | ] 38 | } 39 | 40 | public func __allTests() -> [XCTestCaseEntry] { 41 | return [ 42 | testCase(ObjcModelTranslatorTest.__allTests__ObjcModelTranslatorTest), 43 | ] 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /Tests/ReverseJsonSwiftTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension SwiftTranslatorTest { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__SwiftTranslatorTest = [ 9 | ("testBoolDouble", testBoolDouble), 10 | ("testClassFlag", testClassFlag), 11 | ("testContiguousArrayFlag", testContiguousArrayFlag), 12 | ("testEmptyEnum", testEmptyEnum), 13 | ("testEmptyObject", testEmptyObject), 14 | ("testEnumWithOneCase", testEnumWithOneCase), 15 | ("testEnumWithOneSubDeclarationCase", testEnumWithOneSubDeclarationCase), 16 | ("testEnumWithTwoCases", testEnumWithTwoCases), 17 | ("testListOfEmptyObject", testListOfEmptyObject), 18 | ("testListOfTextList", testListOfTextList), 19 | ("testListOfUnknown", testListOfUnknown), 20 | ("testMutableFieldsFlag", testMutableFieldsFlag), 21 | ("testObjectWithFieldContainingListOfText", testObjectWithFieldContainingListOfText), 22 | ("testObjectWithOneFieldWithSubDeclaration", testObjectWithOneFieldWithSubDeclaration), 23 | ("testObjectWithSingleTextField", testObjectWithSingleTextField), 24 | ("testObjectWithTwoSimpleFields", testObjectWithTwoSimpleFields), 25 | ("testOptionalText", testOptionalText), 26 | ("testOptionalUnknown", testOptionalUnknown), 27 | ("testPublicFieldsFlag", testPublicFieldsFlag), 28 | ("testPublicTypeFlagWithEnum", testPublicTypeFlagWithEnum), 29 | ("testPublicTypeFlagWithObject", testPublicTypeFlagWithObject), 30 | ("testPublicTypeFlagWithTypealias", testPublicTypeFlagWithTypealias), 31 | ("testSimpleDouble", testSimpleDouble), 32 | ("testSimpleFloat", testSimpleFloat), 33 | ("testSimpleInt", testSimpleInt), 34 | ("testSimpleString", testSimpleString), 35 | ("testTextList", testTextList), 36 | ("testTranslatorCombination", testTranslatorCombination), 37 | ("testUnknownType", testUnknownType), 38 | ] 39 | } 40 | 41 | public func __allTests() -> [XCTestCaseEntry] { 42 | return [ 43 | testCase(SwiftTranslatorTest.__allTests__SwiftTranslatorTest), 44 | ] 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: true 4 | comment: 5 | behavior: default 6 | layout: header, diff 7 | require_changes: false 8 | coverage: 9 | precision: 2 10 | range: 11 | - 70.0 12 | - 100.0 13 | round: down 14 | status: 15 | changes: false 16 | patch: true 17 | project: true 18 | ignore: 19 | - "Tests" 20 | - "Packages" 21 | parsers: 22 | gcov: 23 | branch_detection: 24 | conditional: true 25 | loop: true 26 | macro: false 27 | method: false 28 | javascript: 29 | enable_partials: false 30 | 31 | -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # exit on failure 4 | 5 | function xcode_tests { 6 | ( 7 | set +e; 8 | set -x; # print commands executed; use subshell to avoid having to print 'set +x' to disable it 9 | 10 | which -s xcpretty 11 | XCPRETTY_INSTALLED=$? 12 | set -e; 13 | 14 | xcrun swift package generate-xcodeproj --enable-code-coverage 15 | XCB1='xcodebuild test -project ReverseJson.xcodeproj -scheme ReverseJson -destination "platform=OS X"' 16 | 17 | if [[ $TRAVIS || $XCPRETTY_INSTALLED == 0 ]]; then 18 | eval "set -o pipefail && ${XCB1} | xcpretty" && eval "set -o pipefail && ${XCB1} | xcpretty" && eval "set -o pipefail && ${XCB1} | xcpretty" 19 | else 20 | eval "${XCB1}" && eval "${XCB2}" && eval "${XCB3}" 21 | fi 22 | ) 23 | echo 24 | echo "Xcode tests passed" 25 | } 26 | 27 | function spm_tests { 28 | ( 29 | set -e; 30 | set -x; # print commands executed; use subshell to avoid having to print 'set +x' to disable it 31 | swift package clean && 32 | swift build && 33 | swift test 34 | ) 35 | echo 36 | echo "SPM tests passed" 37 | } 38 | 39 | function help { 40 | echo "Usage: $0 COMMANDS..." 41 | echo 42 | echo "Runs specific test suites." 43 | echo 44 | echo "COMMANDS:" 45 | echo " spm: runs tests via Swift Package Manager (Linux)" 46 | echo " xcode: runs tests via Xcode (OS X, iOS, tvOS)" 47 | echo " help: Displays this help" 48 | echo 49 | } 50 | 51 | for arg in "$@" 52 | do 53 | case $arg in 54 | "spm") spm_tests;; 55 | "xcode") xcode_tests;; 56 | "help") help;; 57 | esac 58 | done 59 | 60 | if [ $# == 0 ]; then 61 | help 62 | fi 63 | --------------------------------------------------------------------------------