kensa 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ notifications:
5
+ email:
6
+ - csquared@heroku.com
7
+ - glenn@heroku.com
data/Gemfile.lock CHANGED
@@ -1,23 +1,23 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- kensa (1.2.0)
4
+ kensa (1.2.1)
5
5
  launchy (>= 0.3.2)
6
6
  mechanize (~> 1.0.0)
7
- rest-client (>= 1.4.0, < 1.7.0)
7
+ rest-client (< 1.7.0, >= 1.4.0)
8
8
  term-ansicolor (~> 1.0)
9
- yajl-ruby (~> 0.6)
10
9
 
11
10
  GEM
12
11
  remote: http://rubygems.org/
13
12
  specs:
14
13
  addressable (2.2.6)
15
14
  archive-tar-minitar (0.5.2)
15
+ artifice (0.6)
16
+ rack-test
16
17
  columnize (0.3.4)
17
18
  contest (0.1.3)
18
19
  fakefs (0.4.0)
19
20
  haml (3.1.2)
20
- json (1.5.3)
21
21
  launchy (2.0.5)
22
22
  addressable (~> 2.2.6)
23
23
  linecache (0.43)
@@ -28,6 +28,8 @@ GEM
28
28
  mime-types (1.17.2)
29
29
  nokogiri (1.5.0)
30
30
  rack (1.3.2)
31
+ rack-test (0.6.1)
32
+ rack (>= 1.0)
31
33
  rake (0.9.2.2)
32
34
  rest-client (1.6.7)
33
35
  mime-types (>= 1.16)
@@ -53,16 +55,15 @@ GEM
53
55
  term-ansicolor (1.0.7)
54
56
  tilt (1.3.2)
55
57
  timecop (0.3.5)
56
- yajl-ruby (0.8.3)
57
58
 
58
59
  PLATFORMS
59
60
  ruby
60
61
 
61
62
  DEPENDENCIES
63
+ artifice
62
64
  contest
63
65
  fakefs
64
66
  haml
65
- json
66
67
  kensa!
67
68
  rake
68
69
  rr
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  Kensa
2
2
  =====
3
3
 
4
+ <img src="https://secure.travis-ci.org/heroku/kensa.png" />
5
+
4
6
  Kensa is a command-line utility to help Heroku add-on providers integrating
5
7
  their services to Heroku. It offers commands to create and validate manifests,
6
8
  and to run the same API calls Heroku runs on your service to provision and
@@ -24,6 +26,6 @@ http://provider.heroku.com/resources/technical/build/provisioning
24
26
 
25
27
  ## Meta #######################################################################
26
28
 
27
- Maintained by Pedro Belo.
29
+ Maintained by Glenn Gillen and csquared.
28
30
 
29
31
  Released under the MIT license. http://github.com/heroku/kensa
data/Rakefile CHANGED
@@ -7,14 +7,4 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList["test/*_test.rb"]
8
8
  end
9
9
 
10
- desc 'Start the server'
11
- task :start do
12
- fork { exec "ruby test/resources/server.rb > test_log.txt 2>&1" }
13
- end
14
-
15
- desc 'Stop the server'
16
- task :stop do
17
- system "ps -ax | grep test/resources/server.rb | grep -v grep | awk '{print $1}' | xargs kill"
18
- end
19
-
20
- task :default => [:start, :test, :stop]
10
+ task :default => :test
data/bin/kensa CHANGED
@@ -68,8 +68,11 @@ COMMANDS
68
68
 
69
69
  TEST TYPES
70
70
 
71
- provision
71
+ provision [optional params]
72
72
  Simulate a provision call from Heroku.
73
+ [optional params]
74
+ accepts extra command options and passes them on to the provision request
75
+ ie: kensa test provision --foo bar
73
76
 
74
77
  deprovision <id>
75
78
  Simulate a deprovision call from Heroku.
data/kensa.gemspec CHANGED
@@ -26,13 +26,12 @@ Gem::Specification.new do |s|
26
26
  s.add_development_dependency(%q<contest>, [">= 0"])
27
27
  s.add_development_dependency(%q<timecop>, [">= 0.3.5"])
28
28
  s.add_development_dependency(%q<sinatra>, [">= 0.9"])
29
- s.add_development_dependency(%q<json>, [">= 0"])
30
29
  s.add_development_dependency(%q<contest>, [">= 0"])
31
30
  s.add_development_dependency(%q<haml>, [">= 0"])
32
31
  s.add_development_dependency(%q<rr>, [">= 0"])
33
32
  s.add_development_dependency(%q<fakefs>, [">= 0"])
33
+ s.add_development_dependency(%q<artifice>, [">= 0"])
34
34
  s.add_runtime_dependency(%q<rest-client>, ["< 1.7.0", ">= 1.4.0"])
35
- s.add_runtime_dependency(%q<yajl-ruby>, ["~> 0.6"])
36
35
  s.add_runtime_dependency(%q<term-ansicolor>, ["~> 1.0"])
37
36
  s.add_runtime_dependency(%q<launchy>, [">= 0.3.2"])
38
37
  s.add_runtime_dependency(%q<mechanize>, ["~> 1.0.0"])
data/lib/heroku/kensa.rb CHANGED
@@ -6,3 +6,4 @@ require 'heroku/kensa/sso'
6
6
  require 'heroku/kensa/post_proxy'
7
7
  require 'heroku/kensa/screen'
8
8
  require 'heroku/kensa/git'
9
+ require 'heroku/kensa/okjson'
@@ -1,4 +1,3 @@
1
- require 'yajl'
2
1
  require 'mechanize'
3
2
  require 'socket'
4
3
  require 'timeout'
@@ -102,6 +101,7 @@ module Heroku
102
101
  check "contains production url" do
103
102
  data["api"].has_key?("production")
104
103
  end
104
+
105
105
  if data['api']['production'].is_a? Hash
106
106
  check "production url uses SSL" do
107
107
  data['api']['production']['base_url'] =~ /^https:/
@@ -114,31 +114,35 @@ module Heroku
114
114
  data['api']['production'] =~ /^https:/
115
115
  end
116
116
  end
117
- check "contains config_vars array" do
118
- data["api"].has_key?("config_vars") && data["api"]["config_vars"].is_a?(Array)
119
- end
120
- check "containst at least one config var" do
121
- !data["api"]["config_vars"].empty?
122
- end
123
- check "all config vars are uppercase strings" do
124
- data["api"]["config_vars"].each do |k, v|
125
- if k =~ /^[A-Z][0-9A-Z_]+$/
126
- true
127
- else
128
- error "#{k.inspect} is not a valid ENV key"
117
+
118
+ if data["api"].has_key?("config_vars")
119
+ check "contains config_vars array" do
120
+ data["api"]["config_vars"].is_a?(Array)
121
+ end
122
+ check "containst at least one config var" do
123
+ !data["api"]["config_vars"].empty?
124
+ end
125
+ check "all config vars are uppercase strings" do
126
+ data["api"]["config_vars"].each do |k, v|
127
+ if k =~ /^[A-Z][0-9A-Z_]+$/
128
+ true
129
+ else
130
+ error "#{k.inspect} is not a valid ENV key"
131
+ end
129
132
  end
130
133
  end
131
- end
132
- check "all config vars are prefixed with the addon id" do
133
- data["api"]["config_vars"].each do |k|
134
- addon_key = data['id'].upcase.gsub('-', '_')
135
- if k =~ /^#{addon_key}_/
136
- true
137
- else
138
- error "#{k} is not a valid ENV key - must be prefixed with #{addon_key}_"
134
+ check "all config vars are prefixed with the addon id" do
135
+ data["api"]["config_vars"].each do |k|
136
+ addon_key = data['id'].upcase.gsub('-', '_')
137
+ if k =~ /^#{addon_key}_/
138
+ true
139
+ else
140
+ error "#{k} is not a valid ENV key - must be prefixed with #{addon_key}_"
141
+ end
139
142
  end
140
143
  end
141
144
  end
145
+
142
146
  check "deprecated fields" do
143
147
  if data["api"].has_key?("username")
144
148
  error "username is deprecated: Please authenticate using the add-on id."
@@ -250,7 +254,7 @@ module Heroku
250
254
  :plan => data[:plan] || 'test',
251
255
  :callback_url => callback,
252
256
  :logplex_token => nil,
253
- :options => {}
257
+ :options => data[:options] || {}
254
258
  }
255
259
 
256
260
  if data[:async]
@@ -294,8 +298,8 @@ module Heroku
294
298
 
295
299
  check "valid JSON" do
296
300
  begin
297
- response = Yajl::Parser.parse(json)
298
- rescue Yajl::ParseError => boom
301
+ response = OkJson.decode(json)
302
+ rescue OkJson::Error => boom
299
303
  error boom.message
300
304
  end
301
305
  true
@@ -6,6 +6,8 @@ require 'optparse'
6
6
  module Heroku
7
7
  module Kensa
8
8
  class Client
9
+ attr_accessor :options
10
+
9
11
  def initialize(args, options = {})
10
12
  @args = args
11
13
  @options = OptParser.parse(args).merge(options)
@@ -71,7 +73,7 @@ module Heroku
71
73
 
72
74
  def sso
73
75
  id = @args.shift || abort("! no id specified; see usage")
74
- data = Yajl::Parser.parse(resolve_manifest).merge(:id => id)
76
+ data = OkJson.decode(resolve_manifest).merge(:id => id)
75
77
  sso = Sso.new(data.merge(@options)).start
76
78
  puts sso.message
77
79
  Launchy.open sso.sso_url
@@ -80,7 +82,7 @@ module Heroku
80
82
  def push
81
83
  user, password = ask_for_credentials
82
84
  host = heroku_host
83
- data = Yajl::Parser.parse(resolve_manifest)
85
+ data = OkJson.decode(resolve_manifest)
84
86
  resource = RestClient::Resource.new(host, user, password)
85
87
  resource['provider/addons'].post(resolve_manifest, headers)
86
88
  puts "-----> Manifest for \"#{data['id']}\" was pushed successfully"
@@ -148,11 +150,11 @@ module Heroku
148
150
  options = args.pop if args.last.is_a?(Hash)
149
151
 
150
152
  args.each do |klass|
151
- data = Yajl::Parser.parse(resolve_manifest)
153
+ data = OkJson.decode(resolve_manifest)
152
154
  check = klass.new(data.merge(@options.merge(options)), screen)
153
155
  result = check.call
154
156
  screen.finish
155
- exit 1 if !result
157
+ exit 1 if !result && !(@options[:test])
156
158
  end
157
159
  end
158
160
 
@@ -242,6 +244,10 @@ module Heroku
242
244
 
243
245
 
244
246
  class OptParser
247
+ def self.parse(args)
248
+ defaults.merge(self.parse_options(args))
249
+ end
250
+
245
251
  def self.defaults
246
252
  {
247
253
  :filename => 'addon-manifest.json',
@@ -249,9 +255,31 @@ module Heroku
249
255
  :async => false,
250
256
  }
251
257
  end
252
-
253
- def self.parse(args)
254
- self.defaults.tap do |options|
258
+
259
+ # OptionParser errors out on unnamed options so we have to pull out all the --flags and --flag=somethings
260
+ KNOWN_ARGS = %w{file async production without-sso help plan version sso foreman template}
261
+ def self.pre_parse(args)
262
+ args.partition do |token|
263
+ token.match(/^--/) && !token.match(/^--(#{KNOWN_ARGS.join('|')})/)
264
+ end.reverse
265
+ end
266
+
267
+ def self.parse_provision(flags, args)
268
+ {}.tap do |options|
269
+ flags.each do |arg|
270
+ key, value = arg.split('=')
271
+ unless value
272
+ peek = args[args.index(key) + 1]
273
+ value = peek && !peek.match(/^--/) ? peek : 'true'
274
+ end
275
+ key = key.sub(/^--/,'')
276
+ options[key] = value
277
+ end
278
+ end
279
+ end
280
+
281
+ def self.parse_command_line(args)
282
+ {}.tap do |options|
255
283
  OptionParser.new do |o|
256
284
  o.on("-f file", "--file") { |filename| options[:filename] = filename }
257
285
  o.on("--async") { options[:async] = true }
@@ -265,16 +293,28 @@ module Heroku
265
293
  o.on("-t name", "--template") do |template|
266
294
  options[:template] = template
267
295
  end
296
+ #note: have to add these to KNOWN_ARGS
268
297
 
269
298
  begin
270
299
  o.parse!(args)
271
- rescue OptionParser::InvalidOption
272
- #skip over invalid options
273
- retry
300
+ rescue OptionParser::InvalidOption => e
301
+ raise CommandInvalid, e.message
274
302
  end
275
303
  end
276
304
  end
277
305
  end
306
+
307
+ def self.parse(args)
308
+ if args[0] == 'test' && args[1] == 'provision'
309
+ safe_args, extra_params = self.pre_parse(args)
310
+ self.defaults.tap do |options|
311
+ options.merge! self.parse_command_line(safe_args)
312
+ options.merge! :options => self.parse_provision(extra_params, args)
313
+ end
314
+ else
315
+ self.defaults.merge(self.parse_command_line(args))
316
+ end
317
+ end
278
318
  end
279
319
  end
280
320
  end
@@ -27,7 +27,7 @@ module Heroku
27
27
 
28
28
  begin
29
29
  args = [
30
- (Yajl::Encoder.encode(payload) if payload),
30
+ (OkJson.encode(payload) if payload),
31
31
  {
32
32
  :accept => "application/json",
33
33
  :content_type => "application/json"
@@ -3,7 +3,7 @@ module Heroku
3
3
  class Manifest
4
4
 
5
5
  def initialize(options = {})
6
- @method = options.fetch(:method, 'post').to_sym
6
+ @method = options.fetch(:method, 'get').to_sym
7
7
  @filename = options.fetch(:filename, 'addon-manifest.json')
8
8
  @options = options
9
9
  end
@@ -58,7 +58,7 @@ ENV
58
58
  end
59
59
 
60
60
  def skeleton
61
- Yajl::Parser.parse skeleton_json
61
+ OkJson.decode skeleton_json
62
62
  end
63
63
 
64
64
  def write
@@ -0,0 +1,606 @@
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 OkJson
29
+ extend self
30
+
31
+
32
+ # Decodes a json document in string s and
33
+ # returns the corresponding ruby value.
34
+ # String s must be valid UTF-8. If you have
35
+ # a string in some other encoding, convert
36
+ # it first.
37
+ #
38
+ # String values in the resulting structure
39
+ # will be UTF-8.
40
+ def decode(s)
41
+ ts = lex(s)
42
+ v, ts = textparse(ts)
43
+ if ts.length > 0
44
+ raise Error, 'trailing garbage'
45
+ end
46
+ v
47
+ end
48
+
49
+
50
+ # Parses a "json text" in the sense of RFC 4627.
51
+ # Returns the parsed value and any trailing tokens.
52
+ # Note: this is almost the same as valparse,
53
+ # except that it does not accept atomic values.
54
+ def textparse(ts)
55
+ if ts.length < 0
56
+ raise Error, 'empty'
57
+ end
58
+
59
+ typ, _, val = ts[0]
60
+ case typ
61
+ when '{' then objparse(ts)
62
+ when '[' then arrparse(ts)
63
+ else
64
+ raise Error, "unexpected #{val.inspect}"
65
+ end
66
+ end
67
+
68
+
69
+ # Parses a "value" in the sense of RFC 4627.
70
+ # Returns the parsed value and any trailing tokens.
71
+ def valparse(ts)
72
+ if ts.length < 0
73
+ raise Error, 'empty'
74
+ end
75
+
76
+ typ, _, val = ts[0]
77
+ case typ
78
+ when '{' then objparse(ts)
79
+ when '[' then arrparse(ts)
80
+ when :val,:str then [val, ts[1..-1]]
81
+ else
82
+ raise Error, "unexpected #{val.inspect}"
83
+ end
84
+ end
85
+
86
+
87
+ # Parses an "object" in the sense of RFC 4627.
88
+ # Returns the parsed value and any trailing tokens.
89
+ def objparse(ts)
90
+ ts = eat('{', ts)
91
+ obj = {}
92
+
93
+ if ts[0][0] == '}'
94
+ return obj, ts[1..-1]
95
+ end
96
+
97
+ k, v, ts = pairparse(ts)
98
+ obj[k] = v
99
+
100
+ if ts[0][0] == '}'
101
+ return obj, ts[1..-1]
102
+ end
103
+
104
+ loop do
105
+ ts = eat(',', ts)
106
+
107
+ k, v, ts = pairparse(ts)
108
+ obj[k] = v
109
+
110
+ if ts[0][0] == '}'
111
+ return obj, ts[1..-1]
112
+ end
113
+ end
114
+ end
115
+
116
+
117
+ # Parses a "member" in the sense of RFC 4627.
118
+ # Returns the parsed values and any trailing tokens.
119
+ def pairparse(ts)
120
+ (typ, _, k), ts = ts[0], ts[1..-1]
121
+ if typ != :str
122
+ raise Error, "unexpected #{k.inspect}"
123
+ end
124
+ ts = eat(':', ts)
125
+ v, ts = valparse(ts)
126
+ [k, v, ts]
127
+ end
128
+
129
+
130
+ # Parses an "array" in the sense of RFC 4627.
131
+ # Returns the parsed value and any trailing tokens.
132
+ def arrparse(ts)
133
+ ts = eat('[', ts)
134
+ arr = []
135
+
136
+ if ts[0][0] == ']'
137
+ return arr, ts[1..-1]
138
+ end
139
+
140
+ v, ts = valparse(ts)
141
+ arr << v
142
+
143
+ if ts[0][0] == ']'
144
+ return arr, ts[1..-1]
145
+ end
146
+
147
+ loop do
148
+ ts = eat(',', ts)
149
+
150
+ v, ts = valparse(ts)
151
+ arr << v
152
+
153
+ if ts[0][0] == ']'
154
+ return arr, ts[1..-1]
155
+ end
156
+ end
157
+ end
158
+
159
+
160
+ def eat(typ, ts)
161
+ if ts[0][0] != typ
162
+ raise Error, "expected #{typ} (got #{ts[0].inspect})"
163
+ end
164
+ ts[1..-1]
165
+ end
166
+
167
+
168
+ # Scans s and returns a list of json tokens,
169
+ # excluding white space (as defined in RFC 4627).
170
+ def lex(s)
171
+ ts = []
172
+ while s.length > 0
173
+ typ, lexeme, val = tok(s)
174
+ if typ == nil
175
+ raise Error, "invalid character at #{s[0,10].inspect}"
176
+ end
177
+ if typ != :space
178
+ ts << [typ, lexeme, val]
179
+ end
180
+ s = s[lexeme.length..-1]
181
+ end
182
+ ts
183
+ end
184
+
185
+
186
+ # Scans the first token in s and
187
+ # returns a 3-element list, or nil
188
+ # if s does not begin with a valid token.
189
+ #
190
+ # The first list element is one of
191
+ # '{', '}', ':', ',', '[', ']',
192
+ # :val, :str, and :space.
193
+ #
194
+ # The second element is the lexeme.
195
+ #
196
+ # The third element is the value of the
197
+ # token for :val and :str, otherwise
198
+ # it is the lexeme.
199
+ def tok(s)
200
+ case s[0]
201
+ when ?{ then ['{', s[0,1], s[0,1]]
202
+ when ?} then ['}', s[0,1], s[0,1]]
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 ?n then nulltok(s)
208
+ when ?t then truetok(s)
209
+ when ?f then falsetok(s)
210
+ when ?" then strtok(s)
211
+ when Spc then [:space, s[0,1], s[0,1]]
212
+ when ?\t then [:space, s[0,1], s[0,1]]
213
+ when ?\n then [:space, s[0,1], s[0,1]]
214
+ when ?\r then [:space, s[0,1], s[0,1]]
215
+ else numtok(s)
216
+ end
217
+ end
218
+
219
+
220
+ def nulltok(s); s[0,4] == 'null' && [:val, 'null', nil] end
221
+ def truetok(s); s[0,4] == 'true' && [:val, 'true', true] end
222
+ def falsetok(s); s[0,5] == 'false' && [:val, 'false', false] end
223
+
224
+
225
+ def numtok(s)
226
+ m = /-?([1-9][0-9]+|[0-9])([.][0-9]+)?([eE][+-]?[0-9]+)?/.match(s)
227
+ if m && m.begin(0) == 0
228
+ if m[3] && !m[2]
229
+ [:val, m[0], Integer(m[1])*(10**Integer(m[3][1..-1]))]
230
+ elsif m[2]
231
+ [:val, m[0], Float(m[0])]
232
+ else
233
+ [:val, m[0], Integer(m[0])]
234
+ end
235
+ end
236
+ end
237
+
238
+
239
+ def strtok(s)
240
+ m = /"([^"\\]|\\["\/\\bfnrt]|\\u[0-9a-fA-F]{4})*"/.match(s)
241
+ if ! m
242
+ raise Error, "invalid string literal at #{abbrev(s)}"
243
+ end
244
+ [:str, m[0], unquote(m[0])]
245
+ end
246
+
247
+
248
+ def abbrev(s)
249
+ t = s[0,10]
250
+ p = t['`']
251
+ t = t[0,p] if p
252
+ t = t + '...' if t.length < s.length
253
+ '`' + t + '`'
254
+ end
255
+
256
+
257
+ # Converts a quoted json string literal q into a UTF-8-encoded string.
258
+ # The rules are different than for Ruby, so we cannot use eval.
259
+ # Unquote will raise an error if q contains control characters.
260
+ def unquote(q)
261
+ q = q[1...-1]
262
+ a = q.dup # allocate a big enough string
263
+ r, w = 0, 0
264
+ while r < q.length
265
+ c = q[r]
266
+ case true
267
+ when c == ?\\
268
+ r += 1
269
+ if r >= q.length
270
+ raise Error, "string literal ends with a \"\\\": \"#{q}\""
271
+ end
272
+
273
+ case q[r]
274
+ when ?",?\\,?/,?'
275
+ a[w] = q[r]
276
+ r += 1
277
+ w += 1
278
+ when ?b,?f,?n,?r,?t
279
+ a[w] = Unesc[q[r]]
280
+ r += 1
281
+ w += 1
282
+ when ?u
283
+ r += 1
284
+ uchar = begin
285
+ hexdec4(q[r,4])
286
+ rescue RuntimeError => e
287
+ raise Error, "invalid escape sequence \\u#{q[r,4]}: #{e}"
288
+ end
289
+ r += 4
290
+ if surrogate? uchar
291
+ if q.length >= r+6
292
+ uchar1 = hexdec4(q[r+2,4])
293
+ uchar = subst(uchar, uchar1)
294
+ if uchar != Ucharerr
295
+ # A valid pair; consume.
296
+ r += 6
297
+ end
298
+ end
299
+ end
300
+ w += ucharenc(a, w, uchar)
301
+ else
302
+ raise Error, "invalid escape char #{q[r]} in \"#{q}\""
303
+ end
304
+ when c == ?", c < Spc
305
+ raise Error, "invalid character in string literal \"#{q}\""
306
+ else
307
+ # Copy anything else byte-for-byte.
308
+ # Valid UTF-8 will remain valid UTF-8.
309
+ # Invalid UTF-8 will remain invalid UTF-8.
310
+ a[w] = c
311
+ r += 1
312
+ w += 1
313
+ end
314
+ end
315
+ a[0,w]
316
+ end
317
+
318
+
319
+ # Encodes unicode character u as UTF-8
320
+ # bytes in string a at position i.
321
+ # Returns the number of bytes written.
322
+ def ucharenc(a, i, u)
323
+ case true
324
+ when u <= Uchar1max
325
+ a[i] = (u & 0xff).chr
326
+ 1
327
+ when u <= Uchar2max
328
+ a[i+0] = (Utag2 | ((u>>6)&0xff)).chr
329
+ a[i+1] = (Utagx | (u&Umaskx)).chr
330
+ 2
331
+ when u <= Uchar3max
332
+ a[i+0] = (Utag3 | ((u>>12)&0xff)).chr
333
+ a[i+1] = (Utagx | ((u>>6)&Umaskx)).chr
334
+ a[i+2] = (Utagx | (u&Umaskx)).chr
335
+ 3
336
+ else
337
+ a[i+0] = (Utag4 | ((u>>18)&0xff)).chr
338
+ a[i+1] = (Utagx | ((u>>12)&Umaskx)).chr
339
+ a[i+2] = (Utagx | ((u>>6)&Umaskx)).chr
340
+ a[i+3] = (Utagx | (u&Umaskx)).chr
341
+ 4
342
+ end
343
+ end
344
+
345
+
346
+ def hexdec4(s)
347
+ if s.length != 4
348
+ raise Error, 'short'
349
+ end
350
+ (nibble(s[0])<<12) | (nibble(s[1])<<8) | (nibble(s[2])<<4) | nibble(s[3])
351
+ end
352
+
353
+
354
+ def subst(u1, u2)
355
+ if Usurr1 <= u1 && u1 < Usurr2 && Usurr2 <= u2 && u2 < Usurr3
356
+ return ((u1-Usurr1)<<10) | (u2-Usurr2) + Usurrself
357
+ end
358
+ return Ucharerr
359
+ end
360
+
361
+
362
+ def unsubst(u)
363
+ if u < Usurrself || u > Umax || surrogate?(u)
364
+ return Ucharerr, Ucharerr
365
+ end
366
+ u -= Usurrself
367
+ [Usurr1 + ((u>>10)&0x3ff), Usurr2 + (u&0x3ff)]
368
+ end
369
+
370
+
371
+ def surrogate?(u)
372
+ Usurr1 <= u && u < Usurr3
373
+ end
374
+
375
+
376
+ def nibble(c)
377
+ case true
378
+ when ?0 <= c && c <= ?9 then c.ord - ?0.ord
379
+ when ?a <= c && c <= ?z then c.ord - ?a.ord + 10
380
+ when ?A <= c && c <= ?Z then c.ord - ?A.ord + 10
381
+ else
382
+ raise Error, "invalid hex code #{c}"
383
+ end
384
+ end
385
+
386
+
387
+ # Encodes x into a json text. It may contain only
388
+ # Array, Hash, String, Numeric, true, false, nil.
389
+ # (Note, this list excludes Symbol.)
390
+ # X itself must be an Array or a Hash.
391
+ # No other value can be encoded, and an error will
392
+ # be raised if x contains any other value, such as
393
+ # Nan, Infinity, Symbol, and Proc, or if a Hash key
394
+ # is not a String.
395
+ # Strings contained in x must be valid UTF-8.
396
+ def encode(x)
397
+ case x
398
+ when Hash then objenc(x)
399
+ when Array then arrenc(x)
400
+ else
401
+ raise Error, 'root value must be an Array or a Hash'
402
+ end
403
+ end
404
+
405
+
406
+ def valenc(x)
407
+ case x
408
+ when Hash then objenc(x)
409
+ when Array then arrenc(x)
410
+ when String then strenc(x)
411
+ when Numeric then numenc(x)
412
+ when true then "true"
413
+ when false then "false"
414
+ when nil then "null"
415
+ else
416
+ raise Error, "cannot encode #{x.class}: #{x.inspect}"
417
+ end
418
+ end
419
+
420
+
421
+ def objenc(x)
422
+ '{' + x.map{|k,v| keyenc(k) + ':' + valenc(v)}.join(',') + '}'
423
+ end
424
+
425
+
426
+ def arrenc(a)
427
+ '[' + a.map{|x| valenc(x)}.join(',') + ']'
428
+ end
429
+
430
+
431
+ def keyenc(k)
432
+ case k
433
+ when String then strenc(k)
434
+ else
435
+ raise Error, "Hash key is not a string: #{k.inspect}"
436
+ end
437
+ end
438
+
439
+
440
+ def strenc(s)
441
+ t = StringIO.new
442
+ t.putc(?")
443
+ r = 0
444
+ while r < s.length
445
+ case s[r]
446
+ when ?" then t.print('\\"')
447
+ when ?\\ then t.print('\\\\')
448
+ when ?\b then t.print('\\b')
449
+ when ?\f then t.print('\\f')
450
+ when ?\n then t.print('\\n')
451
+ when ?\r then t.print('\\r')
452
+ when ?\t then t.print('\\t')
453
+ else
454
+ c = s[r]
455
+ case true
456
+ when Spc <= c && c <= ?~
457
+ t.putc(c)
458
+ when true
459
+ u, size = uchardec(s, r)
460
+ r += size - 1 # we add one more at the bottom of the loop
461
+ if u < 0x10000
462
+ t.print('\\u')
463
+ hexenc4(t, u)
464
+ else
465
+ u1, u2 = unsubst(u)
466
+ t.print('\\u')
467
+ hexenc4(t, u1)
468
+ t.print('\\u')
469
+ hexenc4(t, u2)
470
+ end
471
+ else
472
+ # invalid byte; skip it
473
+ end
474
+ end
475
+ r += 1
476
+ end
477
+ t.putc(?")
478
+ t.string
479
+ end
480
+
481
+
482
+ def hexenc4(t, u)
483
+ t.putc(Hex[(u>>12)&0xf])
484
+ t.putc(Hex[(u>>8)&0xf])
485
+ t.putc(Hex[(u>>4)&0xf])
486
+ t.putc(Hex[u&0xf])
487
+ end
488
+
489
+
490
+ def numenc(x)
491
+ if ((x.nan? || x.infinite?) rescue false)
492
+ raise Error, "Numeric cannot be represented: #{x}"
493
+ end
494
+ "#{x}"
495
+ end
496
+
497
+
498
+ # Decodes unicode character u from UTF-8
499
+ # bytes in string s at position i.
500
+ # Returns u and the number of bytes read.
501
+ def uchardec(s, i)
502
+ n = s.length - i
503
+ return [Ucharerr, 1] if n < 1
504
+
505
+ c0 = s[i].ord
506
+
507
+ # 1-byte, 7-bit sequence?
508
+ if c0 < Utagx
509
+ return [c0, 1]
510
+ end
511
+
512
+ # unexpected continuation byte?
513
+ return [Ucharerr, 1] if c0 < Utag2
514
+
515
+ # need continuation byte
516
+ return [Ucharerr, 1] if n < 2
517
+ c1 = s[i+1].ord
518
+ return [Ucharerr, 1] if c1 < Utagx || Utag2 <= c1
519
+
520
+ # 2-byte, 11-bit sequence?
521
+ if c0 < Utag3
522
+ u = (c0&Umask2)<<6 | (c1&Umaskx)
523
+ return [Ucharerr, 1] if u <= Uchar1max
524
+ return [u, 2]
525
+ end
526
+
527
+ # need second continuation byte
528
+ return [Ucharerr, 1] if n < 3
529
+ c2 = s[i+2].ord
530
+ return [Ucharerr, 1] if c2 < Utagx || Utag2 <= c2
531
+
532
+ # 3-byte, 16-bit sequence?
533
+ if c0 < Utag4
534
+ u = (c0&Umask3)<<12 | (c1&Umaskx)<<6 | (c2&Umaskx)
535
+ return [Ucharerr, 1] if u <= Uchar2max
536
+ return [u, 3]
537
+ end
538
+
539
+ # need third continuation byte
540
+ return [Ucharerr, 1] if n < 4
541
+ c3 = s[i+3].ord
542
+ return [Ucharerr, 1] if c3 < Utagx || Utag2 <= c3
543
+
544
+ # 4-byte, 21-bit sequence?
545
+ if c0 < Utag5
546
+ u = (c0&Umask4)<<18 | (c1&Umaskx)<<12 | (c2&Umaskx)<<6 | (c3&Umaskx)
547
+ return [Ucharerr, 1] if u <= Uchar3max
548
+ return [u, 4]
549
+ end
550
+
551
+ return [Ucharerr, 1]
552
+ end
553
+
554
+
555
+ class Error < ::StandardError
556
+ end
557
+
558
+
559
+ Utagx = 0x80 # 1000 0000
560
+ Utag2 = 0xc0 # 1100 0000
561
+ Utag3 = 0xe0 # 1110 0000
562
+ Utag4 = 0xf0 # 1111 0000
563
+ Utag5 = 0xF8 # 1111 1000
564
+ Umaskx = 0x3f # 0011 1111
565
+ Umask2 = 0x1f # 0001 1111
566
+ Umask3 = 0x0f # 0000 1111
567
+ Umask4 = 0x07 # 0000 0111
568
+ Uchar1max = (1<<7) - 1
569
+ Uchar2max = (1<<11) - 1
570
+ Uchar3max = (1<<16) - 1
571
+ Ucharerr = 0xFFFD # unicode "replacement char"
572
+ Usurrself = 0x10000
573
+ Usurr1 = 0xd800
574
+ Usurr2 = 0xdc00
575
+ Usurr3 = 0xe000
576
+ Umax = 0x10ffff
577
+
578
+ Spc = ' '[0]
579
+ Unesc = {?b=>?\b, ?f=>?\f, ?n=>?\n, ?r=>?\r, ?t=>?\t}
580
+ Hex = '0123456789abcdef'
581
+ end
582
+
583
+ class Hash
584
+ def stringify_keys
585
+ new_hash = {}
586
+ each do |key, value|
587
+ case value
588
+ when Hash
589
+ value = value.stringify_keys
590
+ when Array
591
+ value = value.map { |v| v.stringify_keys if v.is_a? Hash }
592
+ end
593
+
594
+ new_hash[key.to_s] = value
595
+ end
596
+ new_hash
597
+ end
598
+ end
599
+
600
+ module OkJson
601
+ alias encode_without_stringify encode
602
+
603
+ def encode(x)
604
+ encode_without_stringify(x.stringify_keys)
605
+ end
606
+ end