├── .all-contributorsrc ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── banner.js ├── package.json ├── rollup.cjs.js ├── rollup.umd.js ├── src └── index.js ├── tests ├── config │ └── setup.js ├── index.test.js └── snapshots │ ├── index.test.js.md │ └── index.test.js.snap └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "transform-json-types", 3 | "projectOwner": "transform-it", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "imageSize": 100, 8 | "commit": true, 9 | "contributors": [ 10 | { 11 | "login": "ritz078", 12 | "name": "Ritesh Kumar", 13 | "avatar_url": "https://avatars3.githubusercontent.com/u/5389035?v=4", 14 | "profile": "http://riteshkr.com", 15 | "contributions": [ 16 | "code", 17 | "doc", 18 | "ideas" 19 | ] 20 | }, 21 | { 22 | "login": "skade", 23 | "name": "Florian Gilcher", 24 | "avatar_url": "https://avatars2.githubusercontent.com/u/47542?v=4", 25 | "profile": "http://asquera.de", 26 | "contributions": [ 27 | "code" 28 | ] 29 | }, 30 | { 31 | "login": "xperiments", 32 | "name": "Pedro Casaubon", 33 | "avatar_url": "https://avatars0.githubusercontent.com/u/417709?v=4", 34 | "profile": "http://www.xperiments.in", 35 | "contributions": [ 36 | "code", 37 | "ideas" 38 | ] 39 | }, 40 | { 41 | "login": "stereobooster", 42 | "name": "stereobooster", 43 | "avatar_url": "https://avatars2.githubusercontent.com/u/179534?v=4", 44 | "profile": "https://github.com/stereobooster", 45 | "contributions": [ 46 | "code" 47 | ] 48 | }, 49 | { 50 | "login": "waf", 51 | "name": "Will Fuqua", 52 | "avatar_url": "https://avatars0.githubusercontent.com/u/97195?v=4", 53 | "profile": "http://fuqua.io", 54 | "contributions": [ 55 | "doc" 56 | ] 57 | }, 58 | { 59 | "login": "cobraz", 60 | "name": "Simen A. W. Olsen", 61 | "avatar_url": "https://avatars.githubusercontent.com/u/3726815?v=4", 62 | "profile": "http://cobraz.no", 63 | "contributions": [ 64 | "code" 65 | ] 66 | } 67 | ], 68 | "repoType": "github", 69 | "repoHost": "https://github.com", 70 | "skipCi": true, 71 | "contributorsPerLine": 7 72 | } 73 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [package.json] 14 | ; The indent size used in the `package.json` file cannot be changed 15 | ; https://github.com/npm/npm/pull/3180#issuecomment-16336516 16 | indent_size = 2 17 | indent_style = space -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transform-it/transform-json-types/5d1335ad5398bf61771f7008b54bf7faa1792547/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard", "prettier"], 3 | "rules": { 4 | "no-useless-escape": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .sass-cache 4 | bower_components 5 | coverage 6 | dist 7 | umd 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | # whitelist 9 | branches: 10 | only: 11 | - master 12 | before_script: 13 | - npm prune 14 | script: 15 | - yarn lint && yarn test 16 | node_js: 17 | - "8" 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | transform-json-types 2 | [![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors) 3 | ========= 4 | 5 | An utility to generate Flow, TypeScript, Rust Serde Struct and Scala Case Class from JSON. 6 | 7 | **Note** : It also detects optional properties for TS/Flow from a Collection. 8 | 9 | Installation 10 | ============ 11 | 12 | ``` 13 | npm install transform-json-types 14 | ``` 15 | 16 | The online REPL is available at 17 | - [JSON to Flow](https://transform.now.sh/json-to-flow-types) 18 | - [JSON to TypeScript](https://transform.now.sh/json-to-ts-interface) 19 | - [JSON to Scala Case Class](https://transform.now.sh/json-to-scala-case-class) 20 | - [JSON to Rust Serde](https://transform.now.sh/json-to-rust-serde) 21 | 22 | Basic Usage 23 | =========== 24 | ```js 25 | import transform from "transform-json-types" 26 | 27 | const json = `{ 28 | "hello": "world" 29 | }` 30 | 31 | console.log(transform(json, { 32 | lang: "typescript" 33 | })) 34 | 35 | // interface RootJson { 36 | // hello: string 37 | // } 38 | 39 | console.log(transform(json, { 40 | lang: "rust-serde" 41 | })) 42 | 43 | // #[derive(Serialize, Deserialize)] 44 | // struct RootInterface { 45 | // hello: String, 46 | // } 47 | ``` 48 | 49 | Usage with [sarcastic](https://github.com/jamiebuilds/sarcastic/) and Flow 50 | === 51 | 52 | ```ts 53 | //@flow 54 | import is, { type AssertionType } from "sarcastic" 55 | 56 | // Interface generated by "transform-json-types" 57 | const PersonInterface = is.shape({ 58 | name: is.string, 59 | age: is.number 60 | }); 61 | 62 | // Use it like this: 63 | type Person = AssertionType 64 | const assertPerson = (val: mixed): Person => 65 | is(val, PersonInterface, "Person") 66 | const person = assertPerson(JSON.parse('{"name":"Giulio","age":43}'))) 67 | ``` 68 | 69 | Usage with [io-ts](https://github.com/gcanti/io-ts) and TypeScript 70 | === 71 | 72 | ```ts 73 | import * as t from "io-ts" 74 | 75 | // Interface generated by "transform-json-types" 76 | const PersonInterface = t.type({ 77 | name: t.string, 78 | age: t.number 79 | }); 80 | 81 | // Use it like this: 82 | PersonInterface.decode(JSON.parse('{"name":"Giulio","age":43}')) // => Right({name: "Giulio", age: 43}) 83 | PersonInterface.decode(JSON.parse('{"name":"Giulio"}')) // => Left([...]) 84 | type Person = t.TypeOf 85 | ``` 86 | 87 | Usage with [runtypes](https://github.com/pelotom/runtypes) and TypeScript 88 | === 89 | 90 | ```ts 91 | import * as rt from "runtypes" 92 | 93 | // Interface generated by "transform-json-types" 94 | const PersonInterface = rt.Record({ 95 | name: rt.String, 96 | age: rt.Number 97 | }); 98 | 99 | // Use it like this: 100 | PersonInterface.check(JSON.parse('{"name":"Giulio","age":43}')) // => {name: "Giulio", age: 43} 101 | ``` 102 | 103 | API 104 | === 105 | ### transform(json, [options]) 106 | 107 | #### json : `String | JSON` 108 | You can pass a parsed JSON or a stringified JSON. 109 | 110 | ### Options 111 | Option|Default|Description 112 | ----|-----|----- 113 | lang| 'flow'| One of `flow`, `typescript`, `scala`, `sarcastic`, `io-ts` or `rust-serde` 114 | rustCase| 'camelCase' | either snakeCase or camelCase 115 | 116 | ### Inspirations 117 | Majority of the inspiration was from [xperiment](https://github.com/xperiments)'s [json2dts](https://github.com/xperiments/json2dts) 118 | 119 | ### Development 120 | 1. Fork and clone the repo. 121 | 1. Create a new branch. 122 | 1. Create features or fix bugs. 123 | 1. Write test to improve stability. 124 | 1. Open a PR. 125 | 126 | License 127 | ======= 128 | MIT @ [Ritesh Kumar](https://twitter.com/ritz078) 129 | 130 | 131 | 132 | 133 | 134 | ## Contributors 135 | 136 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 |

Ritesh Kumar

💻 📖 🤔

Florian Gilcher

💻

Pedro Casaubon

💻 🤔

stereobooster

💻

Will Fuqua

📖

Simen A. W. Olsen

💻
151 | 152 | 153 | 154 | 155 | 156 | 157 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 158 | -------------------------------------------------------------------------------- /banner.js: -------------------------------------------------------------------------------- 1 | const pkg = require("./package.json"); 2 | 3 | const banner = `/* 4 | * ${pkg.name} - v${pkg.version} 5 | * ${pkg.description} 6 | * ${pkg.homepage} 7 | * 8 | * Made by ${pkg.author.name} 9 | * Under ${pkg.license} License 10 | */ 11 | `; 12 | 13 | module.exports = banner; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transform-json-types", 3 | "version": "0.6.0", 4 | "description": "An utility to generate Flow, TypeScript, Rust Serde Struct and Scala Case Class from JSON.", 5 | "main": "dist/index.js", 6 | "module": "src/index.js", 7 | "browser": "umd/index.js", 8 | "files": [ 9 | "dist", 10 | "src", 11 | "umd" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/transform-it/transform-json-types.git" 16 | }, 17 | "scripts": { 18 | "commit": "git cz", 19 | "test": "ava", 20 | "test:watch": "ava --watch", 21 | "test:cover": "nyc ava", 22 | "test:report": "cat ./coverage/lcov.info | codecov && rm -rf ./coverage", 23 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 24 | "build": "npm run build:umd && npm run build:cjs", 25 | "build:umd": "rollup -c rollup.umd.js", 26 | "build:cjs": "rollup -c rollup.cjs.js", 27 | "build:watch": "concurrently 'npm run build:umd -- -w' 'npm run build:cjs -- -w'", 28 | "lint": "eslint src/**/*.js *.js tests/**/*.js", 29 | "lint:fix": "npm run lint -- --fix", 30 | "format": "prettier --write src/**/*.js *.js tests/**/*.js", 31 | "prepublish": "npm run build", 32 | "precommit": "lint-staged" 33 | }, 34 | "license": "MIT", 35 | "devDependencies": { 36 | "all-contributors-cli": "^4.4.0", 37 | "ava": "^0.21.0", 38 | "babel-core": "^6.26.0", 39 | "babel-polyfill": "^6.23.0", 40 | "babel-preset-env": "^1.6.0", 41 | "babel-register": "^6.24.1", 42 | "codecov.io": "^0.1.6", 43 | "concurrently": "^3.5.0", 44 | "cz-conventional-changelog": "^2.0.0", 45 | "eslint": "^4.2.0", 46 | "eslint-config-prettier": "^2.3.0", 47 | "eslint-config-standard": "^10.2.1", 48 | "eslint-plugin-import": "^2.7.0", 49 | "eslint-plugin-node": "^5.1.0", 50 | "eslint-plugin-promise": "^3.5.0", 51 | "eslint-plugin-standard": "^3.0.1", 52 | "husky": "^0.14.3", 53 | "jsdom": "^9.12.0", 54 | "lint-staged": "^4.0.1", 55 | "nyc": "^11.0.3", 56 | "pascal-case": "^2.0.1", 57 | "prettier-eslint-cli": "^4.1.1", 58 | "rollup": "^0.45.2", 59 | "rollup-plugin-buble": "^0.15.0", 60 | "rollup-plugin-filesize": "^1.4.2", 61 | "rollup-watch": "^4.3.1", 62 | "semantic-release": "^6.3.6" 63 | }, 64 | "config": { 65 | "commitizen": { 66 | "path": "node_modules/cz-conventional-changelog" 67 | } 68 | }, 69 | "author": { 70 | "name": "Ritesh Kumar", 71 | "email": "rkritesh078@gmail.com", 72 | "url": "https://github.com/ritz078" 73 | }, 74 | "lint-staged": { 75 | "src/**/*.js": [ 76 | "prettier --write", 77 | "git add" 78 | ] 79 | }, 80 | "homepage": "https://github.com/transform-it/transform-json-types", 81 | "ava": { 82 | "require": [ 83 | "babel-register", 84 | "babel-polyfill" 85 | ] 86 | }, 87 | "dependencies": { 88 | "lodash": "^4.17.4", 89 | "rust-keywords": "^1.1.0", 90 | "sha1": "^1.1.1" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rollup.cjs.js: -------------------------------------------------------------------------------- 1 | const pkg = require("./package.json"); 2 | const fileSize = require("rollup-plugin-filesize"); 3 | 4 | const banner = `/* 5 | * ${pkg.name} - v${pkg.version} 6 | * ${pkg.description} 7 | * ${pkg.homepage} 8 | * 9 | * Made by ${pkg.author.name} 10 | * Under ${pkg.license} License 11 | */ 12 | `; 13 | 14 | const config = { 15 | entry: "src/index.js", 16 | dest: "dist/index.js", 17 | format: "cjs", 18 | banner, 19 | plugins: [fileSize()] 20 | }; 21 | 22 | module.exports = config; 23 | -------------------------------------------------------------------------------- /rollup.umd.js: -------------------------------------------------------------------------------- 1 | const banner = require("./banner"); 2 | const pkg = require("./package.json"); 3 | const buble = require("rollup-plugin-buble"); 4 | const fileSize = require("rollup-plugin-filesize"); 5 | const pascalCase = require("pascal-case"); 6 | 7 | const config = { 8 | entry: "src/index.js", 9 | dest: "umd/index.js", 10 | moduleName: pascalCase(pkg.name), 11 | format: "umd", 12 | banner, 13 | plugins: [buble(), fileSize()] 14 | }; 15 | 16 | module.exports = config; 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable standard/computed-property-even-spacing */ 2 | import SHA1 from "sha1"; 3 | import { 4 | isEmpty, 5 | isArray, 6 | isObject, 7 | isBoolean, 8 | isNumber, 9 | isString, 10 | merge, 11 | isInteger, 12 | intersection, 13 | difference, 14 | union, 15 | assign, 16 | camelCase, 17 | upperFirst, 18 | snakeCase 19 | } from "lodash"; 20 | import rustReserved from "rust-keywords"; 21 | 22 | const typeNames = { 23 | STRING: "string", 24 | NUMBER: "number", 25 | INTEGER: "number", 26 | BOOLEAN: "boolean", 27 | ARRAY: "[]", 28 | ANY: "any" 29 | }; 30 | 31 | const mapping = { 32 | flow: { 33 | interface: "type", 34 | separator: ",", 35 | startingBrace: "{", 36 | endingBrace: "}", 37 | terminator: ";", 38 | equator: " = ", 39 | types: typeNames, 40 | optional: "?", 41 | handleArray: (className = "", any) => (any ? "any[]" : `${className}[]`) 42 | }, 43 | typescript: { 44 | interface: "interface", 45 | separator: ";", 46 | startingBrace: "{", 47 | endingBrace: "}", 48 | terminator: "", 49 | equator: "", 50 | types: typeNames, 51 | optional: "?", 52 | handleArray: (className = "", any) => (any ? "any[]" : `${className}[]`) 53 | }, 54 | "rust-serde": { 55 | interface: "struct", 56 | separator: ",", 57 | startingBrace: "{", 58 | endingBrace: "}", 59 | terminator: "", 60 | equator: "", 61 | types: merge({}, typeNames, { 62 | STRING: "String", 63 | NUMBER: "f64", 64 | INTEGER: "i64", 65 | BOOLEAN: "bool", 66 | ANY: "()" 67 | }), 68 | handleOptionalValue: value => `Option<${value}>`, 69 | handleArray: (className = "") => `Vec<${className}>`, 70 | preInterface: "#[derive(Serialize, Deserialize)]\n" 71 | }, 72 | scala: { 73 | interface: "case class", 74 | separator: ",", 75 | startingBrace: "(", 76 | endingBrace: ")", 77 | terminator: "", 78 | equator: "", 79 | types: merge({}, typeNames, { 80 | STRING: "String", 81 | NUMBER: "Double", 82 | INTEGER: "Int", 83 | ANY: "Any", 84 | BOOLEAN: "Boolean" 85 | }), 86 | hideTerminatorAtLast: true, 87 | handleOptionalValue: value => `Option[${value}]`, 88 | handleArray: (className = "") => `Seq[${className}]` 89 | }, 90 | "io-ts": { 91 | preText: "import * as t from 'io-ts'", 92 | interface: "const", 93 | separator: ",", 94 | startingBrace: "{", 95 | endingBrace: "})", 96 | terminator: ";", 97 | equator: " = t.type(", 98 | handleArray: (className = "", any) => 99 | any ? "t.Array" : `t.array(${className})`, 100 | types: { 101 | STRING: "t.string", 102 | NUMBER: "t.number", 103 | INTEGER: "t.Integer", 104 | ANY: "t.any", 105 | BOOLEAN: "t.boolean", 106 | ARRAY: "t.array" 107 | } 108 | }, 109 | runtypes: { 110 | preText: "import * as rt from 'runtypes'", 111 | interface: "const", 112 | separator: ",", 113 | startingBrace: "{", 114 | endingBrace: "})", 115 | terminator: ";", 116 | equator: " = rt.Record(", 117 | handleArray: (className = "", any) => 118 | any ? "rt.Array" : `rt.Array(${className})`, 119 | types: { 120 | STRING: "rt.String", 121 | NUMBER: "rt.Number", 122 | INTEGER: "rt.Integer", 123 | ANY: "rt.Unknown", 124 | BOOLEAN: "rt.Boolean", 125 | ARRAY: "rt.Array" 126 | } 127 | }, 128 | sarcastic: { 129 | interface: "const", 130 | separator: ",", 131 | startingBrace: "{", 132 | endingBrace: "})", 133 | terminator: ";", 134 | equator: " = is.shape(", 135 | handleArray: (className = "", any) => 136 | any ? "is.array" : `is.arrayOf(${className})`, 137 | handleOptionalValue: value => `is.maybe(${value})`, 138 | types: { 139 | STRING: "is.string", 140 | NUMBER: "is.number", 141 | INTEGER: "is.number", 142 | ANY: "is.any", 143 | BOOLEAN: "is.boolean", 144 | ARRAY: "is.array" 145 | } 146 | } 147 | }; 148 | 149 | let langDetails = {}; 150 | let classes = {}; 151 | let classesCache = {}; 152 | let classesInUse = {}; 153 | let optionalProperties = {}; 154 | 155 | function pascalCase(str) { 156 | return upperFirst(camelCase(str)); 157 | } 158 | 159 | function setOptionalProperties(arr, objectName) { 160 | if (!isValueConsistent(arr)) return; 161 | const arrayOfKeys = arr.map(a => Object.keys(a)); 162 | optionalProperties[objectName] = difference( 163 | union(...arrayOfKeys), 164 | intersection(...arrayOfKeys) 165 | ); 166 | } 167 | 168 | function hasSpecialChars(str) { 169 | return /[ ~`!@#$%\^&*+=\-\[\]\\';,\/{}|\\":<>\?]/g.test(str); 170 | } 171 | 172 | function getBasicType(value) { 173 | const { types } = langDetails; 174 | 175 | let type = types.STRING; 176 | switch (true) { 177 | case isString(value): 178 | type = types.STRING; 179 | break; 180 | case isInteger(value): 181 | type = types.INTEGER; 182 | break; 183 | case isNumber(value): 184 | type = types.NUMBER; 185 | break; 186 | case isBoolean(value): 187 | type = types.BOOLEAN; 188 | break; 189 | } 190 | return type; 191 | } 192 | 193 | function generateSignature(o) { 194 | if (isObject(o)) { 195 | return SHA1(Object.keys(o).map(n => n.toLowerCase()).sort().join("|")); 196 | } else { 197 | return SHA1(Object.keys(o).map(n => typeof n).sort().join("|")); 198 | } 199 | } 200 | 201 | function getValidClassName(key) { 202 | return pascalCase(key); 203 | } 204 | 205 | function getInterfaceType(key, value, classes, classesCache, classesInUse) { 206 | // get a valid className 207 | const className = getValidClassName(key); 208 | const currentObjectSignature = generateSignature(value); 209 | const isKnownClass = 210 | Object.keys(classesCache).indexOf(currentObjectSignature) !== -1; 211 | if (isKnownClass) return classesCache[currentObjectSignature]; 212 | if (classesInUse[className] !== undefined) { 213 | classesInUse[className]++; 214 | classesCache[currentObjectSignature] = className + classesInUse[className]; 215 | return classesCache[currentObjectSignature]; 216 | } 217 | classesCache[currentObjectSignature] = className; 218 | classesInUse[className] = 0; 219 | return className; 220 | } 221 | 222 | function isValueConsistent(arr) { 223 | if (!isEmpty(arr)) { 224 | arr.every(x => (isObject(x) ? "object" : typeof x)); 225 | } 226 | return true; 227 | } 228 | 229 | function analyzeObject(obj, objectName) { 230 | objectName = getInterfaceType( 231 | objectName, 232 | obj, 233 | classes, 234 | classesCache, 235 | classesInUse 236 | ); 237 | classes[objectName] = classes[objectName] || {}; 238 | 239 | Object.keys(obj).map(key => { 240 | let type = "string"; 241 | const value = obj[key]; 242 | const { types, handleArray } = langDetails; 243 | 244 | switch (true) { 245 | case isString(value): 246 | type = types.STRING; 247 | break; 248 | case isInteger(value): 249 | type = types.INTEGER; 250 | break; 251 | case isNumber(value): 252 | type = types.NUMBER; 253 | break; 254 | case isBoolean(value): 255 | type = types.BOOLEAN; 256 | break; 257 | case isArray(value): 258 | type = handleArray("", true); 259 | if (isValueConsistent(value)) { 260 | if (isEmpty(value)) { 261 | type = handleArray("", true); 262 | } else { 263 | if (isObject(value[0])) { 264 | const clsName = getInterfaceType( 265 | key, 266 | assign({}, ...value), 267 | classes, 268 | classesCache, 269 | classesInUse 270 | ); 271 | type = handleArray(clsName); 272 | setOptionalProperties(value, clsName); 273 | analyzeObject(assign({}, ...value), key); 274 | } else { 275 | type = `${handleArray(getBasicType(value[0]))}`; 276 | } 277 | } 278 | } 279 | break; 280 | case isObject(value) && !isArray(value): 281 | type = types.ANY; 282 | if (!isEmpty(value)) { 283 | type = getInterfaceType( 284 | key, 285 | value, 286 | classes, 287 | classesCache, 288 | classesInUse 289 | ); 290 | analyzeObject(value, key); 291 | } 292 | break; 293 | } 294 | if (hasSpecialChars(key)) { 295 | key = `"${key}"`; 296 | } 297 | classes[objectName][key] = type; 298 | }); 299 | 300 | return { classes, classesCache, classesInUse }; 301 | } 302 | 303 | function isOptionalKey(key, objName) { 304 | return ( 305 | optionalProperties[objName] && optionalProperties[objName].indexOf(key) >= 0 306 | ); 307 | } 308 | 309 | function setOptional(key, objName) { 310 | if (isOptionalKey(key, objName) && langDetails.optional) { 311 | return langDetails.optional; 312 | } 313 | return ""; 314 | } 315 | 316 | function rustRename(key, lang, clsName, rustCase) { 317 | const caseFunc = rustCase === "camelCase" ? camelCase : snakeCase; 318 | const casedText = 319 | hasSpecialChars(key) || rustCase === "snakeCase" ? caseFunc(key) : key; 320 | if ( 321 | lang === "rust-serde" && 322 | (rustReserved.indexOf(key) >= 0 || 323 | key.indexOf(" ") >= 0 || 324 | key !== casedText) 325 | ) { 326 | const changedKey = `${casedText.indexOf("_") >= 0 ? "" : "_"}${casedText}`; 327 | return ` #[serde(rename = "${key.replace(/"/g, "")}")]\n ${changedKey}`; 328 | } 329 | return ` ${key}${setOptional(key, clsName)}`; 330 | } 331 | 332 | export default function transform(obj, options) { 333 | obj = isString(obj) ? JSON.parse(obj) : obj; 334 | 335 | if (isArray(obj)) { 336 | obj = merge({}, ...obj); 337 | } 338 | 339 | const defaultOptions = { 340 | objectName: "_RootInterface", 341 | lang: "flow", 342 | rustCase: "camelCase" 343 | }; 344 | 345 | langDetails = {}; 346 | optionalProperties = {}; 347 | 348 | const { objectName, lang, rustCase } = merge({}, defaultOptions, options); 349 | 350 | if (rustCase !== "camelCase" && rustCase !== "snakeCase") { 351 | throw new Error("rustCase can only be 'camelCase' or 'snakeCase'."); 352 | } 353 | 354 | langDetails = mapping[lang]; 355 | let output = langDetails.preText ? `${langDetails.preText};\n\n` : ""; 356 | const localClasses = {}; 357 | classes = {}; 358 | classesCache = {}; 359 | classesInUse = {}; 360 | 361 | analyzeObject(obj, objectName); 362 | 363 | const { 364 | equator, 365 | separator, 366 | endingBrace, 367 | startingBrace, 368 | terminator, 369 | preInterface, 370 | hideTerminatorAtLast, 371 | handleOptionalValue 372 | } = langDetails; 373 | 374 | Object.keys(classes).map(clsName => { 375 | output = preInterface || ""; 376 | output += `${langDetails.interface} ${clsName}${equator} ${startingBrace}\n`; 377 | 378 | const keys = Object.keys(classes[clsName]); 379 | 380 | keys.map((key, i) => { 381 | const _separator = 382 | i === keys.length - 1 && hideTerminatorAtLast ? "" : separator; 383 | const _key = rustRename(key, lang, clsName, rustCase); 384 | let _value = classes[clsName][key]; 385 | _value = 386 | isOptionalKey(key, clsName) && handleOptionalValue 387 | ? handleOptionalValue(_value) 388 | : _value; 389 | 390 | output += `${_key}: ${_value}${_separator}\n`; 391 | }); 392 | output += `${endingBrace}${terminator}\n\n`; 393 | localClasses[clsName] = output; 394 | }); 395 | 396 | output = ""; 397 | 398 | Object.keys(localClasses).sort().forEach(key => { 399 | output += localClasses[key]; 400 | }); 401 | 402 | return output; 403 | } 404 | -------------------------------------------------------------------------------- /tests/config/setup.js: -------------------------------------------------------------------------------- 1 | require("babel-core/register"); 2 | const path = require("path"); 3 | const jsdom = require("jsdom").jsdom; 4 | 5 | const exposedProperties = ["window", "navigator", "document"]; 6 | 7 | global.document = jsdom(""); 8 | global.window = document.defaultView; 9 | Object.keys(document.defaultView).forEach(property => { 10 | if (typeof global[property] === "undefined") { 11 | exposedProperties.push(property); 12 | global[property] = document.defaultView[property]; 13 | } 14 | }); 15 | 16 | global.navigator = { 17 | userAgent: "node.js" 18 | }; 19 | global.__base = `${path.resolve()}/`; 20 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import transform from '../src' 3 | 4 | const x = `{ 5 | "hello": [ 6 | { 7 | "_id": "5988946e45e52d60b33a25c7", 8 | "type": 50.087977, 9 | "aB": "abc", 10 | "longitude": 72.167197, 11 | "tags": [ 12 | "nulla", 13 | "ullamco" 14 | ], 15 | "@friends": [ 16 | { 17 | "id": 0, 18 | "name": "Robinson Woods" 19 | }, 20 | { 21 | "id": 1, 22 | "name": "Lottie Hogan", 23 | "jhkjh":"lklkj" 24 | } 25 | ] 26 | }, 27 | { 28 | "_id": "5988946ef6090217857d7b0f", 29 | "type": 47.460772, 30 | "a b": "abc", 31 | "longitude": 85.95137, 32 | "tags": [ 33 | "aliqua", 34 | "nulla" 35 | ], 36 | "@friends": [ 37 | { 38 | "id": 0, 39 | "name": "Mamie Wyatt" 40 | }, 41 | { 42 | "id": 1, 43 | "name": "Alejandra Mcdaniel" 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | `; 50 | 51 | test('should return correct flow typings', t => { 52 | t.snapshot(transform(x)) 53 | }) 54 | 55 | test('should return correct flow typings if array is passed', t => { 56 | t.snapshot(transform( 57 | [{ 58 | a: 'hello' 59 | }, { 60 | b: "hi" 61 | }] 62 | )) 63 | }) 64 | 65 | test('should return correct typescript interfaces', t => { 66 | t.snapshot(transform(x, { 67 | lang: 'typescript' 68 | })) 69 | }) 70 | 71 | test('should return correct rust-serde struct with camel case option', t => { 72 | t.snapshot(transform(x, { 73 | lang: "rust-serde" 74 | })) 75 | }) 76 | 77 | test('should return correct rust-serde struct with snake case option', t => { 78 | t.snapshot(transform(x, { 79 | lang: "rust-serde", 80 | rustCase: "snakeCase" 81 | })) 82 | }) 83 | 84 | test('should return correct Scala Case Class', t => { 85 | t.snapshot(transform(x, { 86 | lang: "scala" 87 | })) 88 | }) 89 | 90 | test('should return correct Typings when objects have different keys in an Array', t => { 91 | const json = `{ 92 | "x": [{ 93 | "a": "aa", 94 | "b": "bb" 95 | }, { 96 | "a": "aa", 97 | "c": "cc" 98 | }] 99 | }` 100 | 101 | t.snapshot(transform(json)) 102 | }) 103 | 104 | test('should return correct io-ts typings', t => { 105 | t.snapshot(transform(x, { 106 | lang: "io-ts" 107 | })) 108 | }) 109 | 110 | test('should return correct io-ts typings if array is passed', t => { 111 | t.snapshot(transform( 112 | [{ 113 | a: 'hello' 114 | }, { 115 | b: "hi" 116 | }], 117 | { 118 | lang: "io-ts" 119 | } 120 | )) 121 | }) 122 | 123 | test('should return correct sarcastic typings', t => { 124 | t.snapshot(transform(x, { 125 | lang: "sarcastic" 126 | })) 127 | }) 128 | 129 | test('should return correct sarcastic typings if array is passed', t => { 130 | t.snapshot(transform( 131 | [{ 132 | a: 'hello' 133 | }, { 134 | b: "hi" 135 | }], 136 | { 137 | lang: "sarcastic" 138 | } 139 | )) 140 | }) 141 | -------------------------------------------------------------------------------- /tests/snapshots/index.test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `tests/index.test.js` 2 | 3 | The actual snapshot is saved in `index.test.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## should return correct Scala Case Class 8 | 9 | > Snapshot 1 10 | 11 | `case class Friends (␊ 12 | id: Int,␊ 13 | name: String␊ 14 | )␊ 15 | ␊ 16 | case class Hello (␊ 17 | _id: String,␊ 18 | type: Double,␊ 19 | aB: Option[String],␊ 20 | longitude: Double,␊ 21 | tags: Seq[String],␊ 22 | "@friends": Seq[Friends],␊ 23 | "a b": String␊ 24 | )␊ 25 | ␊ 26 | case class RootInterface (␊ 27 | hello: Seq[Hello]␊ 28 | )␊ 29 | ␊ 30 | ` 31 | 32 | ## should return correct Typings when objects have different keys in an Array 33 | 34 | > Snapshot 1 35 | 36 | `type RootInterface = {␊ 37 | x: X[],␊ 38 | };␊ 39 | ␊ 40 | type X = {␊ 41 | a: string,␊ 42 | b?: string,␊ 43 | c?: string,␊ 44 | };␊ 45 | ␊ 46 | ` 47 | 48 | ## should return correct flow typings 49 | 50 | > Snapshot 1 51 | 52 | `type Friends = {␊ 53 | id: number,␊ 54 | name: string,␊ 55 | };␊ 56 | ␊ 57 | type Hello = {␊ 58 | _id: string,␊ 59 | type: number,␊ 60 | aB?: string,␊ 61 | longitude: number,␊ 62 | tags: string[],␊ 63 | "@friends": Friends[],␊ 64 | "a b": string,␊ 65 | };␊ 66 | ␊ 67 | type RootInterface = {␊ 68 | hello: Hello[],␊ 69 | };␊ 70 | ␊ 71 | ` 72 | 73 | ## should return correct flow typings if array is passed 74 | 75 | > Snapshot 1 76 | 77 | `type RootInterface = {␊ 78 | a: string,␊ 79 | b: string,␊ 80 | };␊ 81 | ␊ 82 | ` 83 | 84 | ## should return correct io-ts typings 85 | 86 | > Snapshot 1 87 | 88 | `const Friends = t.type( {␊ 89 | id: t.Integer,␊ 90 | name: t.string,␊ 91 | });␊ 92 | ␊ 93 | const Hello = t.type( {␊ 94 | _id: t.string,␊ 95 | type: t.number,␊ 96 | aB: t.string,␊ 97 | longitude: t.number,␊ 98 | tags: t.array(t.string),␊ 99 | "@friends": t.array(Friends),␊ 100 | "a b": t.string,␊ 101 | });␊ 102 | ␊ 103 | const RootInterface = t.type( {␊ 104 | hello: t.array(Hello),␊ 105 | });␊ 106 | ␊ 107 | ` 108 | 109 | ## should return correct io-ts typings if array is passed 110 | 111 | > Snapshot 1 112 | 113 | `const RootInterface = t.type( {␊ 114 | a: t.string,␊ 115 | b: t.string,␊ 116 | });␊ 117 | ␊ 118 | ` 119 | 120 | ## should return correct rust-serde struct with camel case option 121 | 122 | > Snapshot 1 123 | 124 | `#[derive(Serialize, Deserialize)]␊ 125 | struct Friends {␊ 126 | id: i64,␊ 127 | name: String,␊ 128 | }␊ 129 | ␊ 130 | #[derive(Serialize, Deserialize)]␊ 131 | struct Hello {␊ 132 | _id: String,␊ 133 | #[serde(rename = "type")]␊ 134 | _type: f64,␊ 135 | aB: Option,␊ 136 | longitude: f64,␊ 137 | tags: Vec,␊ 138 | #[serde(rename = "@friends")]␊ 139 | _friends: Vec,␊ 140 | #[serde(rename = "a b")]␊ 141 | _aB: String,␊ 142 | }␊ 143 | ␊ 144 | #[derive(Serialize, Deserialize)]␊ 145 | struct RootInterface {␊ 146 | hello: Vec,␊ 147 | }␊ 148 | ␊ 149 | ` 150 | 151 | ## should return correct rust-serde struct with snake case option 152 | 153 | > Snapshot 1 154 | 155 | `#[derive(Serialize, Deserialize)]␊ 156 | struct Friends {␊ 157 | id: i64,␊ 158 | name: String,␊ 159 | }␊ 160 | ␊ 161 | #[derive(Serialize, Deserialize)]␊ 162 | struct Hello {␊ 163 | #[serde(rename = "_id")]␊ 164 | _id: String,␊ 165 | #[serde(rename = "type")]␊ 166 | _type: f64,␊ 167 | #[serde(rename = "aB")]␊ 168 | a_b: Option,␊ 169 | longitude: f64,␊ 170 | tags: Vec,␊ 171 | #[serde(rename = "@friends")]␊ 172 | _friends: Vec,␊ 173 | #[serde(rename = "a b")]␊ 174 | a_b: String,␊ 175 | }␊ 176 | ␊ 177 | #[derive(Serialize, Deserialize)]␊ 178 | struct RootInterface {␊ 179 | hello: Vec,␊ 180 | }␊ 181 | ␊ 182 | ` 183 | 184 | ## should return correct sarcastic typings 185 | 186 | > Snapshot 1 187 | 188 | `const Friends = is.shape( {␊ 189 | id: is.number,␊ 190 | name: is.string,␊ 191 | });␊ 192 | ␊ 193 | const Hello = is.shape( {␊ 194 | _id: is.string,␊ 195 | type: is.number,␊ 196 | aB: is.maybe(is.string),␊ 197 | longitude: is.number,␊ 198 | tags: is.arrayOf(is.string),␊ 199 | "@friends": is.arrayOf(Friends),␊ 200 | "a b": is.string,␊ 201 | });␊ 202 | ␊ 203 | const RootInterface = is.shape( {␊ 204 | hello: is.arrayOf(Hello),␊ 205 | });␊ 206 | ␊ 207 | ` 208 | 209 | ## should return correct sarcastic typings if array is passed 210 | 211 | > Snapshot 1 212 | 213 | `const RootInterface = is.shape( {␊ 214 | a: is.string,␊ 215 | b: is.string,␊ 216 | });␊ 217 | ␊ 218 | ` 219 | 220 | ## should return correct typescript interfaces 221 | 222 | > Snapshot 1 223 | 224 | `interface Friends {␊ 225 | id: number;␊ 226 | name: string;␊ 227 | }␊ 228 | ␊ 229 | interface Hello {␊ 230 | _id: string;␊ 231 | type: number;␊ 232 | aB?: string;␊ 233 | longitude: number;␊ 234 | tags: string[];␊ 235 | "@friends": Friends[];␊ 236 | "a b": string;␊ 237 | }␊ 238 | ␊ 239 | interface RootInterface {␊ 240 | hello: Hello[];␊ 241 | }␊ 242 | ␊ 243 | ` 244 | -------------------------------------------------------------------------------- /tests/snapshots/index.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transform-it/transform-json-types/5d1335ad5398bf61771f7008b54bf7faa1792547/tests/snapshots/index.test.js.snap --------------------------------------------------------------------------------