File: json.rb

package info (click to toggle)
libjson-ruby 0.4.2-1
  • links: PTS
  • area: main
  • in suites: etch, etch-m68k
  • size: 268 kB
  • ctags: 329
  • sloc: ruby: 2,672; makefile: 13
file content (708 lines) | stat: -rw-r--r-- 21,109 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
# = json - JSON library for Ruby
#
# == Description
#
# == Author
#
# Florian Frank <mailto:flori@ping.de>
#
# == License
#
# This is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License Version 2 as published by the Free
# Software Foundation: www.gnu.org/copyleft/gpl.html
#
# == Download
#
# The latest version of this library can be downloaded at
#
# * http://rubyforge.org/frs?group_id=953
#
# Online Documentation should be located at
#
# * http://json.rubyforge.org
#
# == Examples
#
# To create a JSON string from a ruby data structure, you
# can call JSON.unparse like that:
#
#  json = JSON.unparse [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
#  # => "[1,2,{\"a\":3.141},false,true,null,\"4..10\"]"
#
# It's also possible to call the #to_json method directly.
#
#  json = [1, 2, {"a"=>3.141}, false, true, nil, 4..10].to_json
#  # => "[1,2,{\"a\":3.141},false,true,null,\"4..10\"]"
#
# To get back a ruby data structure, you have to call
# JSON.parse on the JSON string:
#
#  JSON.parse json
#  # => [1, 2, {"a"=>3.141}, false, true, nil, "4..10"]
# 
# Note, that the range from the original data structure is a simple
# string now. The reason for this is, that JSON doesn't support ranges
# or arbitrary classes. In this case the json library falls back to call
# Object#to_json, which is the same as #to_s.to_json.
#
# It's possible to extend JSON to support serialization of arbitray classes by
# simply implementing a more specialized version of the #to_json method, that
# should return a JSON object (a hash converted to JSON with #to_json)
# like this (don't forget the *a for all the arguments):
#
#  class Range
#    def to_json(*a)
#      {
#        'json_class'   => self.class.name,
#        'data'         => [ first, last, exclude_end? ]
#      }.to_json(*a)
#    end
#  end
#
# The hash key 'json_class' is the class, that will be asked to deserialize the
# JSON representation later. In this case it's 'Range', but any namespace of
# the form 'A::B' or '::A::B' will do. All other keys are arbitrary and can be
# used to store the necessary data to configure the object to be deserialized.
#
# If a the key 'json_class' is found in a JSON object, the JSON parser checks
# if the given class responds to the json_create class method. If so, it is
# called with the JSON object converted to a Ruby hash. So a range can
# be deserialized by implementing Range.json_create like this:
# 
#  class Range
#    def self.json_create(o)
#      new(*o['data'])
#    end
#  end
#
# Now it possible to serialize/deserialize ranges as well:
#
#  json = JSON.unparse [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
#  # => "[1,2,{\"a\":3.141},false,true,null,{\"json_class\":\"Range\",\"data\":[4,10,false]}]"
#  JSON.parse json
#  # => [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
#
# JSON.unparse always creates the shortes possible string representation of a
# ruby data structure in one line. This good for data storage or network
# protocols, but not so good for humans to read. Fortunately there's
# also JSON.pretty_unparse that creates a more readable output:
#
#  puts JSON.pretty_unparse([1, 2, {"a"=>3.141}, false, true, nil, 4..10])
#  [
#    1,
#    2,
#    {
#      "a": 3.141
#    },
#    false,
#    true,
#    null,
#    {
#      "json_class": "Range",
#      "data": [
#        4,
#        10,
#        false
#      ]
#    }
#  ]
#
# There are also the methods Kernel#j for unparse, and Kernel#jj for
# pretty_unparse output to the console, that work analogous to Kernel#p and
# Kernel#pp.
#

require 'strscan'

# This module is the namespace for all the JSON related classes. It also 
# defines some module functions to expose a nicer API to users, instead
# of using the parser and other classes directly.
module JSON
  # The base exception for JSON errors.
  JSONError             = Class.new StandardError

  # This exception is raise, if a parser error occurs.
  ParserError           = Class.new JSONError

  # This exception is raise, if a unparser error occurs.
  UnparserError         = Class.new JSONError

  # If a circular data structure is encountered while unparsing
  # this exception is raised.
  CircularDatastructure = Class.new UnparserError

  class << self
    # Switches on Unicode support, if _enable_ is _true_. Otherwise switches
    # Unicode support off.
    def support_unicode=(enable)
      @support_unicode = enable
    end

    # Returns _true_ if JSON supports unicode, otherwise _false_ is returned.
    #
    # If loading of the iconv library fails, or it doesn't support utf8/utf16
    # encoding, this will be set to false, as a fallback.
    def support_unicode?
      !!@support_unicode
    end
  end
  JSON.support_unicode = true # default, however it's possible to switch off
                              # full unicode support, if non-ascii bytes should be
                              # just passed through.

  begin
    require 'iconv'
    # An iconv instance to convert from UTF8 to UTF16 Big Endian.
    UTF16toUTF8 = Iconv.new('utf-8', 'utf-16be')
    # An iconv instance to convert from UTF16 Big Endian to UTF8.
    UTF8toUTF16 = Iconv.new('utf-16be', 'utf-8'); UTF8toUTF16.iconv('no bom')
  rescue Errno::EINVAL
    begin
      old_verbose = $VERBOSE
      $VERBOSE = nil
      # An iconv instance to convert from UTF8 to UTF16 Big Endian.
      UTF16toUTF8 = Iconv.new('utf-8', 'utf-16')
      # An iconv instance to convert from UTF16 Big Endian to UTF8.
      UTF8toUTF16 = Iconv.new('utf-16', 'utf-8'); UTF8toUTF16.iconv('no bom')
      if UTF8toUTF16.iconv("\xe2\x82\xac") == "\xac\x20"
        swapper = Class.new do
          def initialize(iconv)
            @iconv = iconv
          end

          def iconv(string)
            result = @iconv.iconv(string)
            JSON.swap!(result)
          end
        end
        UTF8toUTF16 = swapper.new(UTF8toUTF16)
      end
      if UTF16toUTF8.iconv("\xac\x20") == "\xe2\x82\xac"
        swapper = Class.new do
          def initialize(iconv)
            @iconv = iconv
          end

          def iconv(string)
            string = JSON.swap!(string.dup)
            @iconv.iconv(string)
          end
        end
        UTF16toUTF8 = swapper.new(UTF16toUTF8)
      end
    rescue Errno::EINVAL
      # Enforce disabling of unicode support, if iconv doesn't support
      # UTF8/UTF16 at all.
      JSON.support_unicode = false
    ensure
      $VERBOSE = old_verbose
    end
  rescue LoadError
    # Enforce disabling of unicode support, if iconv doesn't exist.
    JSON.support_unicode = false
  end

  # Swap consecutive bytes in string in place.
  def self.swap!(string)
    0.upto(string.size / 2) do |i|
      break unless string[2 * i + 1]
      string[2 * i], string[2 * i + 1] = string[2 * i + 1], string[2 * i]
    end
    string
  end

  # This class implements the JSON parser that is used to parse a JSON string
  # into a Ruby data structure.
  class Parser < StringScanner
    STRING                = /"((?:[^"\\]|\\.)*)"/
    INTEGER               = /-?\d+/
    FLOAT                 = /-?\d+\.(\d*)(?i:e[+-]?\d+)?/
    OBJECT_OPEN           = /\{/
    OBJECT_CLOSE          = /\}/
    ARRAY_OPEN            = /\[/
    ARRAY_CLOSE           = /\]/
    PAIR_DELIMITER        = /:/
    COLLECTION_DELIMITER  = /,/
    TRUE                  = /true/
    FALSE                 = /false/
    NULL                  = /null/
    IGNORE                = %r(
      (?:
        //[^\n\r]*[\n\r]| # line comments
        /\*               # c-style comments
          (?:
            [^*/]|        # normal chars
            /[^*]|        # slashes that do not start a nested comment
            \*[^/]|       # asterisks that do not end this comment
            /(?=\*/)      # single slash before this comment's end 
          )*
        \*/               # the end of this comment
        |\s+              # whitespaces
      )+
    )mx

    UNPARSED = Object.new

    # Parses the current JSON string and returns the complete data structure
    # as a result.
    def parse
      reset
      until eos?
        case
        when scan(ARRAY_OPEN)
          return parse_array
        when scan(OBJECT_OPEN)
          return parse_object
        when skip(IGNORE)
          ;
        when !((value = parse_value).equal? UNPARSED)
          return value
        else
          raise ParserError, "source '#{peek(20)}' not in JSON!"
        end
      end
    end

    private

    def parse_string
      if scan(STRING)
        return '' if self[1].empty?
        self[1].gsub(%r(\\(?:[\\bfnrt"/]|u([A-Fa-f\d]{4})))) do
          case $~[0]
          when '\\\\' then '\\'
          when '\\b'  then "\b"
          when '\\f'  then "\f"
          when '\\n'  then "\n"
          when '\\r'  then "\r"
          when '\\t'  then "\t"
          when '\\"'  then '"'
          when '\\/'  then '/'
          else
            if JSON.support_unicode? and $KCODE == 'UTF8'
              JSON.utf16_to_utf8($~[1])
            else
              # if utf8 mode is switched off or unicode not supported, try to
              # transform unicode \u-notation to bytes directly:
              $~[1].to_i(16).chr
            end
          end
        end
      else
        UNPARSED
      end
    end

    def parse_value
      case
      when scan(FLOAT)
        Float(self[0])
      when scan(INTEGER)
        Integer(self[0])
      when scan(TRUE)
        true
      when scan(FALSE)
        false
      when scan(NULL)
        nil
      when (string = parse_string) != UNPARSED
        string
      when scan(ARRAY_OPEN)
        parse_array
      when scan(OBJECT_OPEN)
        parse_object
      else
        UNPARSED
      end
    end

    def parse_array
      result = []
      until eos?
        case
        when (value = parse_value) != UNPARSED
          result << value
          skip(IGNORE)
          unless scan(COLLECTION_DELIMITER) or match?(ARRAY_CLOSE)
            raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!"
          end
        when scan(ARRAY_CLOSE)
          break
        when skip(IGNORE)
          ;
        else
          raise ParserError, "unexpected token in array at '#{peek(20)}'!"
        end
      end
      result
    end

    def parse_object
      result = {}
      until eos?
        case
        when (string = parse_string) != UNPARSED
          skip(IGNORE)
          unless scan(PAIR_DELIMITER)
            raise ParserError, "expected ':' in object at '#{peek(20)}'!"
          end
          skip(IGNORE)
          unless (value = parse_value).equal? UNPARSED
            result[string] = value
            skip(IGNORE)
            unless scan(COLLECTION_DELIMITER) or match?(OBJECT_CLOSE)
              raise ParserError,
                "expected ',' or '}' in object at '#{peek(20)}'!"
            end
          else
            raise ParserError, "expected value in object at '#{peek(20)}'!"
          end
        when scan(OBJECT_CLOSE)
          if klassname = result['json_class']
            klass = klassname.sub(/^:+/, '').split(/::/).inject(Object) do |p,k|
              p.const_get(k) rescue nil
            end
            break unless klass and klass.json_creatable?
            result = klass.json_create(result)
          end
          break
        when skip(IGNORE)
          ;
        else
          raise ParserError, "unexpected token in object at '#{peek(20)}'!"
        end
      end
      result
    end
  end

  # This class is used to create State instances, that are use to hold data
  # while unparsing a Ruby data structure into a JSON string.
  class State
    # Creates a State object from _opts_, which ought to be Hash to create a
    # new State instance configured by opts, something else to create an
    # unconfigured instance. If _opts_ is a State object, it is just returned.
    def self.from_state(opts)
      case opts
      when self
        opts
      when Hash
        new(opts)
      else
        new
      end
    end

    # Instantiates a new State object, configured by _opts_.
    def initialize(opts = {})
      @indent     = opts[:indent]     || ''
      @space      = opts[:space]      || ''
      @object_nl  = opts[:object_nl]  || ''
      @array_nl   = opts[:array_nl]   || ''
      @seen       = {}
    end

    # This string is used to indent levels in the JSON string.
    attr_accessor :indent

    # This string is used to include a space between the tokens in a JSON
    # string.
    attr_accessor :space

    # This string is put at the end of a line that holds a JSON object (or
    # Hash).
    attr_accessor :object_nl

    # This string is put at the end of a line that holds a JSON array.
    attr_accessor :array_nl

    # Returns _true_, if _object_ was already seen during this Unparsing run. 
    def seen?(object)
      @seen.key?(object.__id__)
    end

    # Remember _object_, to find out if it was already encountered (to find out
    # if a cyclic data structure is unparsed). 
    def remember(object)
      @seen[object.__id__] = true
    end

    # Forget _object_ for this Unparsing run.
    def forget(object)
      @seen.delete object.__id__
    end
  end

  module_function

  # Convert _string_ from UTF8 encoding to UTF16 (big endian) encoding and
  # return it.
  def utf8_to_utf16(string)
    JSON::UTF8toUTF16.iconv(string).unpack('H*')[0]
  end

  # Convert _string_ from UTF16 (big endian) encoding to UTF8 encoding and
  # return it.
  def utf16_to_utf8(string)
    bytes = '' << string[0, 2].to_i(16) << string[2, 2].to_i(16)
    JSON::UTF16toUTF8.iconv(bytes)
  end

  # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with
  # UTF16 big endian characters as \u????, and return it.
  def utf8_to_json(string)
    i, n, result = 0, string.size, ''
    while i < n
      char = string[i]
      case
      when char == ?\b then result << '\b'
      when char == ?\t then result << '\t'
      when char == ?\n then result << '\n'
      when char == ?\f then result << '\f'
      when char == ?\r then result << '\r'
      when char == ?"  then result << '\"'
      when char == ?\\ then result << '\\\\'
      when char == ?/ then result << '\/'
      when char.between?(0x0, 0x1f) then result << "\\u%04x" % char
      when char.between?(0x20, 0x7f) then result << char
      when !(JSON.support_unicode? && $KCODE == 'UTF8')
        # if utf8 mode is switched off or unicode not supported, just pass
        # bytes through:
        result << char
      when char & 0xe0 == 0xc0
        result << '\u' << utf8_to_utf16(string[i, 2])
        i += 1
      when char & 0xf0 == 0xe0
        result << '\u' << utf8_to_utf16(string[i, 3])
        i += 2
      when char & 0xf8 == 0xf0
        result << '\u' << utf8_to_utf16(string[i, 4])
        i += 3
      when char & 0xfc == 0xf8
        result << '\u' << utf8_to_utf16(string[i, 5])
        i += 4
      when char & 0xfe == 0xfc
        result << '\u' << utf8_to_utf16(string[i, 6])
        i += 5
      else
        raise JSON::UnparserError, "Encountered unknown UTF-8 byte: %x!" % char
      end
      i += 1
    end
    result
  end

  # Parse the JSON string _source_ into a Ruby data structure and return it.
  def parse(source)
    Parser.new(source).parse
  end

  # Unparse the Ruby data structure _obj_ into a single line JSON string and
  # return it. _state_ is a JSON::State object, that can be used to configure
  # the output further.
  def unparse(obj, state = nil)
    obj.to_json(JSON::State.from_state(state))
  end

  # Unparse the Ruby data structure _obj_ into a JSON string and return it.
  # The returned string is a prettier form of the string returned by #unparse.
  def pretty_unparse(obj)
    state = JSON::State.new(
      :indent     => '  ',
      :space      => ' ',
      :object_nl  => "\n",
      :array_nl   => "\n"
    )
    obj.to_json(state)
  end
end

class Object
  # Converts this object to a string (calling #to_s), converts
  # it to a JSON string, and returns the result. This is a fallback, if no
  # special method #to_json was defined for some object.
  # _state_ is a JSON::State object, that can also be used
  # to configure the produced JSON string output further.

  def to_json(*) to_s.to_json end
end

class Hash
  # Returns a JSON string containing a JSON object, that is unparsed from
  # this Hash instance.
  # _state_ is a JSON::State object, that can also be used to configure the
  # produced JSON string output further.
  # _depth_ is used to find out nesting depth, to indent accordingly.
  def to_json(state = nil, depth = 0)
    state = JSON::State.from_state(state)
    json_check_circular(state) { json_transform(state, depth) }
  end

  private

  def json_check_circular(state)
    if state
      state.seen?(self) and raise JSON::CircularDatastructure,
          "circular data structures not supported!"
      state.remember self
    end
    yield
  ensure
    state and state.forget self
  end

  def json_shift(state, depth)
    state and not state.object_nl.empty? or return ''
    state.indent * depth
  end

  def json_transform(state, depth)
    delim = ','
    delim << state.object_nl if state
    result = '{'
    result << state.object_nl if state
    result << map { |key,value|
      json_shift(state, depth + 1) <<
        key.to_s.to_json(state, depth + 1) <<
        ':' << state.space << value.to_json(state, depth + 1)
    }.join(delim)
    result << state.object_nl if state
    result << json_shift(state, depth)
    result << '}'
    result
  end
end

class Array
  # Returns a JSON string containing a JSON array, that is unparsed from
  # this Array instance.
  # _state_ is a JSON::State object, that can also be used to configure the
  # produced JSON string output further.
  # _depth_ is used to find out nesting depth, to indent accordingly.
  def to_json(state = nil, depth = 0)
    state = JSON::State.from_state(state)
    json_check_circular(state) { json_transform(state, depth) }
  end

  private

  def json_check_circular(state)
    if state
      state.seen?(self) and raise JSON::CircularDatastructure,
        "circular data structures not supported!"
      state.remember self
    end
    yield
  ensure
    state and state.forget self
  end

  def json_shift(state, depth)
    state and not state.array_nl.empty? or return ''
    state.indent * depth
  end

  def json_transform(state, depth)
    delim = ','
    delim << state.array_nl if state
    result = '['
    result << state.array_nl if state
    result << map { |value|
      json_shift(state, depth + 1) << value.to_json(state, depth + 1)
    }.join(delim)
    result << state.array_nl if state
    result << json_shift(state, depth) 
    result << ']'
    result
  end
end

class Integer
  # Returns a JSON string representation for this Integer number.
  def to_json(*) to_s end
end

class Float
  # Returns a JSON string representation for this Float number.
  def to_json(*) to_s end
end

class String
  # This string should be encoded with UTF-8 (if JSON unicode support is
  # enabled). A call to this method returns a JSON string
  # encoded with UTF16 big endian characters as \u????. If
  # JSON.support_unicode? is false only control characters are encoded this
  # way, all 8-bit bytes are just passed through.
  def to_json(*)
    '"' << JSON::utf8_to_json(self) << '"'
  end

  # Raw Strings are JSON Objects (the raw bytes are stored in an array for the
  # key "raw"). The Ruby String can be created by this class method.
  def self.json_create(o)
    o['raw'].pack('C*')
  end

  # This method creates a raw object, that can be nested into other data
  # structures and will be unparsed as a raw string.
  def to_json_raw_object
    {
      'json_class'  => self.class.name,
      'raw'         => self.unpack('C*'),
    }
  end

  # This method should be used, if you want to convert raw strings to JSON
  # instead of UTF-8 strings, e. g. binary data (and JSON Unicode support is
  # enabled).
  def to_json_raw(*args)
    to_json_raw_object.to_json(*args)
  end
end

class TrueClass
  # Returns a JSON string for true: 'true'.
  def to_json(*) to_s end
end

class FalseClass
  # Returns a JSON string for false: 'false'.
  def to_json(*) to_s end
end

class NilClass
  # Returns a JSON string for nil: 'null'.
  def to_json(*) 'null' end
end

module Kernel
  # Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in
  # one line.
  def j(*objs)
    objs.each do |obj|
      puts JSON::unparse(obj)
    end
    nil
  end

  # Ouputs _objs_ to STDOUT as JSON strings in a pretty format, with
  # indentation and over many lines.
  def jj(*objs)
    objs.each do |obj|
      puts JSON::pretty_unparse(obj)
    end
    nil
  end
end

class Class
  # Returns true, if this class can be used to create an instance
  # from a serialised JSON string. The class has to implement a class
  # method _json_create_ that expects a hash as first parameter, which includes
  # the required data.
  def json_creatable?
    respond_to?(:json_create)
  end
end
  # vim: set et sw=2 ts=2: