kensa 1.2.0 → 1.2.1

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.
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