heroku-api 0.1.0

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