├── .gitignore ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── JSONCopExample ├── JSONCopExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── JSONCopExample │ ├── Person.swift │ └── main.swift ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── console ├── cop └── setup ├── images ├── jsoncop-banner.png ├── jsoncop-demo.png └── run-script.png ├── jsoncop.gemspec ├── lib ├── jsoncop.rb └── jsoncop │ ├── analyzer.rb │ ├── command.rb │ ├── command │ ├── generate.rb │ ├── install.rb │ └── uninstall.rb │ ├── generator.rb │ ├── helper.rb │ ├── model │ ├── attribute.rb │ ├── model.rb │ └── transformer.rb │ └── version.rb └── spec ├── jsoncop_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.gem 11 | xcuserdata/ 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.3.1 5 | before_install: gem install bundler -v 1.12.5 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at stark.draven@gmail.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in jsoncop.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /JSONCopExample/JSONCopExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 72EE44CA1D90D523004729ED /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72EE44C91D90D523004729ED /* main.swift */; }; 11 | 72EE44D11D90D52E004729ED /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72EE44D01D90D52E004729ED /* Person.swift */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXCopyFilesBuildPhase section */ 15 | 72EE44C41D90D523004729ED /* Copy Files */ = { 16 | isa = PBXCopyFilesBuildPhase; 17 | buildActionMask = 2147483647; 18 | dstPath = /usr/share/man/man1/; 19 | dstSubfolderSpec = 0; 20 | files = ( 21 | ); 22 | name = "Copy Files"; 23 | runOnlyForDeploymentPostprocessing = 1; 24 | }; 25 | /* End PBXCopyFilesBuildPhase section */ 26 | 27 | /* Begin PBXFileReference section */ 28 | 72EE44C61D90D523004729ED /* JSONCopExample */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = JSONCopExample; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | 72EE44C91D90D523004729ED /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 30 | 72EE44D01D90D52E004729ED /* Person.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Person.swift; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 72EE44C31D90D523004729ED /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 72EE44BD1D90D523004729ED = { 45 | isa = PBXGroup; 46 | children = ( 47 | 72EE44C81D90D523004729ED /* JSONCopExample */, 48 | 72EE44C71D90D523004729ED /* Products */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | 72EE44C71D90D523004729ED /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 72EE44C61D90D523004729ED /* JSONCopExample */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | 72EE44C81D90D523004729ED /* JSONCopExample */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 72EE44C91D90D523004729ED /* main.swift */, 64 | 72EE44D01D90D52E004729ED /* Person.swift */, 65 | ); 66 | path = JSONCopExample; 67 | sourceTree = ""; 68 | }; 69 | /* End PBXGroup section */ 70 | 71 | /* Begin PBXNativeTarget section */ 72 | 72EE44C51D90D523004729ED /* JSONCopExample */ = { 73 | isa = PBXNativeTarget; 74 | buildConfigurationList = 72EE44CD1D90D523004729ED /* Build configuration list for PBXNativeTarget "JSONCopExample" */; 75 | buildPhases = ( 76 | FEA770172395CC0F2EF9F824 /* [JC] JSONCop Install Script */, 77 | 72EE44C21D90D523004729ED /* Sources */, 78 | 72EE44C31D90D523004729ED /* Frameworks */, 79 | 72EE44C41D90D523004729ED /* Copy Files */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | name = JSONCopExample; 86 | productName = JSONCopExample; 87 | productReference = 72EE44C61D90D523004729ED /* JSONCopExample */; 88 | productType = "com.apple.product-type.tool"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | 72EE44BE1D90D523004729ED /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | LastSwiftUpdateCheck = 0800; 97 | LastUpgradeCheck = 0800; 98 | ORGANIZATIONNAME = metamodel; 99 | TargetAttributes = { 100 | 72EE44C51D90D523004729ED = { 101 | CreatedOnToolsVersion = 8.0; 102 | ProvisioningStyle = Automatic; 103 | }; 104 | }; 105 | }; 106 | buildConfigurationList = 72EE44C11D90D523004729ED /* Build configuration list for PBXProject "JSONCopExample" */; 107 | compatibilityVersion = "Xcode 3.2"; 108 | developmentRegion = English; 109 | hasScannedForEncodings = 0; 110 | knownRegions = ( 111 | en, 112 | ); 113 | mainGroup = 72EE44BD1D90D523004729ED; 114 | productRefGroup = 72EE44C71D90D523004729ED /* Products */; 115 | projectDirPath = ""; 116 | projectRoot = ""; 117 | targets = ( 118 | 72EE44C51D90D523004729ED /* JSONCopExample */, 119 | ); 120 | }; 121 | /* End PBXProject section */ 122 | 123 | /* Begin PBXShellScriptBuildPhase section */ 124 | FEA770172395CC0F2EF9F824 /* [JC] JSONCop Install Script */ = { 125 | isa = PBXShellScriptBuildPhase; 126 | buildActionMask = 2147483647; 127 | files = ( 128 | ); 129 | inputPaths = ( 130 | ); 131 | name = "[JC] JSONCop Install Script"; 132 | outputPaths = ( 133 | ); 134 | runOnlyForDeploymentPostprocessing = 0; 135 | shellPath = /usr/bin/ruby; 136 | shellScript = "require 'jsoncop'\nEncoding.default_external = Encoding::UTF_8\nJSONCop::Command.run([\"generate\"])\n"; 137 | showEnvVarsInLog = 0; 138 | }; 139 | /* End PBXShellScriptBuildPhase section */ 140 | 141 | /* Begin PBXSourcesBuildPhase section */ 142 | 72EE44C21D90D523004729ED /* Sources */ = { 143 | isa = PBXSourcesBuildPhase; 144 | buildActionMask = 2147483647; 145 | files = ( 146 | 72EE44D11D90D52E004729ED /* Person.swift in Sources */, 147 | 72EE44CA1D90D523004729ED /* main.swift in Sources */, 148 | ); 149 | runOnlyForDeploymentPostprocessing = 0; 150 | }; 151 | /* End PBXSourcesBuildPhase section */ 152 | 153 | /* Begin XCBuildConfiguration section */ 154 | 72EE44CB1D90D523004729ED /* Debug */ = { 155 | isa = XCBuildConfiguration; 156 | buildSettings = { 157 | ALWAYS_SEARCH_USER_PATHS = NO; 158 | CLANG_ANALYZER_NONNULL = YES; 159 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 160 | CLANG_CXX_LIBRARY = "libc++"; 161 | CLANG_ENABLE_MODULES = YES; 162 | CLANG_ENABLE_OBJC_ARC = YES; 163 | CLANG_WARN_BOOL_CONVERSION = YES; 164 | CLANG_WARN_CONSTANT_CONVERSION = YES; 165 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 166 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 167 | CLANG_WARN_EMPTY_BODY = YES; 168 | CLANG_WARN_ENUM_CONVERSION = YES; 169 | CLANG_WARN_INFINITE_RECURSION = YES; 170 | CLANG_WARN_INT_CONVERSION = YES; 171 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 172 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 173 | CLANG_WARN_UNREACHABLE_CODE = YES; 174 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 175 | CODE_SIGN_IDENTITY = "-"; 176 | COPY_PHASE_STRIP = NO; 177 | DEBUG_INFORMATION_FORMAT = dwarf; 178 | ENABLE_STRICT_OBJC_MSGSEND = YES; 179 | ENABLE_TESTABILITY = YES; 180 | GCC_C_LANGUAGE_STANDARD = gnu99; 181 | GCC_DYNAMIC_NO_PIC = NO; 182 | GCC_NO_COMMON_BLOCKS = YES; 183 | GCC_OPTIMIZATION_LEVEL = 0; 184 | GCC_PREPROCESSOR_DEFINITIONS = ( 185 | "DEBUG=1", 186 | "$(inherited)", 187 | ); 188 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 189 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 190 | GCC_WARN_UNDECLARED_SELECTOR = YES; 191 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 192 | GCC_WARN_UNUSED_FUNCTION = YES; 193 | GCC_WARN_UNUSED_VARIABLE = YES; 194 | MACOSX_DEPLOYMENT_TARGET = 10.11; 195 | MTL_ENABLE_DEBUG_INFO = YES; 196 | ONLY_ACTIVE_ARCH = YES; 197 | SDKROOT = macosx; 198 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 199 | }; 200 | name = Debug; 201 | }; 202 | 72EE44CC1D90D523004729ED /* Release */ = { 203 | isa = XCBuildConfiguration; 204 | buildSettings = { 205 | ALWAYS_SEARCH_USER_PATHS = NO; 206 | CLANG_ANALYZER_NONNULL = YES; 207 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 208 | CLANG_CXX_LIBRARY = "libc++"; 209 | CLANG_ENABLE_MODULES = YES; 210 | CLANG_ENABLE_OBJC_ARC = YES; 211 | CLANG_WARN_BOOL_CONVERSION = YES; 212 | CLANG_WARN_CONSTANT_CONVERSION = YES; 213 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 214 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 215 | CLANG_WARN_EMPTY_BODY = YES; 216 | CLANG_WARN_ENUM_CONVERSION = YES; 217 | CLANG_WARN_INFINITE_RECURSION = YES; 218 | CLANG_WARN_INT_CONVERSION = YES; 219 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 220 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 221 | CLANG_WARN_UNREACHABLE_CODE = YES; 222 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 223 | CODE_SIGN_IDENTITY = "-"; 224 | COPY_PHASE_STRIP = NO; 225 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 226 | ENABLE_NS_ASSERTIONS = NO; 227 | ENABLE_STRICT_OBJC_MSGSEND = YES; 228 | GCC_C_LANGUAGE_STANDARD = gnu99; 229 | GCC_NO_COMMON_BLOCKS = YES; 230 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 231 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 232 | GCC_WARN_UNDECLARED_SELECTOR = YES; 233 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 234 | GCC_WARN_UNUSED_FUNCTION = YES; 235 | GCC_WARN_UNUSED_VARIABLE = YES; 236 | MACOSX_DEPLOYMENT_TARGET = 10.11; 237 | MTL_ENABLE_DEBUG_INFO = NO; 238 | SDKROOT = macosx; 239 | }; 240 | name = Release; 241 | }; 242 | 72EE44CE1D90D523004729ED /* Debug */ = { 243 | isa = XCBuildConfiguration; 244 | buildSettings = { 245 | PRODUCT_NAME = "$(TARGET_NAME)"; 246 | SWIFT_VERSION = 3.0; 247 | }; 248 | name = Debug; 249 | }; 250 | 72EE44CF1D90D523004729ED /* Release */ = { 251 | isa = XCBuildConfiguration; 252 | buildSettings = { 253 | PRODUCT_NAME = "$(TARGET_NAME)"; 254 | SWIFT_VERSION = 3.0; 255 | }; 256 | name = Release; 257 | }; 258 | /* End XCBuildConfiguration section */ 259 | 260 | /* Begin XCConfigurationList section */ 261 | 72EE44C11D90D523004729ED /* Build configuration list for PBXProject "JSONCopExample" */ = { 262 | isa = XCConfigurationList; 263 | buildConfigurations = ( 264 | 72EE44CB1D90D523004729ED /* Debug */, 265 | 72EE44CC1D90D523004729ED /* Release */, 266 | ); 267 | defaultConfigurationIsVisible = 0; 268 | defaultConfigurationName = Release; 269 | }; 270 | 72EE44CD1D90D523004729ED /* Build configuration list for PBXNativeTarget "JSONCopExample" */ = { 271 | isa = XCConfigurationList; 272 | buildConfigurations = ( 273 | 72EE44CE1D90D523004729ED /* Debug */, 274 | 72EE44CF1D90D523004729ED /* Release */, 275 | ); 276 | defaultConfigurationIsVisible = 0; 277 | defaultConfigurationName = Release; 278 | }; 279 | /* End XCConfigurationList section */ 280 | }; 281 | rootObject = 72EE44BE1D90D523004729ED /* Project object */; 282 | } 283 | -------------------------------------------------------------------------------- /JSONCopExample/JSONCopExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JSONCopExample/JSONCopExample/Person.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Person.swift 3 | // JSONCopExample 4 | // 5 | // Created by Draveness on 9/20/16. 6 | // Copyright © 2016 Draveness. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //@jsoncop 12 | struct Person { 13 | let id: Int 14 | let name: String 15 | let gender: Gender 16 | let currentProjectName: String 17 | 18 | enum Gender: Int { 19 | case male = 0 20 | case female = 1 21 | } 22 | 23 | static func JSONKeyPathByPropertyKey() -> [String: String] { 24 | return ["currentProjectName": "project.name"] 25 | } 26 | 27 | static func genderJSONTransformer(value: Int) -> Gender? { 28 | return Gender(rawValue: value) 29 | } 30 | 31 | static func projectsJSONTransformer(value: [[String: Any]]) -> [Project] { 32 | return value.flatMap(Project.parse) 33 | } 34 | 35 | static func dateJSONTransformer(value: Double) -> Date { 36 | return Date(timeIntervalSince1970: value) 37 | } 38 | } 39 | 40 | //@jsoncop 41 | struct Project { 42 | let name: String 43 | } 44 | 45 | // MARK: - JSONCop-Start 46 | 47 | extension Person { 48 | static func parse(json: Any) -> Person? { 49 | guard let json = json as? [String: Any] else { return nil } 50 | guard let id = (json["id"] as? Int), 51 | let name = (json["name"] as? String), 52 | let gender = (json["gender"] as? Int).flatMap(genderJSONTransformer), 53 | let currentProjectName = ((json["project"] as? [String: Any])?["name"] as? String) else { return nil } 54 | return Person(id: id, name: name, gender: gender, currentProjectName: currentProjectName) 55 | } 56 | static func parses(jsons: Any) -> [Person] { 57 | guard let jsons = jsons as? [[String: Any]] else { return [] } 58 | return jsons.flatMap(parse) 59 | } 60 | } 61 | 62 | extension Project { 63 | static func parse(json: Any) -> Project? { 64 | guard let json = json as? [String: Any] else { return nil } 65 | guard let name = (json["name"] as? String) else { return nil } 66 | return Project(name: name) 67 | } 68 | static func parses(jsons: Any) -> [Project] { 69 | guard let jsons = jsons as? [[String: Any]] else { return [] } 70 | return jsons.flatMap(parse) 71 | } 72 | } 73 | 74 | // JSONCop-End 75 | -------------------------------------------------------------------------------- /JSONCopExample/JSONCopExample/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // JSONCopExample 4 | // 5 | // Created by Draveness on 9/20/16. 6 | // Copyright © 2016 Draveness. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | let antherJSON = [ 12 | "id": 1, 13 | "name": "draven", 14 | "date": NSTimeIntervalSince1970, 15 | "gender": 1, 16 | "project": [ 17 | "name":"project-1" 18 | ] 19 | ] as [String : Any] 20 | 21 | print(Person.parse(json: antherJSON)) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Draveness 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./images/jsoncop-banner.png) 2 | 3 | # JSONCop 4 | 5 | [![License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/draveness/jsoncop/blob/master/LICENSE) 6 | [![Gem](https://img.shields.io/gem/v/jsoncop.svg?style=flat)](http://rubygems.org/gems/jsoncop) 7 | [![Swift](https://img.shields.io/badge/swift-3.0-yellow.svg)](https://img.shields.io/badge/Swift-%203.0%20-yellow.svg) 8 | 9 | JSONCop makes it easy to write a simple model layer for your Cocoa and Cocoa Touch application. 10 | 11 | > JSONCop scans all the methods and variables like `JSONKeyPathByPropertyKey` `xxxJSONTransformer` in all the structs which declare `//@jsoncop` before struct declaration line. And it won't change your original project structure. 12 | 13 | ```swift 14 | let json: [String: Any] = [ 15 | "id": 1, 16 | "name": "Draven", 17 | "createdAt": NSTimeIntervalSince1970 18 | ] 19 | let person = Person.parse(json: json) 20 | ``` 21 | 22 | ## Usage 23 | 24 | 1. Run `cop install` in a project root folder 25 | 2. Add `//@jsoncop` just before model definition line 26 | 27 | ```shell 28 | $ sudo gem install jsoncop --verbose 29 | $ cop install 30 | ``` 31 | 32 | ```swift 33 | //@jsoncop 34 | struct Person { 35 | let id: Int 36 | let name: String 37 | } 38 | ``` 39 | 40 | Then, each time build action is triggered, JSONCop would generate several parsing methods in swift files. 41 | 42 | ![](./images/jsoncop-demo.png) 43 | 44 | All the code between `// MARK: - JSONCop-Start` and `// JSONCop-End` and will be replaced when re-run `cop generate` in current project folder. Other codes will remain unchanged. Please don't write any codes in this area. 45 | 46 | ```swift 47 | extension Person { 48 | static func parse(json: Any) -> Person? { 49 | guard let json = json as? [String: Any] else { return nil } 50 | guard let id = json["id"] as? Int, 51 | let name = json["name"] as? String, 52 | let projects = (json["projects"] as? [[String: Any]]).flatMap(projectsJSONTransformer) else { return nil } 53 | return Person(id: id, name: name, projects: projects) 54 | } 55 | static func parses(jsons: Any) -> [Person] { 56 | guard let jsons = jsons as? [[String: Any]] else { return [] } 57 | return jsons.flatMap(parse) 58 | } 59 | } 60 | ``` 61 | 62 | Checkout [JSONCopExample](./JSONCopExample) for more information. 63 | 64 | ## Customize 65 | 66 | + JSON key to attribute customisation 67 | 68 | ```swift 69 | struct Person { 70 | let id: Int 71 | let name: String 72 | 73 | static func JSONKeyPathByPropertyKey() -> [String: String] { 74 | return ["name": "nickname"] 75 | } 76 | } 77 | ``` 78 | 79 | + Value transformer customisation 80 | 81 | ```swift 82 | struct Person { 83 | let id: Int 84 | let name: String 85 | let gender: Gender 86 | let projects: [Project] 87 | 88 | enum Gender: Int { 89 | case male = 0 90 | case female = 1 91 | } 92 | 93 | static func genderJSONTransformer(value: Int) -> Gender? { 94 | return Gender(rawValue: value) 95 | } 96 | 97 | static func projectsJSONTransformer(value: [[String: Any]]) -> [Project] { 98 | return value.flatMap(Project.parse) 99 | } 100 | } 101 | ``` 102 | 103 | + Nested JSON value extraction 104 | 105 | ```swift 106 | static func JSONKeyPathByPropertyKey() -> [String: String] { 107 | return ["currentProjectName": "project.name"] 108 | } 109 | ``` 110 | 111 | ## Installation 112 | 113 | ```shell 114 | sudo gem install jsoncop --verbose 115 | ``` 116 | 117 | ## Contributing 118 | 119 | Bug reports and pull requests are welcome on GitHub at https://github.com/draveness/jsoncop. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 120 | 121 | ## License 122 | 123 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 124 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # require "bundler/gem_tasks" 2 | # require "rspec/core/rake_task" 3 | require_relative 'lib/jsoncop/version' 4 | 5 | require "pathname" 6 | 7 | # RSpec::Core::RakeTask.new(:spec) 8 | 9 | task :default => [:build, :install, :clean] 10 | 11 | task :release => [:build, :push, :clean] 12 | 13 | task :push do 14 | system %(gem push #{build_product_file}) 15 | end 16 | 17 | task :build do 18 | system %(gem build jsoncop.gemspec) 19 | end 20 | 21 | task :install do 22 | system %(gem install #{build_product_file}) 23 | end 24 | 25 | task :clean do 26 | system %(rm *.gem) 27 | end 28 | 29 | def build_product_file 30 | "jsoncop-#{JSONCop::VERSION}.gem" 31 | end 32 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "jsoncop" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/cop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'jsoncop' 4 | 5 | JSONCop::Command.run(ARGV) 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /images/jsoncop-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/draveness/JSONCop/a7a707fb51175c9f4ed8fb379a8dd5e7c0a82e3f/images/jsoncop-banner.png -------------------------------------------------------------------------------- /images/jsoncop-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/draveness/JSONCop/a7a707fb51175c9f4ed8fb379a8dd5e7c0a82e3f/images/jsoncop-demo.png -------------------------------------------------------------------------------- /images/run-script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/draveness/JSONCop/a7a707fb51175c9f4ed8fb379a8dd5e7c0a82e3f/images/run-script.png -------------------------------------------------------------------------------- /jsoncop.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'jsoncop/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "jsoncop" 8 | spec.version = JSONCop::VERSION 9 | spec.authors = ["Draveness"] 10 | spec.email = ["stark.draven@gmail.com"] 11 | 12 | spec.summary = %q{A JSON to model method generator.} 13 | spec.description = %q{A light-weight JSON to model method generator.} 14 | spec.homepage = "https://github.com/Draveness/JSONCop" 15 | spec.license = "MIT" 16 | 17 | spec.files = Dir["lib/**/*.rb"] + %w{ bin/cop README.md LICENSE } 18 | 19 | spec.executables = %w{ cop } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_runtime_dependency 'claide', '>= 1.0.0', '< 2.0' 23 | spec.add_runtime_dependency 'colored', '~> 1.2' 24 | spec.add_runtime_dependency 'xcodeproj', '>= 1.2.0', '< 2.0' 25 | spec.add_runtime_dependency 'activesupport', '>= 3', '< 5.0' 26 | 27 | spec.add_development_dependency "bundler", "~> 1.12" 28 | spec.add_development_dependency "rake", "~> 10.0" 29 | spec.add_development_dependency "rspec", "~> 3.0" 30 | 31 | spec.rubygems_version = "2.0.0" 32 | end 33 | -------------------------------------------------------------------------------- /lib/jsoncop.rb: -------------------------------------------------------------------------------- 1 | require "jsoncop/version" 2 | 3 | module JSONCop 4 | 5 | class Informative < StandardError; end 6 | 7 | require 'jsoncop/version' 8 | require 'jsoncop/helper' 9 | 10 | autoload :Command, 'jsoncop/command' 11 | end 12 | -------------------------------------------------------------------------------- /lib/jsoncop/analyzer.rb: -------------------------------------------------------------------------------- 1 | module JSONCop 2 | class Analyzer 3 | 4 | require 'jsoncop/model/model' 5 | require 'jsoncop/model/attribute' 6 | require 'jsoncop/model/transformer' 7 | 8 | JSON_COP_ENABLED = /@jsoncop/ 9 | 10 | NEED_PARSING_CONTENT = /(\/\/\s*@jsoncop\s+(?:struct|class)(?:.|\s)*?\n})/ 11 | MODEL_NAME_REGEX = /(struct|class)\s+(.+)\s*{/ 12 | ATTRIBUTE_REGEX = /^\s+(let|var)\s(.+):(.+)/ 13 | JSON_TRANSFORMER_REGEX = /^\s+static\s+func\s+(.+)JSONTransformer\(.+?\:(.+)\).+->.+/ 14 | JSON_BY_PROPERTY_HASH_REGEX = /static\s+func\s+JSONKeyPathByPropertyKey\(\)\s*->\s*\[String\s*:\s*String\]\s*{\s*return\s*(\[[\s\."a-z0-9A-Z_\-:\[\],]*)}/ 15 | 16 | attr_reader :file_path 17 | attr_accessor :current_model 18 | 19 | def initialize(file_path) 20 | @file_path = file_path 21 | end 22 | 23 | def analyze! 24 | content = File.read file_path 25 | return unless content =~ JSON_COP_ENABLED 26 | models = [] 27 | content.scan(NEED_PARSING_CONTENT).flatten.each do |content| 28 | content.each_line do |line| 29 | if line =~ MODEL_NAME_REGEX 30 | model_name = line.scan(MODEL_NAME_REGEX).flatten.last 31 | @current_model = Model::Model.new model_name 32 | models << @current_model 33 | elsif line =~ ATTRIBUTE_REGEX 34 | result = line.scan(ATTRIBUTE_REGEX).flatten 35 | @current_model.attributes << Model::Attribute.new(result[1], result[2]) 36 | elsif line =~ JSON_TRANSFORMER_REGEX 37 | result = line.scan(JSON_TRANSFORMER_REGEX).flatten 38 | @current_model.transformers << Model::Transformer.new(result[0], result[1]) 39 | end 40 | end 41 | 42 | json_attr_list = content.scan(JSON_BY_PROPERTY_HASH_REGEX).flatten.first 43 | if json_attr_list 44 | json_attr_list.gsub!(/[(\[\]"\s)]*/, '') 45 | @current_model.attr_json_hash = Hash[json_attr_list.split(",").map { 46 | |attr_json_pair| attr_json_pair.split(":") 47 | }] 48 | end 49 | end 50 | 51 | models 52 | end 53 | 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/jsoncop/command.rb: -------------------------------------------------------------------------------- 1 | require 'colored' 2 | require 'claide' 3 | 4 | module JSONCop 5 | class Command < CLAide::Command 6 | require "jsoncop/command/install" 7 | require "jsoncop/command/generate" 8 | require "jsoncop/command/uninstall" 9 | 10 | self.abstract_command = true 11 | self.command = 'cop' 12 | self.version = VERSION 13 | self.description = 'JSONCop, the JSON parsing methods generator.' 14 | self.plugin_prefixes = %w(claide meta) 15 | 16 | def self.run(argv) 17 | raise Informative, "JSONCop must run in project root folder which contains a xcodeproj file" \ 18 | unless Dir.glob("*.xcodeproj").count > 0 19 | super(argv) 20 | end 21 | 22 | def self.options 23 | super 24 | end 25 | 26 | def initialize(argv) 27 | super 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/jsoncop/command/generate.rb: -------------------------------------------------------------------------------- 1 | module JSONCop 2 | class Command 3 | class Generate < Command 4 | 5 | require "jsoncop/analyzer" 6 | require "jsoncop/generator" 7 | 8 | self.summary = "" 9 | self.description = <<-DESC 10 | DESC 11 | 12 | def initialize(argv) 13 | super 14 | end 15 | 16 | def run 17 | Dir.glob("**/*.swift").each do |file| 18 | analyzer = create_analyzer_for_file file 19 | model = analyzer.analyze! 20 | next unless model 21 | 22 | generator = create_generator_for_file file, model 23 | generator.generate! 24 | end 25 | end 26 | 27 | def create_analyzer_for_file(file_path) 28 | Analyzer.new file_path 29 | end 30 | 31 | def create_generator_for_file(file_path, model) 32 | Generator.new file_path, model 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/jsoncop/command/install.rb: -------------------------------------------------------------------------------- 1 | require "xcodeproj" 2 | 3 | module JSONCop 4 | class Command 5 | class Install < Command 6 | 7 | BUILD_PHASE_PREFIX = "[JC] ".freeze 8 | 9 | JSONCOP_INSTALL_PHASE_NAME = "JSONCop Install Script".freeze 10 | 11 | self.summary = "" 12 | self.description = <<-DESC 13 | DESC 14 | 15 | def initialize(argv) 16 | super 17 | end 18 | 19 | def run 20 | project = Xcodeproj::Project.open(Dir.glob("*.xcodeproj").first) 21 | native_target = project.targets.first 22 | phase = create_or_update_build_phase(native_target, JSONCOP_INSTALL_PHASE_NAME) 23 | phase.shell_path = "/usr/bin/ruby" 24 | phase.shell_script = install_shell_script 25 | project.save 26 | end 27 | 28 | def create_or_update_build_phase(target, phase_name, phase_class = Xcodeproj::Project::Object::PBXShellScriptBuildPhase) 29 | prefixed_phase_name = BUILD_PHASE_PREFIX + phase_name 30 | build_phases = target.build_phases.grep(phase_class) 31 | build_phases.find { |phase| phase.name && phase.name.end_with?(phase_name) }.tap { |p| p.name = prefixed_phase_name if p } || 32 | target.project.new(phase_class).tap do |phase| 33 | phase.name = prefixed_phase_name 34 | phase.show_env_vars_in_log = '0' 35 | target.build_phases.unshift phase 36 | end 37 | end 38 | 39 | def install_shell_script 40 | <<-SCRIPT#!/usr/bin/env ruby 41 | require 'jsoncop' 42 | Encoding.default_external = Encoding::UTF_8 43 | JSONCop::Command.run(["generate"]) 44 | SCRIPT 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/jsoncop/command/uninstall.rb: -------------------------------------------------------------------------------- 1 | module JSONCop 2 | class Command 3 | class Uninstall < Command 4 | self.summary = "" 5 | self.description = <<-DESC 6 | DESC 7 | 8 | def initialize(argv) 9 | super 10 | end 11 | 12 | def run 13 | Dir.glob("**/*.swift").each do |file_path| 14 | jsoncop_generate_start = /jsoncop: generate\-start/ 15 | jsoncop_generate_end = /jsoncop: generate\-end/ 16 | content = File.read file_path 17 | if content.match(jsoncop_generate_start) && content.match(jsoncop_generate_end) 18 | content.gsub!(/\/\/ jsoncop: generate-start[^$]*jsoncop: generate\-end/, "") 19 | end 20 | File.write file_path, content 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/jsoncop/generator.rb: -------------------------------------------------------------------------------- 1 | module JSONCop 2 | class Generator 3 | 4 | require 'jsoncop/model/model' 5 | require 'jsoncop/model/attribute' 6 | 7 | attr_reader :file_path 8 | attr_reader :model 9 | 10 | def initialize(file_path, models) 11 | @file_path = file_path 12 | @models = models 13 | end 14 | 15 | def generate! 16 | jsoncop_generate_start = /\/\/ MARK: \- JSONCop\-Start/ 17 | jsoncop_generate_end = /\/\/ JSONCop\-End/ 18 | content = File.read file_path 19 | if content.match(jsoncop_generate_start) && content.match(jsoncop_generate_end) 20 | content.gsub!(/\/\/ MARK: \- JSONCop\-Start[^$]*JSONCop\-End/, json_cop_template) 21 | else 22 | content += json_cop_template 23 | end 24 | File.write file_path, content 25 | end 26 | 27 | def json_cop_template 28 | result = "// MARK: - JSONCop-Start\n\n" 29 | @models.each do |model| 30 | result += <<-JSONCOP 31 | extension #{model.name} { 32 | static func parse(json: Any) -> #{model.name}? { 33 | guard let json = json as? [String: Any] else { return nil } 34 | guard #{json_parsing_template model} else { return nil } 35 | return #{model.name}(#{model.key_value_pair}) 36 | } 37 | static func parses(jsons: Any) -> [#{model.name}] { 38 | guard let jsons = jsons as? [[String: Any]] else { return [] } 39 | return jsons.flatMap(parse) 40 | } 41 | } 42 | 43 | JSONCOP 44 | # result += <<-CODING 45 | # extension #{model.name}: NSCoding { 46 | # func encode(with aCoder: NSCoder) { 47 | # 48 | # } 49 | # 50 | # init?(coder decoder: NSCoder) { 51 | # guard #{decode_template model} else { return nil } 52 | # 53 | # self.init(#{model.key_value_pair}) 54 | # } 55 | # } 56 | # 57 | # CODING 58 | end 59 | result += "// JSONCop-End" 60 | result 61 | end 62 | 63 | def json_parsing_template(model) 64 | model.attributes.map do |attr| 65 | transformer = model.transformers.select { |t| t.name == attr.name }.first 66 | attr_key_path = model.attr_json_hash[attr.name] || attr.name 67 | return unless attr_key_path 68 | value = "json" 69 | key_paths = attr_key_path.split(".") 70 | key_paths.each_with_index do |key, index| 71 | value.insert 0, "(" 72 | value += "[\"#{key}\"] as? " 73 | if index == key_paths.count - 1 74 | if transformer 75 | value += "#{transformer.type})" 76 | value += ".flatMap(#{attr.name}JSONTransformer)" 77 | else 78 | value += "#{attr.type})" 79 | end 80 | else 81 | value += "[String: Any])?" 82 | end 83 | end 84 | 85 | "let #{attr.name} = #{value}" 86 | end.join(",\n\t\t\t") 87 | end 88 | 89 | def decode_template(model) 90 | model.attributes.map do |attr| 91 | "let #{attr.name} = decoder.decode#{attr.decode_type}(forKey: \"#{attr.name}\") as? #{attr.type}" 92 | end.join(",\n\t\t\t") 93 | end 94 | 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/jsoncop/helper.rb: -------------------------------------------------------------------------------- 1 | module JSONCop 2 | module Helper 3 | class String 4 | def clear_white_space 5 | self.gsub(/\s+/, "") 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/jsoncop/model/attribute.rb: -------------------------------------------------------------------------------- 1 | module JSONCop 2 | module Model 3 | class Attribute 4 | attr_reader :name, :type 5 | def initialize(name, type) 6 | @name = name.gsub(/\s+/, "") 7 | @type = type.gsub(/\s+/, "") 8 | end 9 | 10 | def real_type 11 | return type[0..-2] if type.end_with? "?" 12 | return type 13 | end 14 | 15 | def optional_type 16 | return type if type.end_with? "?" 17 | return type + "?" 18 | end 19 | 20 | def decode_type 21 | built_in_types = %w[Int32 Int64 Bool Float Double] 22 | return real_type if built_in_types.include? real_type 23 | 24 | case real_type 25 | when "Int" then "Integer" 26 | when "Date" then "Object" 27 | else "Object" 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/jsoncop/model/model.rb: -------------------------------------------------------------------------------- 1 | module JSONCop 2 | module Model 3 | class Model 4 | attr_reader :name 5 | attr_accessor :is_struct 6 | attr_accessor :attributes 7 | attr_accessor :transformers 8 | attr_accessor :attr_json_hash 9 | 10 | def initialize(name) 11 | @name = name.gsub(/\s+/, "") 12 | @attributes = [] 13 | @transformers = [] 14 | @attr_json_hash = {} 15 | end 16 | 17 | def key_value_pair 18 | attributes.map { |attribute| "#{attribute.name}: #{attribute.name}" }.join(", ") 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/jsoncop/model/transformer.rb: -------------------------------------------------------------------------------- 1 | module JSONCop 2 | module Model 3 | class Transformer 4 | attr_reader :name, :type 5 | def initialize(name, type) 6 | @name = name.gsub(/\s+/, "") 7 | @type = type.gsub(/\s+/, "").gsub(/:/, ": ") 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/jsoncop/version.rb: -------------------------------------------------------------------------------- 1 | module JSONCop 2 | VERSION = "0.4.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/jsoncop_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe JSONCop do 4 | it 'has a version number' do 5 | expect(JSONCop::VERSION).not_to be nil 6 | end 7 | 8 | it 'does something useful' do 9 | expect(false).to eq(true) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'jsoncop' 3 | --------------------------------------------------------------------------------