├── .travis.yml ├── LICENSE.txt ├── README.md ├── apidts ├── json2tsif.go ├── json2tsif_test.go ├── stringize.go └── stringize_test.go └── main.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: tip 3 | sudo: false 4 | before_install: 5 | - go get github.com/axw/gocov/gocov 6 | - go get github.com/mattn/goveralls 7 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 8 | install: npm install -g typescript 9 | script: 10 | - go test -coverprofile=coverage.out ./apidts 11 | - $HOME/gopath/bin/goveralls -coverprofile coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 rhysd 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 15 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 16 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 17 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 18 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `d.ts` generator using JSON API response 2 | ======================================== 3 | [![Build Status](https://travis-ci.org/rhysd/api-dts.svg)](https://travis-ci.org/rhysd/api-dts) 4 | [![Coverage Status](https://coveralls.io/repos/rhysd/api-dts/badge.svg?branch=master&service=github)](https://coveralls.io/github/rhysd/api-dts?branch=master) 5 | 6 | `api-dts` is a generator for TypeScript programmer who use some JSON APIs. API response JSON has too many fields to write the type definition for it manually. `api-dts` generates such an annoying type definition automatically. 7 | 8 | ``` 9 | $ api-dts some-api.json > some-api.d.ts 10 | ``` 11 | 12 | `api-dts` reads JSON text from file specified as argument and simply writes the result to STDOUT. If command argument is ommited, `api-dts` reads STDIN. `api-dts` defines interface name of the API from the specified file name, so specifying `-out` prefers to redirecting to file. 13 | You can install `api-dts` with `go get`. 14 | 15 | ``` 16 | go get github.com/rhysd/api-dts 17 | ``` 18 | 19 | ## Example 20 | 21 | Assume that below JSON is API response. 22 | 23 | ```json 24 | [ 25 | { 26 | "user": { 27 | "name": "rhysd", 28 | "age": 27, 29 | "lang": "Dachs" 30 | }, 31 | "has_progress": false 32 | }, 33 | { 34 | "user": { 35 | "name": "linda", 36 | "age": 24, 37 | "lang": "scala" 38 | }, 39 | "has_progress": true 40 | } 41 | ] 42 | ``` 43 | 44 | `$ api-dts my-api-user.json > my-api.d.ts` generates below type definition. 45 | 46 | ```typescript 47 | interface MyApiUser { 48 | user: { 49 | age: number; 50 | lang: string; 51 | name: string; 52 | }; 53 | progress: boolean; 54 | } 55 | ``` 56 | 57 | ## TODO 58 | 59 | - Seprate sub interfaces. Their names are made using the key names of them. 60 | - Detect optional field (suffix `?`) 61 | - When the JSON is an array, check all elements have the same interface 62 | 63 | ## Real World Example 64 | 65 | In general, API document shows an example response. You can simply copy it. 66 | 67 | For example, below is `GET users/show` Twitter API response shown in [document](https://dev.twitter.com/rest/reference/get/users/show). Assume that you copied it as `twitter-user.json`. 68 | 69 | ```json 70 | { 71 | "contributors_enabled": false, 72 | "created_at": "Sat Dec 14 04:35:55 +0000 2013", 73 | "default_profile": false, 74 | "default_profile_image": false, 75 | "description": "Developer and Platform Relations @Twitter. We are developer advocates. We can't answer all your questions, but we listen to all of them!", 76 | "entities": { 77 | "description": { 78 | "urls": [] 79 | }, 80 | "url": { 81 | "urls": [ 82 | { 83 | "display_url": "dev.twitter.com", 84 | "expanded_url": "https://dev.twitter.com/", 85 | "indices": [ 86 | 0, 87 | 23 88 | ], 89 | "url": "https://t.co/66w26cua1O" 90 | } 91 | ] 92 | } 93 | }, 94 | "favourites_count": 757, 95 | "follow_request_sent": false, 96 | "followers_count": 143916, 97 | "following": false, 98 | "friends_count": 1484, 99 | "geo_enabled": true, 100 | "id": 2244994945, 101 | "id_str": "2244994945", 102 | "is_translation_enabled": false, 103 | "is_translator": false, 104 | "lang": "en", 105 | "listed_count": 516, 106 | "location": "Internet", 107 | "name": "TwitterDev", 108 | "notifications": false, 109 | "profile_background_color": "FFFFFF", 110 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", 111 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", 112 | "profile_background_tile": false, 113 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/2244994945/1396995246", 114 | "profile_image_url": "http://pbs.twimg.com/profile_images/530814764687949824/npQQVkq8_normal.png", 115 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/530814764687949824/npQQVkq8_normal.png", 116 | "profile_link_color": "0084B4", 117 | "profile_location": null, 118 | "profile_sidebar_border_color": "FFFFFF", 119 | "profile_sidebar_fill_color": "DDEEF6", 120 | "profile_text_color": "333333", 121 | "profile_use_background_image": false, 122 | "protected": false, 123 | "screen_name": "TwitterDev", 124 | "status": { 125 | "contributors": null, 126 | "coordinates": null, 127 | "created_at": "Fri Jun 12 19:50:18 +0000 2015", 128 | "entities": { 129 | "hashtags": [], 130 | "symbols": [], 131 | "urls": [ 132 | { 133 | "display_url": "github.com/twitterdev/twi\u2026", 134 | "expanded_url": "https://github.com/twitterdev/twitter-for-bigquery", 135 | "indices": [ 136 | 36, 137 | 59 138 | ], 139 | "url": "https://t.co/K5orgXzhOM" 140 | } 141 | ], 142 | "user_mentions": [ 143 | { 144 | "id": 18518601, 145 | "id_str": "18518601", 146 | "indices": [ 147 | 3, 148 | 13 149 | ], 150 | "name": "William Vambenepe", 151 | "screen_name": "vambenepe" 152 | } 153 | ] 154 | }, 155 | "favorite_count": 0, 156 | "favorited": false, 157 | "geo": null, 158 | "id": 609447655429787648, 159 | "id_str": "609447655429787648", 160 | "in_reply_to_screen_name": null, 161 | "in_reply_to_status_id": null, 162 | "in_reply_to_status_id_str": null, 163 | "in_reply_to_user_id": null, 164 | "in_reply_to_user_id_str": null, 165 | "lang": "en", 166 | "place": null, 167 | "possibly_sensitive": false, 168 | "retweet_count": 19, 169 | "retweeted": false, 170 | "retweeted_status": { 171 | "contributors": null, 172 | "coordinates": null, 173 | "created_at": "Fri Jun 12 05:19:11 +0000 2015", 174 | "entities": { 175 | "hashtags": [], 176 | "symbols": [], 177 | "urls": [ 178 | { 179 | "display_url": "github.com/twitterdev/twi\u2026", 180 | "expanded_url": "https://github.com/twitterdev/twitter-for-bigquery", 181 | "indices": [ 182 | 21, 183 | 44 184 | ], 185 | "url": "https://t.co/K5orgXzhOM" 186 | } 187 | ], 188 | "user_mentions": [] 189 | }, 190 | "favorite_count": 23, 191 | "favorited": false, 192 | "geo": null, 193 | "id": 609228428915552257, 194 | "id_str": "609228428915552257", 195 | "in_reply_to_screen_name": null, 196 | "in_reply_to_status_id": null, 197 | "in_reply_to_status_id_str": null, 198 | "in_reply_to_user_id": null, 199 | "in_reply_to_user_id_str": null, 200 | "lang": "en", 201 | "place": null, 202 | "possibly_sensitive": false, 203 | "retweet_count": 19, 204 | "retweeted": false, 205 | "source": "Twitter Web Client", 206 | "text": "Twitter for BigQuery https://t.co/K5orgXzhOM See how easy it is to stream Twitter data into BigQuery.", 207 | "truncated": false 208 | }, 209 | "source": "Twitter for iPhone", 210 | "text": "RT @vambenepe: Twitter for BigQuery https://t.co/K5orgXzhOM See how easy it is to stream Twitter data into BigQuery.", 211 | "truncated": false 212 | }, 213 | "statuses_count": 1279, 214 | "time_zone": "Pacific Time (US & Canada)", 215 | "url": "https://t.co/66w26cua1O", 216 | "utc_offset": -25200, 217 | "verified": true 218 | } 219 | ``` 220 | 221 | Then execute 222 | 223 | ``` 224 | $ api-dts twitter-user.json > twitter-user.d.ts 225 | ``` 226 | 227 | It writes type definition to `twitter-user.d.ts` for the API as below. 228 | 229 | ```typescript 230 | interface TwitterUser { 231 | time_zone: string; 232 | created_at: string; 233 | screen_name: string; 234 | following: boolean; 235 | listed_count: number; 236 | description: string; 237 | id: number; 238 | profile_background_color: string; 239 | location: string; 240 | default_profile: boolean; 241 | is_translator: boolean; 242 | profile_background_image_url_https: string; 243 | statuses_count: number; 244 | name: string; 245 | profile_text_color: string; 246 | contributors_enabled: boolean; 247 | profile_banner_url: string; 248 | profile_image_url_https: string; 249 | friends_count: number; 250 | profile_link_color: string; 251 | geo_enabled: boolean; 252 | is_translation_enabled: boolean; 253 | favourites_count: number; 254 | notifications: boolean; 255 | profile_background_tile: boolean; 256 | profile_image_url: string; 257 | utc_offset: number; 258 | profile_sidebar_fill_color: string; 259 | protected: boolean; 260 | profile_location: any; 261 | lang: string; 262 | default_profile_image: boolean; 263 | id_str: string; 264 | status: { 265 | contributors: any; 266 | id: number; 267 | in_reply_to_user_id: any; 268 | retweet_count: number; 269 | truncated: boolean; 270 | possibly_sensitive: boolean; 271 | source: string; 272 | geo: any; 273 | place: any; 274 | retweeted_status: { 275 | favorite_count: number; 276 | geo: any; 277 | in_reply_to_status_id: any; 278 | possibly_sensitive: boolean; 279 | truncated: boolean; 280 | created_at: string; 281 | text: string; 282 | entities: { 283 | user_mentions: any[]; 284 | hashtags: any[]; 285 | symbols: any[]; 286 | urls: { 287 | expanded_url: string; 288 | indices: number[]; 289 | url: string; 290 | display_url: string; 291 | }[]; 292 | }; 293 | favorited: boolean; 294 | in_reply_to_user_id: any; 295 | retweeted: boolean; 296 | in_reply_to_user_id_str: any; 297 | contributors: any; 298 | coordinates: any; 299 | place: any; 300 | retweet_count: number; 301 | source: string; 302 | in_reply_to_status_id_str: any; 303 | lang: string; 304 | id_str: string; 305 | in_reply_to_screen_name: any; 306 | id: number; 307 | }; 308 | text: string; 309 | retweeted: boolean; 310 | created_at: string; 311 | in_reply_to_status_id: any; 312 | lang: string; 313 | coordinates: any; 314 | favorite_count: number; 315 | entities: { 316 | urls: { 317 | display_url: string; 318 | expanded_url: string; 319 | indices: number[]; 320 | url: string; 321 | }[]; 322 | user_mentions: { 323 | id: number; 324 | id_str: string; 325 | indices: number[]; 326 | name: string; 327 | screen_name: string; 328 | }[]; 329 | hashtags: any[]; 330 | symbols: any[]; 331 | }; 332 | in_reply_to_screen_name: any; 333 | id_str: string; 334 | in_reply_to_status_id_str: any; 335 | favorited: boolean; 336 | in_reply_to_user_id_str: any; 337 | }; 338 | profile_sidebar_border_color: string; 339 | profile_background_image_url: string; 340 | url: string; 341 | entities: { 342 | url: { 343 | urls: { 344 | expanded_url: string; 345 | indices: number[]; 346 | url: string; 347 | display_url: string; 348 | }[]; 349 | }; 350 | description: { 351 | urls: any[]; 352 | }; 353 | }; 354 | followers_count: number; 355 | profile_use_background_image: boolean; 356 | follow_request_sent: boolean; 357 | verified: boolean; 358 | } 359 | ``` 360 | -------------------------------------------------------------------------------- /apidts/json2tsif.go: -------------------------------------------------------------------------------- 1 | package apidts 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | type TypeToken uint 9 | 10 | const ( 11 | TypeUnknown TypeToken = iota 12 | TypeBoolean 13 | TypeNumber 14 | TypeString 15 | TypeArray 16 | TypeObject 17 | TypeNull 18 | ) 19 | 20 | type TypeScriptDef struct { 21 | token TypeToken 22 | elem_type *TypeScriptDef 23 | fields map[string]*TypeScriptDef 24 | } 25 | 26 | func NewTypeScriptDef(t TypeToken) *TypeScriptDef { 27 | return &TypeScriptDef{ 28 | t, 29 | nil, 30 | nil, 31 | } 32 | } 33 | 34 | func convertToArrayDef(a []interface{}) *TypeScriptDef { 35 | def := NewTypeScriptDef(TypeArray) 36 | if len(a) == 0 { 37 | elem := NewTypeScriptDef(TypeNull) 38 | def.elem_type = elem 39 | return def 40 | } 41 | 42 | // Note: Should check all array element types are the same 43 | def.elem_type = convertToDef(a[0]) 44 | return def 45 | } 46 | 47 | func convertToObjDef(m map[string]interface{}) *TypeScriptDef { 48 | def := NewTypeScriptDef(TypeObject) 49 | def.fields = make(map[string]*TypeScriptDef, len(m)) 50 | 51 | for n, t := range m { 52 | def.fields[n] = convertToDef(t) 53 | } 54 | return def 55 | } 56 | 57 | func convertToDef(val interface{}) *TypeScriptDef { 58 | switch t := val.(type) { 59 | case bool: 60 | return NewTypeScriptDef(TypeBoolean) 61 | case float64: 62 | return NewTypeScriptDef(TypeNumber) 63 | case string: 64 | return NewTypeScriptDef(TypeString) 65 | case []interface{}: 66 | return convertToArrayDef(t) 67 | case map[string]interface{}: 68 | return convertToObjDef(t) 69 | default: 70 | return NewTypeScriptDef(TypeNull) 71 | } 72 | } 73 | 74 | func ConvertJsonToDts(input io.Reader) (*TypeScriptDef, error) { 75 | var decoded interface{} 76 | if err := json.NewDecoder(input).Decode(&decoded); err != nil { 77 | return nil, err 78 | } 79 | return convertToDef(decoded), nil 80 | } 81 | -------------------------------------------------------------------------------- /apidts/json2tsif_test.go: -------------------------------------------------------------------------------- 1 | package apidts 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestNewTypeScriptDef(t *testing.T) { 9 | d := NewTypeScriptDef(TypeUnknown) 10 | if d.token != TypeUnknown { 11 | t.Errorf("Want %v but %v", TypeUnknown, d) 12 | } 13 | if d.elem_type != nil { 14 | t.Errorf("'elem_type' must be nil on init but %v", d.elem_type) 15 | } 16 | if d.fields != nil { 17 | t.Errorf("'fields' must be nil on init but %v", d.fields) 18 | } 19 | } 20 | 21 | func TestConvertJsonToDts(t *testing.T) { 22 | reader := func(s string) *strings.Reader { 23 | return strings.NewReader(s) 24 | } 25 | 26 | var e error 27 | 28 | b, e := ConvertJsonToDts(reader("true")) 29 | if e != nil { 30 | t.Errorf("b must not occur an error: %v", e) 31 | } 32 | if b.token != TypeBoolean { 33 | t.Errorf("Type must be boolean but %v", b.token) 34 | } 35 | 36 | n, e := ConvertJsonToDts(reader("42")) 37 | if e != nil { 38 | t.Errorf("n must not occur an error: %v", e) 39 | } 40 | if n.token != TypeNumber { 41 | t.Errorf("Type must be number but %v", n.token) 42 | } 43 | 44 | s, e := ConvertJsonToDts(reader(`"foo"`)) 45 | if e != nil { 46 | t.Errorf("s must not occur an error: %v", e) 47 | } 48 | if s.token != TypeString { 49 | t.Errorf("Type must be string but %v", s.token) 50 | } 51 | 52 | n2, e := ConvertJsonToDts(reader("null")) 53 | if e != nil { 54 | t.Errorf("n2 must not occur an error: %v", e) 55 | } 56 | if n2.token != TypeNull { 57 | t.Errorf("Type must be null but %v", n2.token) 58 | } 59 | 60 | a, e := ConvertJsonToDts(reader("[1, 2, 3]")) 61 | if e != nil { 62 | t.Errorf("a must not occur an error: %v", e) 63 | } 64 | if a.token != TypeArray { 65 | t.Errorf("Type must be array but %v", a.token) 66 | } 67 | if a.elem_type == nil { 68 | t.Errorf("'elem_type' must not be nil for array") 69 | } 70 | if a.elem_type.token != TypeNumber { 71 | t.Errorf("'elem_type' must be element type number but actually %v", a.elem_type.token) 72 | } 73 | 74 | if r, _ := ConvertJsonToDts(reader("[]")); r.elem_type.token != TypeNull { 75 | t.Errorf("'elem_type' of empty array must be null") 76 | } 77 | 78 | o, e := ConvertJsonToDts(reader(`{"foo": 1, "bar": 2}`)) 79 | if e != nil { 80 | t.Errorf("o must not occur an error: %v", e) 81 | } 82 | if o.token != TypeObject { 83 | t.Errorf("Type must be object but %v", o.token) 84 | } 85 | if o.fields == nil { 86 | t.Errorf("'fields' must not be nil for object") 87 | } 88 | if o.fields["foo"].token != TypeNumber { 89 | t.Errorf("'foo' field must have TypeNumber token but %v", o.fields["foo"].token) 90 | } 91 | 92 | o2, e := ConvertJsonToDts(reader(`{"foo": {"poyo": true}, "bar": {"puyo": false}}`)) 93 | if e != nil { 94 | t.Errorf("o2 must not occur an error: %v", e) 95 | } 96 | if o2.fields["foo"].token != TypeObject { 97 | t.Errorf("'foo' field must have TypeObject token but %v", o2.fields["foo"].token) 98 | } 99 | if o2.fields["foo"].fields["poyo"].token != TypeBoolean { 100 | t.Errorf("'foo' field's child must have field 'poyo' and its token TypeBoolean but %v", o2.fields["foo"].fields["foo"].token) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /apidts/stringize.go: -------------------------------------------------------------------------------- 1 | package apidts 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | type StringizeError struct { 11 | token TypeToken 12 | } 13 | 14 | func (e *StringizeError) Error() string { 15 | return fmt.Sprintf("Invalid type token: Input must be object or array of object but token was %s", e.token) 16 | } 17 | 18 | func camelize(str string) string { 19 | buf := new(bytes.Buffer) 20 | heading := true 21 | for _, c := range str { 22 | if unicode.IsLetter(c) || unicode.IsDigit(c) { 23 | if heading { 24 | heading = false 25 | buf.WriteRune(unicode.ToUpper(c)) 26 | } else { 27 | buf.WriteRune(c) 28 | } 29 | } else { 30 | heading = true 31 | } 32 | } 33 | return buf.String() 34 | } 35 | 36 | // TODO: Add indent string customization 37 | type DtsStringizer struct { 38 | counter uint 39 | indent uint 40 | buffer bytes.Buffer 41 | } 42 | 43 | func NewDtsStringizer() DtsStringizer { 44 | return DtsStringizer{0, 0, bytes.Buffer{}} 45 | } 46 | 47 | func (s *DtsStringizer) write(str string) { 48 | s.buffer.WriteString(str) 49 | } 50 | 51 | func (s *DtsStringizer) writeIndent() { 52 | for i := uint(0); i < s.indent; i++ { 53 | s.write(" ") 54 | } 55 | } 56 | 57 | func (s *DtsStringizer) visit(dts *TypeScriptDef) { 58 | switch dts.token { 59 | case TypeBoolean: 60 | s.write(" boolean") 61 | case TypeNumber: 62 | s.write(" number") 63 | case TypeString: 64 | s.write(" string") 65 | case TypeArray: 66 | s.visit(dts.elem_type) 67 | s.write("[]") 68 | case TypeObject: 69 | s.write(" {\n") 70 | s.indent += 1 71 | for n, f := range dts.fields { 72 | s.writeIndent() 73 | s.write(n) 74 | s.write(":") 75 | s.visit(f) 76 | s.write(";\n") 77 | } 78 | s.indent -= 1 79 | s.writeIndent() 80 | s.write("}") 81 | case TypeNull: 82 | s.write(" any") 83 | case TypeUnknown: 84 | panic("Unknown type token") 85 | default: 86 | panic(fmt.Sprintf("Invalid type token: %d", dts.token)) 87 | } 88 | } 89 | 90 | func (s *DtsStringizer) Stringize(dts *TypeScriptDef, hint string) (string, error) { 91 | if dts.token == TypeArray { 92 | return s.Stringize(dts.elem_type, hint) 93 | } 94 | 95 | if dts.token != TypeObject { 96 | return "", &StringizeError{dts.token} 97 | } 98 | 99 | s.writeIndent() 100 | s.write("interface ") 101 | if hint != "" { 102 | idx := strings.IndexRune(hint, '.') 103 | if idx == -1 { 104 | s.write(camelize(hint)) 105 | } else { 106 | s.write(camelize(hint[:idx])) 107 | } 108 | } else { 109 | s.write("FixMe") 110 | } 111 | s.visit(dts) 112 | return s.buffer.String(), nil 113 | } 114 | 115 | func StringizeDts(dts *TypeScriptDef, hint string) (string, error) { 116 | s := NewDtsStringizer() 117 | r, err := (&s).Stringize(dts, hint) 118 | if err != nil { 119 | return "", err 120 | } 121 | return r, nil 122 | } 123 | -------------------------------------------------------------------------------- /apidts/stringize_test.go: -------------------------------------------------------------------------------- 1 | package apidts 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func convertThenStringizeWithHint(s string, h string) (string, error) { 11 | reader := strings.NewReader(s) 12 | var e error 13 | 14 | d, e := ConvertJsonToDts(reader) 15 | if e != nil { 16 | return "", e 17 | } 18 | 19 | r, e := StringizeDts(d, h) 20 | if e != nil { 21 | return "", e 22 | } 23 | 24 | return r, nil 25 | } 26 | 27 | func convertThenStringize(s string) (string, error) { 28 | return convertThenStringizeWithHint(s, "") 29 | } 30 | 31 | func testCompileWithTsc(s string) error { 32 | code, e1 := convertThenStringize(s) 33 | if e1 != nil { 34 | return e1 35 | } 36 | 37 | t, e2 := os.Create("test.ts") 38 | if e2 != nil { 39 | panic(e2.Error()) 40 | } 41 | defer t.Close() 42 | defer os.Remove(t.Name()) 43 | 44 | t.WriteString(code) 45 | 46 | _, e3 := exec.Command("tsc", "--noEmit", t.Name()).Output() 47 | if e3 != nil { 48 | return e3 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func TestCamelize(t *testing.T) { 55 | check := func(input string, expected string) { 56 | actual := camelize(input) 57 | if actual != expected { 58 | t.Errorf("camelize() should modify %s to %s but actually %s", input, expected, actual) 59 | } 60 | } 61 | check("aaa", "Aaa") 62 | check(".aaa", "Aaa") 63 | check("aaa-bbb", "AaaBbb") 64 | check("foo_bar", "FooBar") 65 | check("aaa-$-bbb", "AaaBbb") 66 | check("tsura poyo", "TsuraPoyo") 67 | check("aAa-BbB", "AAaBbB") 68 | check("UhhNyaa", "UhhNyaa") 69 | check("-", "") 70 | check("", "") 71 | } 72 | 73 | func TestStringizeDts(t *testing.T) { 74 | 75 | check := func(json string, expected string) { 76 | actual, e := convertThenStringize(json) 77 | if e != nil { 78 | t.Errorf("'%s' must not occur error", json) 79 | } 80 | if actual != expected { 81 | t.Errorf("Expected '%v' but actually '%v'", expected, actual) 82 | } 83 | } 84 | 85 | check(`{"foo": true}`, `interface FixMe { 86 | foo: boolean; 87 | }`) 88 | check(`[{"foo": true}, {"foo": false}]`, `interface FixMe { 89 | foo: boolean; 90 | }`) 91 | check(`{"bar": 42}`, `interface FixMe { 92 | bar: number; 93 | }`) 94 | check(`{"foo": [1, 2, 3]}`, `interface FixMe { 95 | foo: number[]; 96 | }`) 97 | check(`{"foo": {"poyo": [1, 2, 3]}}`, `interface FixMe { 98 | foo: { 99 | poyo: number[]; 100 | }; 101 | }`) 102 | check(`{"foo": {"poyo": {"puyo": [true]}}}`, `interface FixMe { 103 | foo: { 104 | poyo: { 105 | puyo: boolean[]; 106 | }; 107 | }; 108 | }`) 109 | check(`{"foo": []}`, `interface FixMe { 110 | foo: any[]; 111 | }`) 112 | check(`{"foo": null}`, `interface FixMe { 113 | foo: any; 114 | }`) 115 | 116 | if _, e := convertThenStringize("true"); e == nil { 117 | t.Errorf("Can't convert 'true' to interface but error does't occur") 118 | } 119 | 120 | if _, e := convertThenStringize("true"); strings.Index(e.Error(), "Invalid type token: ") != 0 { 121 | t.Errorf("Returned error must be StringizeError") 122 | } 123 | 124 | if _, e := convertThenStringize("42"); e == nil { 125 | t.Errorf("Can't convert 'true' to interface but error does't occur") 126 | } 127 | 128 | if _, e := convertThenStringize("null"); e == nil { 129 | t.Errorf("Can't convert 'true' to interface but error does't occur") 130 | } 131 | 132 | hinted, e := convertThenStringizeWithHint(`{"aaa": 0}`, "foo") 133 | if e != nil { 134 | t.Errorf("Can't convert with hint") 135 | } 136 | if strings.Index(hinted, "Foo") == -1 { 137 | t.Errorf("Hint 'foo' does not reflect to result: %s", hinted) 138 | } 139 | 140 | testCompileWithTsc(`{"foo": "aaa", "bar": {"poyo": [true, false, true], "puyo": {"aaa": 42}}}`) 141 | testCompileWithTsc(`[ 142 | { 143 | "contributors": null, 144 | "coordinates": null, 145 | "created_at": "Tue Sep 01 08:40:28 +0000 2015", 146 | "entities": { 147 | "hashtags": [], 148 | "symbols": [], 149 | "urls": [ 150 | { 151 | "display_url": "twitter.com/Linda_pp/statu…", 152 | "expanded_url": "https://twitter.com/Linda_pp/status/638402862934986752", 153 | "indices": [ 154 | 6, 155 | 29 156 | ], 157 | "url": "https://t.co/vZil2mnSvQ" 158 | } 159 | ], 160 | "user_mentions": [] 161 | }, 162 | "favorite_count": 0, 163 | "favorited": false, 164 | "filter_level": "low", 165 | "geo": null, 166 | "id": 638632504522444800, 167 | "id_str": "638632504522444800", 168 | "in_reply_to_screen_name": null, 169 | "in_reply_to_status_id": null, 170 | "in_reply_to_status_id_str": null, 171 | "in_reply_to_user_id": null, 172 | "in_reply_to_user_id_str": null, 173 | "is_quote_status": true, 174 | "lang": "ja", 175 | "place": null, 176 | "possibly_sensitive": false, 177 | "quoted_status": { 178 | "contributors": null, 179 | "coordinates": null, 180 | "created_at": "Mon Aug 31 17:27:58 +0000 2015", 181 | "entities": { 182 | "hashtags": [], 183 | "symbols": [], 184 | "urls": [], 185 | "user_mentions": [] 186 | }, 187 | "favorite_count": 0, 188 | "favorited": false, 189 | "filter_level": "low", 190 | "geo": null, 191 | "id": 638402862934986800, 192 | "id_str": "638402862934986752", 193 | "in_reply_to_screen_name": null, 194 | "in_reply_to_status_id": null, 195 | "in_reply_to_status_id_str": null, 196 | "in_reply_to_user_id": null, 197 | "in_reply_to_user_id_str": null, 198 | "is_quote_status": false, 199 | "lang": "ja", 200 | "place": null, 201 | "retweet_count": 0, 202 | "retweeted": false, 203 | "source": "YoruFukurou", 204 | "text": "眠すぎるため寝ます", 205 | "truncated": false, 206 | "user": { 207 | "contributors_enabled": false, 208 | "created_at": "Thu Mar 04 17:10:18 +0000 2010", 209 | "default_profile": false, 210 | "default_profile_image": false, 211 | "description": "ソフトウェアエンジニア見習い.趣味で C++ (C++11 or later),Ruby,Dachs をVimったりする.計算機言語などのプログラミングツールが好き.Electron + TypeScript でデスクトップアプリ始めました.あと写真も楽しい.犬.", 212 | "favourites_count": 384, 213 | "follow_request_sent": null, 214 | "followers_count": 1297, 215 | "following": null, 216 | "friends_count": 373, 217 | "geo_enabled": false, 218 | "id": 119789510, 219 | "id_str": "119789510", 220 | "is_translator": false, 221 | "lang": "en", 222 | "listed_count": 149, 223 | "location": "Tokyo ^ Kanagawa", 224 | "name": "ドッグ", 225 | "notifications": null, 226 | "profile_background_color": "B3B3B3", 227 | "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/458967069522817025/VbYAPpF5.png", 228 | "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/458967069522817025/VbYAPpF5.png", 229 | "profile_background_tile": true, 230 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/119789510/1367930390", 231 | "profile_image_url": "http://pbs.twimg.com/profile_images/3626384430/3a64cf406665c1940d68ab737003605c_normal.jpeg", 232 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/3626384430/3a64cf406665c1940d68ab737003605c_normal.jpeg", 233 | "profile_link_color": "545454", 234 | "profile_sidebar_border_color": "FFFFFF", 235 | "profile_sidebar_fill_color": "E6E6E6", 236 | "profile_text_color": "050505", 237 | "profile_use_background_image": true, 238 | "protected": false, 239 | "screen_name": "Linda_pp", 240 | "statuses_count": 126429, 241 | "time_zone": "Osaka", 242 | "url": "https://github.com/rhysd", 243 | "utc_offset": 32400, 244 | "verified": false 245 | } 246 | }, 247 | "quoted_status_id": 638402862934986800, 248 | "quoted_status_id_str": "638402862934986752", 249 | "retweet_count": 0, 250 | "retweeted": false, 251 | "source": "犬Vim", 252 | "text": "テストです https://t.co/vZil2mnSvQ", 253 | "timestamp_ms": "1441096828953", 254 | "truncated": false, 255 | "user": { 256 | "contributors_enabled": false, 257 | "created_at": "Thu Mar 04 17:10:18 +0000 2010", 258 | "default_profile": false, 259 | "default_profile_image": false, 260 | "description": "ソフトウェアエンジニア見習い.趣味で C++ (C++11 or later),Ruby,Dachs をVimったりする.計算機言語などのプログラミングツールが好き.Electron + TypeScript でデスクトップアプリ始めました.あと写真も楽しい.犬.", 261 | "favourites_count": 384, 262 | "follow_request_sent": null, 263 | "followers_count": 1297, 264 | "following": null, 265 | "friends_count": 373, 266 | "geo_enabled": false, 267 | "id": 119789510, 268 | "id_str": "119789510", 269 | "is_translator": false, 270 | "lang": "en", 271 | "listed_count": 149, 272 | "location": "Tokyo ^ Kanagawa", 273 | "name": "ドッグ", 274 | "notifications": null, 275 | "profile_background_color": "B3B3B3", 276 | "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/458967069522817025/VbYAPpF5.png", 277 | "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/458967069522817025/VbYAPpF5.png", 278 | "profile_background_tile": true, 279 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/119789510/1367930390", 280 | "profile_image_url": "http://pbs.twimg.com/profile_images/3626384430/3a64cf406665c1940d68ab737003605c_normal.jpeg", 281 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/3626384430/3a64cf406665c1940d68ab737003605c_normal.jpeg", 282 | "profile_link_color": "545454", 283 | "profile_sidebar_border_color": "FFFFFF", 284 | "profile_sidebar_fill_color": "E6E6E6", 285 | "profile_text_color": "050505", 286 | "profile_use_background_image": true, 287 | "protected": false, 288 | "screen_name": "Linda_pp", 289 | "statuses_count": 126430, 290 | "time_zone": "Osaka", 291 | "url": "https://github.com/rhysd", 292 | "utc_offset": 32400, 293 | "verified": false 294 | } 295 | } 296 | ]`) 297 | } 298 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/rhysd/api-dts/apidts" 7 | "io" 8 | "os" 9 | ) 10 | 11 | func ParseArgv() (string, io.Reader) { 12 | fs := flag.NewFlagSet(fmt.Sprintf("%s [file]", os.Args[0]), flag.ExitOnError) 13 | vf := fs.Bool("version", false, "Display version") 14 | fs.Parse(os.Args[1:]) 15 | 16 | if *vf { 17 | fmt.Println("0.0.0") 18 | os.Exit(0) 19 | } 20 | 21 | args := fs.Args() 22 | if len(args) == 0 { 23 | return "", os.Stdin 24 | } 25 | 26 | f, err := os.Open(args[0]) 27 | if err != nil { 28 | fmt.Fprintln(os.Stderr, err.Error()) 29 | os.Exit(1) 30 | } 31 | 32 | return args[0], f 33 | } 34 | 35 | func main() { 36 | file_name, input := ParseArgv() 37 | 38 | var err error 39 | dts, err := apidts.ConvertJsonToDts(input) 40 | if err != nil { 41 | fmt.Fprintln(os.Stderr, err.Error()) 42 | os.Exit(1) 43 | } 44 | 45 | stringized, err := apidts.StringizeDts(dts, file_name) 46 | if err != nil { 47 | fmt.Fprintln(os.Stderr, err.Error()) 48 | os.Exit(1) 49 | } 50 | 51 | fmt.Println(stringized) 52 | } 53 | --------------------------------------------------------------------------------