├── .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 | [](https://travis-ci.org/rhysd/api-dts)
4 | [](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 |
--------------------------------------------------------------------------------