azuki-api 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.travis.yml +16 -0
  4. data/Gemfile +4 -0
  5. data/README.md +128 -0
  6. data/Rakefile +41 -0
  7. data/azuki-api.gemspec +23 -0
  8. data/changelog.txt +204 -0
  9. data/lib/azuki-api.rb +1 -0
  10. data/lib/azuki/api.rb +146 -0
  11. data/lib/azuki/api/addons.rb +48 -0
  12. data/lib/azuki/api/apps.rb +71 -0
  13. data/lib/azuki/api/attachments.rb +14 -0
  14. data/lib/azuki/api/collaborators.rb +33 -0
  15. data/lib/azuki/api/config_vars.rb +33 -0
  16. data/lib/azuki/api/domains.rb +42 -0
  17. data/lib/azuki/api/errors.rb +26 -0
  18. data/lib/azuki/api/features.rb +45 -0
  19. data/lib/azuki/api/keys.rb +42 -0
  20. data/lib/azuki/api/login.rb +14 -0
  21. data/lib/azuki/api/logs.rb +18 -0
  22. data/lib/azuki/api/mock.rb +179 -0
  23. data/lib/azuki/api/mock/addons.rb +153 -0
  24. data/lib/azuki/api/mock/apps.rb +205 -0
  25. data/lib/azuki/api/mock/attachments.rb +19 -0
  26. data/lib/azuki/api/mock/cache/get_addons.json +1 -0
  27. data/lib/azuki/api/mock/cache/get_features.json +1 -0
  28. data/lib/azuki/api/mock/cache/get_user.json +1 -0
  29. data/lib/azuki/api/mock/collaborators.rb +55 -0
  30. data/lib/azuki/api/mock/config_vars.rb +49 -0
  31. data/lib/azuki/api/mock/domains.rb +80 -0
  32. data/lib/azuki/api/mock/features.rb +120 -0
  33. data/lib/azuki/api/mock/keys.rb +46 -0
  34. data/lib/azuki/api/mock/login.rb +22 -0
  35. data/lib/azuki/api/mock/logs.rb +20 -0
  36. data/lib/azuki/api/mock/processes.rb +198 -0
  37. data/lib/azuki/api/mock/releases.rb +69 -0
  38. data/lib/azuki/api/mock/stacks.rb +83 -0
  39. data/lib/azuki/api/mock/user.rb +16 -0
  40. data/lib/azuki/api/processes.rb +77 -0
  41. data/lib/azuki/api/releases.rb +33 -0
  42. data/lib/azuki/api/ssl_endpoints.rb +62 -0
  43. data/lib/azuki/api/stacks.rb +22 -0
  44. data/lib/azuki/api/user.rb +14 -0
  45. data/lib/azuki/api/vendor/okjson.rb +600 -0
  46. data/lib/azuki/api/version.rb +5 -0
  47. data/test/data/site.crt +19 -0
  48. data/test/data/site.key +27 -0
  49. data/test/test_addons.rb +193 -0
  50. data/test/test_apps.rb +147 -0
  51. data/test/test_attachments.rb +23 -0
  52. data/test/test_collaborators.rb +73 -0
  53. data/test/test_config_vars.rb +54 -0
  54. data/test/test_domains.rb +65 -0
  55. data/test/test_error_conditions.rb +11 -0
  56. data/test/test_features.rb +87 -0
  57. data/test/test_helper.rb +48 -0
  58. data/test/test_keys.rb +39 -0
  59. data/test/test_login.rb +20 -0
  60. data/test/test_logs.rb +29 -0
  61. data/test/test_processes.rb +245 -0
  62. data/test/test_releases.rb +73 -0
  63. data/test/test_ssl_endpoints.rb +132 -0
  64. data/test/test_stacks.rb +49 -0
  65. data/test/test_user.rb +13 -0
  66. metadata +168 -0
@@ -0,0 +1,69 @@
1
+ module Azuki
2
+ class API
3
+ module Mock
4
+
5
+ # stub GET /apps/:app/releases
6
+ Excon.stub(:expects => 200, :method => :get, :path => %r{^/apps/([^/]+)/releases$} ) do |params|
7
+ request_params, mock_data = parse_stub_params(params)
8
+ app, _ = request_params[:captures][:path]
9
+ with_mock_app(mock_data, app) do |app_data|
10
+ {
11
+ :body => Azuki::API::OkJson.encode(mock_data[:releases][app]),
12
+ :status => 200
13
+ }
14
+ end
15
+ end
16
+
17
+ # stub GET /apps/:app/releases/:release
18
+ Excon.stub(:expects => 200, :method => :get, :path => %r{^/apps/([^/]+)/releases/([^/]+)$} ) do |params|
19
+ request_params, mock_data = parse_stub_params(params)
20
+ app, release_name, _ = request_params[:captures][:path]
21
+ with_mock_app(mock_data, app) do |app_data|
22
+ releases = mock_data[:releases][app]
23
+ if release_data = (release_name == 'current' && releases.last) || releases.detect {|release| release['name'] == release_name}
24
+ {
25
+ :body => Azuki::API::OkJson.encode(release_data),
26
+ :status => 200
27
+ }
28
+ else
29
+ {
30
+ :body => 'Record not found.',
31
+ :status => 404
32
+ }
33
+ end
34
+ end
35
+ end
36
+
37
+ # stub POST /apps/:app/releases/:release
38
+ Excon.stub(:expects => 200, :method => :post, :path => %r{^/apps/([^/]+)/releases} ) do |params|
39
+ request_params, mock_data = parse_stub_params(params)
40
+ app, _ = request_params[:captures][:path]
41
+ release_name = request_params[:query]['rollback'] || mock_data[:releases][app][-2] && mock_data[:releases][app][-2]['name']
42
+ with_mock_app(mock_data, app) do |app_data|
43
+ releases = mock_data[:releases][app]
44
+ if release_data = releases.detect {|release| release['name'] == release_name}
45
+ if release_data['addons'] == mock_data[:releases][app].last['addons']
46
+ add_mock_release(mock_data, app, {'descr' => "Rollback to #{release_name}"})
47
+
48
+ {
49
+ :body => release_data['name'],
50
+ :status => 200
51
+ }
52
+ else
53
+ {
54
+ :body => Azuki::API::OkJson.encode({'error' => 'Cannot rollback to a release that had a different set of addons installed'}),
55
+ :status => 422
56
+ }
57
+ end
58
+ else
59
+ {
60
+ :body => 'Record not found.',
61
+ :status => 404
62
+ }
63
+ end
64
+ end
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,83 @@
1
+ module Azuki
2
+ class API
3
+ module Mock
4
+
5
+ STACKS = [
6
+ {
7
+ "beta" => false,
8
+ "requested" => false,
9
+ "current" => false,
10
+ "name" => "aspen-mri-1.8.6"
11
+ },
12
+ {
13
+ "beta" => false,
14
+ "requested" => false,
15
+ "current" => false,
16
+ "name" => "bamboo-mri-1.9.2"
17
+ },
18
+ {
19
+ "beta" => false,
20
+ "requested" => false,
21
+ "current" => false,
22
+ "name" => "bamboo-ree-1.8.7"
23
+ },
24
+ {
25
+ "beta" => true,
26
+ "requested" => false,
27
+ "current" => false,
28
+ "name" => "cedar"
29
+ }
30
+ ]
31
+
32
+ # stub GET /apps/:app/stack
33
+ Excon.stub(:expects => 200, :method => :get, :path => %r{^/apps/([^/]+)/stack}) do |params|
34
+ request_params, mock_data = parse_stub_params(params)
35
+ app, _ = request_params[:captures][:path]
36
+ with_mock_app(mock_data, app) do |app_data|
37
+ stack_data = Marshal::load(Marshal.dump(STACKS))
38
+ stack_data.detect {|stack| stack['name'] == app_data['stack']}['current'] = true
39
+ {
40
+ :body => Azuki::API::OkJson.encode(stack_data),
41
+ :status => 200
42
+ }
43
+ end
44
+ end
45
+
46
+ # stub PUT /apps/:app/stack
47
+ Excon.stub(:expects => 200, :method => :put, :path => %r{^/apps/([^/]+)/stack}) do |params|
48
+ request_params, mock_data = parse_stub_params(params)
49
+ app, _ = request_params[:captures][:path]
50
+ stack = request_params[:body]
51
+ with_mock_app(mock_data, app) do |app_data|
52
+ if app_data['stack'] != 'cedar' && stack != 'cedar'
53
+ if STACKS.map {|stack_data| stack_data['name']}.include?(stack)
54
+ {
55
+ :body => <<-BODY,
56
+ -----> Preparing to migrate #{app}
57
+ #{app_data['stack']} -> #{stack}
58
+
59
+ NOTE: Additional details here
60
+
61
+ -----> Migration prepared.
62
+ Run 'git push azuki master' to execute migration.
63
+ BODY
64
+ :status => 200
65
+ }
66
+ else
67
+ {
68
+ :body => Azuki::API::OkJson.encode('error' => 'Stack not found'),
69
+ :status => 404
70
+ }
71
+ end
72
+ else
73
+ {
74
+ :body => Azuki::API::OkJson.encode('error' => 'Stack migration to/from Cedar is not available. Create a new app with --stack cedar instead.'),
75
+ :status => 422
76
+ }
77
+ end
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,16 @@
1
+ module Azuki
2
+ class API
3
+ module Mock
4
+
5
+ # stub GET /user
6
+ Excon.stub(:expects => 200, :method => :get, :path => %r{^/user$}) do |params|
7
+ request_params, mock_data = parse_stub_params(params)
8
+ {
9
+ :body => File.read("#{File.dirname(__FILE__)}/cache/get_user.json"),
10
+ :status => 200
11
+ }
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,77 @@
1
+ module Azuki
2
+ class API
3
+
4
+ # GET /apps/:app/ps
5
+ def get_ps(app)
6
+ request(
7
+ :expects => 200,
8
+ :method => :get,
9
+ :path => "/apps/#{app}/ps"
10
+ )
11
+ end
12
+
13
+ # POST /apps/:app/ps
14
+ def post_ps(app, command, options={})
15
+ options = { 'command' => command }.merge(options)
16
+ request(
17
+ :expects => 200,
18
+ :method => :post,
19
+ :path => "/apps/#{app}/ps",
20
+ :query => ps_options(options)
21
+ )
22
+ end
23
+
24
+ # POST /apps/:app/ps/restart
25
+ def post_ps_restart(app, options={})
26
+ request(
27
+ :expects => 200,
28
+ :method => :post,
29
+ :path => "/apps/#{app}/ps/restart",
30
+ :query => options
31
+ )
32
+ end
33
+
34
+ # POST /apps/:app/ps/scale
35
+ def post_ps_scale(app, type, quantity)
36
+ request(
37
+ :expects => 200,
38
+ :method => :post,
39
+ :path => "/apps/#{app}/ps/scale",
40
+ :query => {
41
+ 'type' => type,
42
+ 'qty' => quantity
43
+ }
44
+ )
45
+ end
46
+
47
+ # POST /apps/:app/ps/stop
48
+ def post_ps_stop(app, options)
49
+ request(
50
+ :expects => 200,
51
+ :method => :post,
52
+ :path => "/apps/#{app}/ps/stop",
53
+ :query => options
54
+ )
55
+ end
56
+
57
+ # PUT /apps/:app/dynos
58
+ def put_dynos(app, dynos)
59
+ request(
60
+ :expects => 200,
61
+ :method => :put,
62
+ :path => "/apps/#{app}/dynos",
63
+ :query => {'dynos' => dynos}
64
+ )
65
+ end
66
+
67
+ # PUT /apps/:app/workers
68
+ def put_workers(app, workers)
69
+ request(
70
+ :expects => 200,
71
+ :method => :put,
72
+ :path => "/apps/#{app}/workers",
73
+ :query => {'workers' => workers}
74
+ )
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,33 @@
1
+ module Azuki
2
+ class API
3
+
4
+ # GET /apps/:app/releases
5
+ def get_releases(app)
6
+ request(
7
+ :expects => 200,
8
+ :method => :get,
9
+ :path => "/apps/#{app}/releases"
10
+ )
11
+ end
12
+
13
+ # GET /apps/:app/releases/:release
14
+ def get_release(app, release)
15
+ request(
16
+ :expects => 200,
17
+ :method => :get,
18
+ :path => "/apps/#{app}/releases/#{release}"
19
+ )
20
+ end
21
+
22
+ # POST /apps/:app/releases/:release
23
+ def post_release(app, release=nil)
24
+ request(
25
+ :expects => 200,
26
+ :method => :post,
27
+ :path => "/apps/#{app}/releases",
28
+ :query => {'rollback' => release}
29
+ )
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,62 @@
1
+ module Azuki
2
+ class API
3
+
4
+ # DELETE /apps/:app/ssl-endpoint/:cname
5
+ def delete_ssl_endpoint(app, cname)
6
+ request(
7
+ :expects => 200,
8
+ :method => :delete,
9
+ :path => "/apps/#{app}/ssl-endpoints/#{escape(cname)}"
10
+ )
11
+ end
12
+
13
+ # GET /apps/:app/ssl-endpoint/:cname
14
+ def get_ssl_endpoint(app, cname)
15
+ request(
16
+ :expects => 200,
17
+ :method => :get,
18
+ :path => "/apps/#{app}/ssl-endpoints/#{escape(cname)}"
19
+ )
20
+ end
21
+
22
+ # GET /apps/:app/ssl-endpoints
23
+ def get_ssl_endpoints(app)
24
+ request(
25
+ :expects => 200,
26
+ :method => :get,
27
+ :path => "/apps/#{app}/ssl-endpoints"
28
+ )
29
+ end
30
+
31
+ # POST /apps/:app/ssl-endpoints
32
+ def post_ssl_endpoint(app, pem, key)
33
+ request(
34
+ :expects => 200,
35
+ :method => :post,
36
+ :path => "/apps/#{app}/ssl-endpoints",
37
+ :query => { 'key' => key, 'pem' => pem }
38
+ )
39
+ end
40
+
41
+ # POST /apps/:app/ssl-endpoints/:cname/rollback
42
+ def post_ssl_endpoint_rollback(app, cname)
43
+ request(
44
+ :expects => 200,
45
+ :method => :post,
46
+ :path => "/apps/#{app}/ssl-endpoints/#{escape(cname)}/rollback"
47
+ )
48
+ end
49
+
50
+ # PUT /apps/:app/ssl-endpoints/:cname
51
+ def put_ssl_endpoint(app, cname, pem, key)
52
+ request(
53
+ :expects => 200,
54
+ :method => :put,
55
+ :path => "/apps/#{app}/ssl-endpoints/#{escape(cname)}",
56
+ :query => { 'key' => key, 'pem' => pem }
57
+ )
58
+ end
59
+
60
+ end
61
+ end
62
+
@@ -0,0 +1,22 @@
1
+ module Azuki
2
+ class API
3
+
4
+ def get_stack(app)
5
+ request(
6
+ :expects => 200,
7
+ :method => :get,
8
+ :path => "/apps/#{app}/stack"
9
+ )
10
+ end
11
+
12
+ def put_stack(app, stack)
13
+ request(
14
+ :body => stack,
15
+ :expects => 200,
16
+ :method => :put,
17
+ :path => "/apps/#{app}/stack"
18
+ )
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module Azuki
2
+ class API
3
+
4
+ # GET /user
5
+ def get_user
6
+ request(
7
+ :expects => 200,
8
+ :method => :get,
9
+ :path => "/user"
10
+ )
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,600 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2011, 2012 Keith Rarick
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ # See https://github.com/kr/okjson for updates.
24
+
25
+ require 'stringio'
26
+
27
+ # Some parts adapted from
28
+ # http://golang.org/src/pkg/json/decode.go and
29
+ # http://golang.org/src/pkg/utf8/utf8.go
30
+ module Azuki
31
+ class API
32
+ module OkJson
33
+ extend self
34
+
35
+
36
+ # Decodes a json document in string s and
37
+ # returns the corresponding ruby value.
38
+ # String s must be valid UTF-8. If you have
39
+ # a string in some other encoding, convert
40
+ # it first.
41
+ #
42
+ # String values in the resulting structure
43
+ # will be UTF-8.
44
+ def decode(s)
45
+ ts = lex(s)
46
+ v, ts = textparse(ts)
47
+ if ts.length > 0
48
+ raise Error, 'trailing garbage'
49
+ end
50
+ v
51
+ end
52
+
53
+
54
+ # Parses a "json text" in the sense of RFC 4627.
55
+ # Returns the parsed value and any trailing tokens.
56
+ # Note: this is almost the same as valparse,
57
+ # except that it does not accept atomic values.
58
+ def textparse(ts)
59
+ if ts.length < 0
60
+ raise Error, 'empty'
61
+ end
62
+
63
+ typ, _, val = ts[0]
64
+ case typ
65
+ when '{' then objparse(ts)
66
+ when '[' then arrparse(ts)
67
+ else
68
+ raise Error, "unexpected #{val.inspect}"
69
+ end
70
+ end
71
+
72
+
73
+ # Parses a "value" in the sense of RFC 4627.
74
+ # Returns the parsed value and any trailing tokens.
75
+ def valparse(ts)
76
+ if ts.length < 0
77
+ raise Error, 'empty'
78
+ end
79
+
80
+ typ, _, val = ts[0]
81
+ case typ
82
+ when '{' then objparse(ts)
83
+ when '[' then arrparse(ts)
84
+ when :val,:str then [val, ts[1..-1]]
85
+ else
86
+ raise Error, "unexpected #{val.inspect}"
87
+ end
88
+ end
89
+
90
+
91
+ # Parses an "object" in the sense of RFC 4627.
92
+ # Returns the parsed value and any trailing tokens.
93
+ def objparse(ts)
94
+ ts = eat('{', ts)
95
+ obj = {}
96
+
97
+ if ts[0][0] == '}'
98
+ return obj, ts[1..-1]
99
+ end
100
+
101
+ k, v, ts = pairparse(ts)
102
+ obj[k] = v
103
+
104
+ if ts[0][0] == '}'
105
+ return obj, ts[1..-1]
106
+ end
107
+
108
+ loop do
109
+ ts = eat(',', ts)
110
+
111
+ k, v, ts = pairparse(ts)
112
+ obj[k] = v
113
+
114
+ if ts[0][0] == '}'
115
+ return obj, ts[1..-1]
116
+ end
117
+ end
118
+ end
119
+
120
+
121
+ # Parses a "member" in the sense of RFC 4627.
122
+ # Returns the parsed values and any trailing tokens.
123
+ def pairparse(ts)
124
+ (typ, _, k), ts = ts[0], ts[1..-1]
125
+ if typ != :str
126
+ raise Error, "unexpected #{k.inspect}"
127
+ end
128
+ ts = eat(':', ts)
129
+ v, ts = valparse(ts)
130
+ [k, v, ts]
131
+ end
132
+
133
+
134
+ # Parses an "array" in the sense of RFC 4627.
135
+ # Returns the parsed value and any trailing tokens.
136
+ def arrparse(ts)
137
+ ts = eat('[', ts)
138
+ arr = []
139
+
140
+ if ts[0][0] == ']'
141
+ return arr, ts[1..-1]
142
+ end
143
+
144
+ v, ts = valparse(ts)
145
+ arr << v
146
+
147
+ if ts[0][0] == ']'
148
+ return arr, ts[1..-1]
149
+ end
150
+
151
+ loop do
152
+ ts = eat(',', ts)
153
+
154
+ v, ts = valparse(ts)
155
+ arr << v
156
+
157
+ if ts[0][0] == ']'
158
+ return arr, ts[1..-1]
159
+ end
160
+ end
161
+ end
162
+
163
+
164
+ def eat(typ, ts)
165
+ if ts[0][0] != typ
166
+ raise Error, "expected #{typ} (got #{ts[0].inspect})"
167
+ end
168
+ ts[1..-1]
169
+ end
170
+
171
+
172
+ # Scans s and returns a list of json tokens,
173
+ # excluding white space (as defined in RFC 4627).
174
+ def lex(s)
175
+ ts = []
176
+ while s.length > 0
177
+ typ, lexeme, val = tok(s)
178
+ if typ == nil
179
+ raise Error, "invalid character at #{s[0,10].inspect}"
180
+ end
181
+ if typ != :space
182
+ ts << [typ, lexeme, val]
183
+ end
184
+ s = s[lexeme.length..-1]
185
+ end
186
+ ts
187
+ end
188
+
189
+
190
+ # Scans the first token in s and
191
+ # returns a 3-element list, or nil
192
+ # if s does not begin with a valid token.
193
+ #
194
+ # The first list element is one of
195
+ # '{', '}', ':', ',', '[', ']',
196
+ # :val, :str, and :space.
197
+ #
198
+ # The second element is the lexeme.
199
+ #
200
+ # The third element is the value of the
201
+ # token for :val and :str, otherwise
202
+ # it is the lexeme.
203
+ def tok(s)
204
+ case s[0]
205
+ when ?{ then ['{', s[0,1], s[0,1]]
206
+ when ?} then ['}', s[0,1], s[0,1]]
207
+ when ?: then [':', s[0,1], s[0,1]]
208
+ when ?, then [',', s[0,1], s[0,1]]
209
+ when ?[ then ['[', s[0,1], s[0,1]]
210
+ when ?] then [']', s[0,1], s[0,1]]
211
+ when ?n then nulltok(s)
212
+ when ?t then truetok(s)
213
+ when ?f then falsetok(s)
214
+ when ?" then strtok(s)
215
+ when Spc then [:space, s[0,1], s[0,1]]
216
+ when ?\t then [:space, s[0,1], s[0,1]]
217
+ when ?\n then [:space, s[0,1], s[0,1]]
218
+ when ?\r then [:space, s[0,1], s[0,1]]
219
+ else numtok(s)
220
+ end
221
+ end
222
+
223
+
224
+ def nulltok(s); s[0,4] == 'null' ? [:val, 'null', nil] : [] end
225
+ def truetok(s); s[0,4] == 'true' ? [:val, 'true', true] : [] end
226
+ def falsetok(s); s[0,5] == 'false' ? [:val, 'false', false] : [] end
227
+
228
+
229
+ def numtok(s)
230
+ m = /-?([1-9][0-9]+|[0-9])([.][0-9]+)?([eE][+-]?[0-9]+)?/.match(s)
231
+ if m && m.begin(0) == 0
232
+ if m[3] && !m[2]
233
+ [:val, m[0], Integer(m[1])*(10**Integer(m[3][1..-1]))]
234
+ elsif m[2]
235
+ [:val, m[0], Float(m[0])]
236
+ else
237
+ [:val, m[0], Integer(m[0])]
238
+ end
239
+ else
240
+ []
241
+ end
242
+ end
243
+
244
+
245
+ def strtok(s)
246
+ m = /"([^"\\]|\\["\/\\bfnrt]|\\u[0-9a-fA-F]{4})*"/.match(s)
247
+ if ! m
248
+ raise Error, "invalid string literal at #{abbrev(s)}"
249
+ end
250
+ [:str, m[0], unquote(m[0])]
251
+ end
252
+
253
+
254
+ def abbrev(s)
255
+ t = s[0,10]
256
+ p = t['`']
257
+ t = t[0,p] if p
258
+ t = t + '...' if t.length < s.length
259
+ '`' + t + '`'
260
+ end
261
+
262
+
263
+ # Converts a quoted json string literal q into a UTF-8-encoded string.
264
+ # The rules are different than for Ruby, so we cannot use eval.
265
+ # Unquote will raise an error if q contains control characters.
266
+ def unquote(q)
267
+ q = q[1...-1]
268
+ rubydoesenc = false
269
+ # In ruby >= 1.9, a[w] is a codepoint, not a byte.
270
+ if q.class.method_defined?(:force_encoding)
271
+ q.force_encoding('UTF-8')
272
+ rubydoesenc = true
273
+ end
274
+ a = q.dup # allocate a big enough string
275
+ r, w = 0, 0
276
+ while r < q.length
277
+ c = q[r]
278
+ case true
279
+ when c == ?\\
280
+ r += 1
281
+ if r >= q.length
282
+ raise Error, "string literal ends with a \"\\\": \"#{q}\""
283
+ end
284
+
285
+ case q[r]
286
+ when ?",?\\,?/,?'
287
+ a[w] = q[r]
288
+ r += 1
289
+ w += 1
290
+ when ?b,?f,?n,?r,?t
291
+ a[w] = Unesc[q[r]]
292
+ r += 1
293
+ w += 1
294
+ when ?u
295
+ r += 1
296
+ uchar = begin
297
+ hexdec4(q[r,4])
298
+ rescue RuntimeError => e
299
+ raise Error, "invalid escape sequence \\u#{q[r,4]}: #{e}"
300
+ end
301
+ r += 4
302
+ if surrogate? uchar
303
+ if q.length >= r+6
304
+ uchar1 = hexdec4(q[r+2,4])
305
+ uchar = subst(uchar, uchar1)
306
+ if uchar != Ucharerr
307
+ # A valid pair; consume.
308
+ r += 6
309
+ end
310
+ end
311
+ end
312
+ if rubydoesenc
313
+ a[w] = '' << uchar
314
+ w += 1
315
+ else
316
+ w += ucharenc(a, w, uchar)
317
+ end
318
+ else
319
+ raise Error, "invalid escape char #{q[r]} in \"#{q}\""
320
+ end
321
+ when c == ?", c < Spc
322
+ raise Error, "invalid character in string literal \"#{q}\""
323
+ else
324
+ # Copy anything else byte-for-byte.
325
+ # Valid UTF-8 will remain valid UTF-8.
326
+ # Invalid UTF-8 will remain invalid UTF-8.
327
+ # In ruby >= 1.9, c is a codepoint, not a byte,
328
+ # in which case this is still what we want.
329
+ a[w] = c
330
+ r += 1
331
+ w += 1
332
+ end
333
+ end
334
+ a[0,w]
335
+ end
336
+
337
+
338
+ # Encodes unicode character u as UTF-8
339
+ # bytes in string a at position i.
340
+ # Returns the number of bytes written.
341
+ def ucharenc(a, i, u)
342
+ case true
343
+ when u <= Uchar1max
344
+ a[i] = (u & 0xff).chr
345
+ 1
346
+ when u <= Uchar2max
347
+ a[i+0] = (Utag2 | ((u>>6)&0xff)).chr
348
+ a[i+1] = (Utagx | (u&Umaskx)).chr
349
+ 2
350
+ when u <= Uchar3max
351
+ a[i+0] = (Utag3 | ((u>>12)&0xff)).chr
352
+ a[i+1] = (Utagx | ((u>>6)&Umaskx)).chr
353
+ a[i+2] = (Utagx | (u&Umaskx)).chr
354
+ 3
355
+ else
356
+ a[i+0] = (Utag4 | ((u>>18)&0xff)).chr
357
+ a[i+1] = (Utagx | ((u>>12)&Umaskx)).chr
358
+ a[i+2] = (Utagx | ((u>>6)&Umaskx)).chr
359
+ a[i+3] = (Utagx | (u&Umaskx)).chr
360
+ 4
361
+ end
362
+ end
363
+
364
+
365
+ def hexdec4(s)
366
+ if s.length != 4
367
+ raise Error, 'short'
368
+ end
369
+ (nibble(s[0])<<12) | (nibble(s[1])<<8) | (nibble(s[2])<<4) | nibble(s[3])
370
+ end
371
+
372
+
373
+ def subst(u1, u2)
374
+ if Usurr1 <= u1 && u1 < Usurr2 && Usurr2 <= u2 && u2 < Usurr3
375
+ return ((u1-Usurr1)<<10) | (u2-Usurr2) + Usurrself
376
+ end
377
+ return Ucharerr
378
+ end
379
+
380
+
381
+ def surrogate?(u)
382
+ Usurr1 <= u && u < Usurr3
383
+ end
384
+
385
+
386
+ def nibble(c)
387
+ case true
388
+ when ?0 <= c && c <= ?9 then c.ord - ?0.ord
389
+ when ?a <= c && c <= ?z then c.ord - ?a.ord + 10
390
+ when ?A <= c && c <= ?Z then c.ord - ?A.ord + 10
391
+ else
392
+ raise Error, "invalid hex code #{c}"
393
+ end
394
+ end
395
+
396
+
397
+ # Encodes x into a json text. It may contain only
398
+ # Array, Hash, String, Numeric, true, false, nil.
399
+ # (Note, this list excludes Symbol.)
400
+ # X itself must be an Array or a Hash.
401
+ # No other value can be encoded, and an error will
402
+ # be raised if x contains any other value, such as
403
+ # Nan, Infinity, Symbol, and Proc, or if a Hash key
404
+ # is not a String.
405
+ # Strings contained in x must be valid UTF-8.
406
+ def encode(x)
407
+ case x
408
+ when Hash then objenc(x)
409
+ when Array then arrenc(x)
410
+ else
411
+ raise Error, 'root value must be an Array or a Hash'
412
+ end
413
+ end
414
+
415
+
416
+ def valenc(x)
417
+ case x
418
+ when Hash then objenc(x)
419
+ when Array then arrenc(x)
420
+ when String then strenc(x)
421
+ when Numeric then numenc(x)
422
+ when true then "true"
423
+ when false then "false"
424
+ when nil then "null"
425
+ else
426
+ raise Error, "cannot encode #{x.class}: #{x.inspect}"
427
+ end
428
+ end
429
+
430
+
431
+ def objenc(x)
432
+ '{' + x.map{|k,v| keyenc(k) + ':' + valenc(v)}.join(',') + '}'
433
+ end
434
+
435
+
436
+ def arrenc(a)
437
+ '[' + a.map{|x| valenc(x)}.join(',') + ']'
438
+ end
439
+
440
+
441
+ def keyenc(k)
442
+ case k
443
+ when String then strenc(k)
444
+ else
445
+ raise Error, "Hash key is not a string: #{k.inspect}"
446
+ end
447
+ end
448
+
449
+
450
+ def strenc(s)
451
+ t = StringIO.new
452
+ t.putc(?")
453
+ r = 0
454
+
455
+ # In ruby >= 1.9, s[r] is a codepoint, not a byte.
456
+ rubydoesenc = s.class.method_defined?(:encoding)
457
+
458
+ while r < s.length
459
+ case s[r]
460
+ when ?" then t.print('\\"')
461
+ when ?\\ then t.print('\\\\')
462
+ when ?\b then t.print('\\b')
463
+ when ?\f then t.print('\\f')
464
+ when ?\n then t.print('\\n')
465
+ when ?\r then t.print('\\r')
466
+ when ?\t then t.print('\\t')
467
+ else
468
+ c = s[r]
469
+ case true
470
+ when rubydoesenc
471
+ begin
472
+ c.ord # will raise an error if c is invalid UTF-8
473
+ t.write(c)
474
+ rescue
475
+ t.write(Ustrerr)
476
+ end
477
+ when Spc <= c && c <= ?~
478
+ t.putc(c)
479
+ else
480
+ n = ucharcopy(t, s, r) # ensure valid UTF-8 output
481
+ r += n - 1 # r is incremented below
482
+ end
483
+ end
484
+ r += 1
485
+ end
486
+ t.putc(?")
487
+ t.string
488
+ end
489
+
490
+
491
+ def numenc(x)
492
+ if ((x.nan? || x.infinite?) rescue false)
493
+ raise Error, "Numeric cannot be represented: #{x}"
494
+ end
495
+ "#{x}"
496
+ end
497
+
498
+
499
+ # Copies the valid UTF-8 bytes of a single character
500
+ # from string s at position i to I/O object t, and
501
+ # returns the number of bytes copied.
502
+ # If no valid UTF-8 char exists at position i,
503
+ # ucharcopy writes Ustrerr and returns 1.
504
+ def ucharcopy(t, s, i)
505
+ n = s.length - i
506
+ raise Utf8Error if n < 1
507
+
508
+ c0 = s[i].ord
509
+
510
+ # 1-byte, 7-bit sequence?
511
+ if c0 < Utagx
512
+ t.putc(c0)
513
+ return 1
514
+ end
515
+
516
+ raise Utf8Error if c0 < Utag2 # unexpected continuation byte?
517
+
518
+ raise Utf8Error if n < 2 # need continuation byte
519
+ c1 = s[i+1].ord
520
+ raise Utf8Error if c1 < Utagx || Utag2 <= c1
521
+
522
+ # 2-byte, 11-bit sequence?
523
+ if c0 < Utag3
524
+ raise Utf8Error if ((c0&Umask2)<<6 | (c1&Umaskx)) <= Uchar1max
525
+ t.putc(c0)
526
+ t.putc(c1)
527
+ return 2
528
+ end
529
+
530
+ # need second continuation byte
531
+ raise Utf8Error if n < 3
532
+
533
+ c2 = s[i+2].ord
534
+ raise Utf8Error if c2 < Utagx || Utag2 <= c2
535
+
536
+ # 3-byte, 16-bit sequence?
537
+ if c0 < Utag4
538
+ u = (c0&Umask3)<<12 | (c1&Umaskx)<<6 | (c2&Umaskx)
539
+ raise Utf8Error if u <= Uchar2max
540
+ t.putc(c0)
541
+ t.putc(c1)
542
+ t.putc(c2)
543
+ return 3
544
+ end
545
+
546
+ # need third continuation byte
547
+ raise Utf8Error if n < 4
548
+ c3 = s[i+3].ord
549
+ raise Utf8Error if c3 < Utagx || Utag2 <= c3
550
+
551
+ # 4-byte, 21-bit sequence?
552
+ if c0 < Utag5
553
+ u = (c0&Umask4)<<18 | (c1&Umaskx)<<12 | (c2&Umaskx)<<6 | (c3&Umaskx)
554
+ raise Utf8Error if u <= Uchar3max
555
+ t.putc(c0)
556
+ t.putc(c1)
557
+ t.putc(c2)
558
+ t.putc(c3)
559
+ return 4
560
+ end
561
+
562
+ raise Utf8Error
563
+ rescue Utf8Error
564
+ t.write(Ustrerr)
565
+ return 1
566
+ end
567
+
568
+
569
+ class Utf8Error < ::StandardError
570
+ end
571
+
572
+
573
+ class Error < ::StandardError
574
+ end
575
+
576
+
577
+ Utagx = 0x80 # 1000 0000
578
+ Utag2 = 0xc0 # 1100 0000
579
+ Utag3 = 0xe0 # 1110 0000
580
+ Utag4 = 0xf0 # 1111 0000
581
+ Utag5 = 0xF8 # 1111 1000
582
+ Umaskx = 0x3f # 0011 1111
583
+ Umask2 = 0x1f # 0001 1111
584
+ Umask3 = 0x0f # 0000 1111
585
+ Umask4 = 0x07 # 0000 0111
586
+ Uchar1max = (1<<7) - 1
587
+ Uchar2max = (1<<11) - 1
588
+ Uchar3max = (1<<16) - 1
589
+ Ucharerr = 0xFFFD # unicode "replacement char"
590
+ Ustrerr = "\xef\xbf\xbd" # unicode "replacement char"
591
+ Usurrself = 0x10000
592
+ Usurr1 = 0xd800
593
+ Usurr2 = 0xdc00
594
+ Usurr3 = 0xe000
595
+
596
+ Spc = ' '[0]
597
+ Unesc = {?b=>?\b, ?f=>?\f, ?n=>?\n, ?r=>?\r, ?t=>?\t}
598
+ end
599
+ end
600
+ end