summaryrefslogtreecommitdiff
path: root/ext/json/lib
diff options
context:
space:
mode:
Diffstat (limited to 'ext/json/lib')
-rw-r--r--[-rwxr-xr-x]ext/json/lib/json.rb677
-rw-r--r--ext/json/lib/json/add/bigdecimal.rb58
-rw-r--r--ext/json/lib/json/add/complex.rb51
-rw-r--r--ext/json/lib/json/add/core.rb144
-rw-r--r--ext/json/lib/json/add/date.rb54
-rw-r--r--ext/json/lib/json/add/date_time.rb67
-rw-r--r--ext/json/lib/json/add/exception.rb49
-rw-r--r--ext/json/lib/json/add/ostruct.rb54
-rw-r--r--ext/json/lib/json/add/rails.rb58
-rw-r--r--ext/json/lib/json/add/range.rb54
-rw-r--r--ext/json/lib/json/add/rational.rb49
-rw-r--r--ext/json/lib/json/add/regexp.rb48
-rw-r--r--ext/json/lib/json/add/set.rb48
-rw-r--r--ext/json/lib/json/add/string.rb35
-rw-r--r--ext/json/lib/json/add/struct.rb52
-rw-r--r--ext/json/lib/json/add/symbol.rb52
-rw-r--r--ext/json/lib/json/add/time.rb52
-rw-r--r--[-rwxr-xr-x]ext/json/lib/json/common.rb1301
-rwxr-xr-xext/json/lib/json/editor.rb1371
-rw-r--r--ext/json/lib/json/ext.rb40
-rw-r--r--ext/json/lib/json/ext/generator/state.rb103
-rw-r--r--ext/json/lib/json/generic_object.rb67
-rw-r--r--ext/json/lib/json/version.rb9
23 files changed, 2673 insertions, 1820 deletions
diff --git a/ext/json/lib/json.rb b/ext/json/lib/json.rb
index 789b0de546..26d601926f 100755..100644
--- a/ext/json/lib/json.rb
+++ b/ext/json/lib/json.rb
@@ -1,10 +1,675 @@
+# frozen_string_literal: true
require 'json/common'
+
+##
+# = JavaScript \Object Notation (\JSON)
+#
+# \JSON is a lightweight data-interchange format.
+#
+# \JSON is easy for us humans to read and write,
+# and equally simple for machines to read (parse) and write (generate).
+#
+# \JSON is language-independent, making it an ideal interchange format
+# for applications in differing programming languages
+# and on differing operating systems.
+#
+# == \JSON Values
+#
+# A \JSON value is one of the following:
+# - Double-quoted text: <tt>"foo"</tt>.
+# - Number: +1+, +1.0+, +2.0e2+.
+# - Boolean: +true+, +false+.
+# - Null: +null+.
+# - \Array: an ordered list of values, enclosed by square brackets:
+# ["foo", 1, 1.0, 2.0e2, true, false, null]
+#
+# - \Object: a collection of name/value pairs, enclosed by curly braces;
+# each name is double-quoted text;
+# the values may be any \JSON values:
+# {"a": "foo", "b": 1, "c": 1.0, "d": 2.0e2, "e": true, "f": false, "g": null}
+#
+# A \JSON array or object may contain nested arrays, objects, and scalars
+# to any depth:
+# {"foo": {"bar": 1, "baz": 2}, "bat": [0, 1, 2]}
+# [{"foo": 0, "bar": 1}, ["baz", 2]]
+#
+# == Using \Module \JSON
+#
+# To make module \JSON available in your code, begin with:
+# require 'json'
+#
+# All examples here assume that this has been done.
+#
+# === Parsing \JSON
+#
+# You can parse a \String containing \JSON data using
+# either of two methods:
+# - <tt>JSON.parse(source, opts)</tt>
+# - <tt>JSON.parse!(source, opts)</tt>
+#
+# where
+# - +source+ is a Ruby object.
+# - +opts+ is a \Hash object containing options
+# that control both input allowed and output formatting.
+#
+# The difference between the two methods
+# is that JSON.parse! omits some checks
+# and may not be safe for some +source+ data;
+# use it only for data from trusted sources.
+# Use the safer method JSON.parse for less trusted sources.
+#
+# ==== Parsing \JSON Arrays
+#
+# When +source+ is a \JSON array, JSON.parse by default returns a Ruby \Array:
+# json = '["foo", 1, 1.0, 2.0e2, true, false, null]'
+# ruby = JSON.parse(json)
+# ruby # => ["foo", 1, 1.0, 200.0, true, false, nil]
+# ruby.class # => Array
+#
+# The \JSON array may contain nested arrays, objects, and scalars
+# to any depth:
+# json = '[{"foo": 0, "bar": 1}, ["baz", 2]]'
+# JSON.parse(json) # => [{"foo"=>0, "bar"=>1}, ["baz", 2]]
+#
+# ==== Parsing \JSON \Objects
+#
+# When the source is a \JSON object, JSON.parse by default returns a Ruby \Hash:
+# json = '{"a": "foo", "b": 1, "c": 1.0, "d": 2.0e2, "e": true, "f": false, "g": null}'
+# ruby = JSON.parse(json)
+# ruby # => {"a"=>"foo", "b"=>1, "c"=>1.0, "d"=>200.0, "e"=>true, "f"=>false, "g"=>nil}
+# ruby.class # => Hash
+#
+# The \JSON object may contain nested arrays, objects, and scalars
+# to any depth:
+# json = '{"foo": {"bar": 1, "baz": 2}, "bat": [0, 1, 2]}'
+# JSON.parse(json) # => {"foo"=>{"bar"=>1, "baz"=>2}, "bat"=>[0, 1, 2]}
+#
+# ==== Parsing \JSON Scalars
+#
+# When the source is a \JSON scalar (not an array or object),
+# JSON.parse returns a Ruby scalar.
+#
+# \String:
+# ruby = JSON.parse('"foo"')
+# ruby # => 'foo'
+# ruby.class # => String
+# \Integer:
+# ruby = JSON.parse('1')
+# ruby # => 1
+# ruby.class # => Integer
+# \Float:
+# ruby = JSON.parse('1.0')
+# ruby # => 1.0
+# ruby.class # => Float
+# ruby = JSON.parse('2.0e2')
+# ruby # => 200
+# ruby.class # => Float
+# Boolean:
+# ruby = JSON.parse('true')
+# ruby # => true
+# ruby.class # => TrueClass
+# ruby = JSON.parse('false')
+# ruby # => false
+# ruby.class # => FalseClass
+# Null:
+# ruby = JSON.parse('null')
+# ruby # => nil
+# ruby.class # => NilClass
+#
+# ==== Parsing Options
+#
+# ====== Input Options
+#
+# Option +max_nesting+ (\Integer) specifies the maximum nesting depth allowed;
+# defaults to +100+; specify +false+ to disable depth checking.
+#
+# With the default, +false+:
+# source = '[0, [1, [2, [3]]]]'
+# ruby = JSON.parse(source)
+# ruby # => [0, [1, [2, [3]]]]
+# Too deep:
+# # Raises JSON::NestingError (nesting of 2 is too deep):
+# JSON.parse(source, {max_nesting: 1})
+# Bad value:
+# # Raises TypeError (wrong argument type Symbol (expected Fixnum)):
+# JSON.parse(source, {max_nesting: :foo})
+#
+# ---
+#
+# Option +allow_duplicate_key+ specifies whether duplicate keys in objects
+# should be ignored or cause an error to be raised:
+#
+# When not specified:
+# # The last value is used and a deprecation warning emitted.
+# JSON.parse('{"a": 1, "a":2}') => {"a" => 2}
+# # warning: detected duplicate keys in JSON object.
+# # This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`
+#
+# When set to `+true+`
+# # The last value is used.
+# JSON.parse('{"a": 1, "a":2}') => {"a" => 2}
+#
+# When set to `+false+`, the future default:
+# JSON.parse('{"a": 1, "a":2}') => duplicate key at line 1 column 1 (JSON::ParserError)
+#
+# ---
+#
+# Option +allow_nan+ (boolean) specifies whether to allow
+# NaN, Infinity, and MinusInfinity in +source+;
+# defaults to +false+.
+#
+# With the default, +false+:
+# # Raises JSON::ParserError (225: unexpected token at '[NaN]'):
+# JSON.parse('[NaN]')
+# # Raises JSON::ParserError (232: unexpected token at '[Infinity]'):
+# JSON.parse('[Infinity]')
+# # Raises JSON::ParserError (248: unexpected token at '[-Infinity]'):
+# JSON.parse('[-Infinity]')
+# Allow:
+# source = '[NaN, Infinity, -Infinity]'
+# ruby = JSON.parse(source, {allow_nan: true})
+# ruby # => [NaN, Infinity, -Infinity]
+#
+# ---
+#
+# Option +allow_trailing_comma+ (boolean) specifies whether to allow
+# trailing commas in objects and arrays;
+# defaults to +false+.
+#
+# With the default, +false+:
+# JSON.parse('[1,]') # unexpected character: ']' at line 1 column 4 (JSON::ParserError)
+#
+# When enabled:
+# JSON.parse('[1,]', allow_trailing_comma: true) # => [1]
+#
+# ---
+#
+# Option +allow_control_characters+ (boolean) specifies whether to allow
+# unescaped ASCII control characters, such as newlines, in strings;
+# defaults to +false+.
+#
+# With the default, +false+:
+# JSON.parse(%{"Hello\nWorld"}) # invalid ASCII control character in string (JSON::ParserError)
+#
+# When enabled:
+# JSON.parse(%{"Hello\nWorld"}, allow_control_characters: true) # => "Hello\nWorld"
+#
+# ---
+#
+# Option +allow_invalid_escape+ (boolean) specifies whether to ignore backslahes that are followed
+# by an invalid escape character in strings;
+# defaults to +false+.
+#
+# With the default, +false+:
+# JSON.parse('"Hell\o"') # invalid escape character in string (JSON::ParserError)
+#
+# When enabled:
+# JSON.parse('"Hell\o"', allow_invalid_escape: true) # => "Hello"
+#
+# ====== Output Options
+#
+# Option +freeze+ (boolean) specifies whether the returned objects will be frozen;
+# defaults to +false+.
+#
+# Option +symbolize_names+ (boolean) specifies whether returned \Hash keys
+# should be Symbols;
+# defaults to +false+ (use Strings).
+#
+# With the default, +false+:
+# source = '{"a": "foo", "b": 1.0, "c": true, "d": false, "e": null}'
+# ruby = JSON.parse(source)
+# ruby # => {"a"=>"foo", "b"=>1.0, "c"=>true, "d"=>false, "e"=>nil}
+# Use Symbols:
+# ruby = JSON.parse(source, {symbolize_names: true})
+# ruby # => {:a=>"foo", :b=>1.0, :c=>true, :d=>false, :e=>nil}
+#
+# ---
+#
+# Option +object_class+ (\Class) specifies the Ruby class to be used
+# for each \JSON object;
+# defaults to \Hash.
+#
+# With the default, \Hash:
+# source = '{"a": "foo", "b": 1.0, "c": true, "d": false, "e": null}'
+# ruby = JSON.parse(source)
+# ruby.class # => Hash
+# Use class \OpenStruct:
+# ruby = JSON.parse(source, {object_class: OpenStruct})
+# ruby # => #<OpenStruct a="foo", b=1.0, c=true, d=false, e=nil>
+#
+# ---
+#
+# Option +array_class+ (\Class) specifies the Ruby class to be used
+# for each \JSON array;
+# defaults to \Array.
+#
+# With the default, \Array:
+# source = '["foo", 1.0, true, false, null]'
+# ruby = JSON.parse(source)
+# ruby.class # => Array
+# Use class \Set:
+# ruby = JSON.parse(source, {array_class: Set})
+# ruby # => #<Set: {"foo", 1.0, true, false, nil}>
+#
+# ---
+#
+# Option +create_additions+ (boolean) specifies whether to use \JSON additions in parsing.
+# See {\JSON Additions}[#module-JSON-label-JSON+Additions].
+#
+# === Generating \JSON
+#
+# To generate a Ruby \String containing \JSON data,
+# use method <tt>JSON.generate(source, opts)</tt>, where
+# - +source+ is a Ruby object.
+# - +opts+ is a \Hash object containing options
+# that control both input allowed and output formatting.
+#
+# ==== Generating \JSON from Arrays
+#
+# When the source is a Ruby \Array, JSON.generate returns
+# a \String containing a \JSON array:
+# ruby = [0, 's', :foo]
+# json = JSON.generate(ruby)
+# json # => '[0,"s","foo"]'
+#
+# The Ruby \Array array may contain nested arrays, hashes, and scalars
+# to any depth:
+# ruby = [0, [1, 2], {foo: 3, bar: 4}]
+# json = JSON.generate(ruby)
+# json # => '[0,[1,2],{"foo":3,"bar":4}]'
+#
+# ==== Generating \JSON from Hashes
+#
+# When the source is a Ruby \Hash, JSON.generate returns
+# a \String containing a \JSON object:
+# ruby = {foo: 0, bar: 's', baz: :bat}
+# json = JSON.generate(ruby)
+# json # => '{"foo":0,"bar":"s","baz":"bat"}'
+#
+# The Ruby \Hash array may contain nested arrays, hashes, and scalars
+# to any depth:
+# ruby = {foo: [0, 1], bar: {baz: 2, bat: 3}, bam: :bad}
+# json = JSON.generate(ruby)
+# json # => '{"foo":[0,1],"bar":{"baz":2,"bat":3},"bam":"bad"}'
+#
+# ==== Generating \JSON from Other Objects
+#
+# When the source is neither an \Array nor a \Hash,
+# the generated \JSON data depends on the class of the source.
+#
+# When the source is a Ruby \Integer or \Float, JSON.generate returns
+# a \String containing a \JSON number:
+# JSON.generate(42) # => '42'
+# JSON.generate(0.42) # => '0.42'
+#
+# When the source is a Ruby \String, JSON.generate returns
+# a \String containing a \JSON string (with double-quotes):
+# JSON.generate('A string') # => '"A string"'
+#
+# When the source is +true+, +false+ or +nil+, JSON.generate returns
+# a \String containing the corresponding \JSON token:
+# JSON.generate(true) # => 'true'
+# JSON.generate(false) # => 'false'
+# JSON.generate(nil) # => 'null'
+#
+# When the source is none of the above, JSON.generate returns
+# a \String containing a \JSON string representation of the source:
+# JSON.generate(:foo) # => '"foo"'
+# JSON.generate(Complex(0, 0)) # => '"0+0i"'
+# JSON.generate(Dir.new('.')) # => '"#<Dir>"'
+#
+# ==== Generating Options
+#
+# ====== Input Options
+#
+# Option +allow_nan+ (boolean) specifies whether
+# +NaN+, +Infinity+, and <tt>-Infinity</tt> may be generated;
+# defaults to +false+.
+#
+# With the default, +false+:
+# # Raises JSON::GeneratorError (920: NaN not allowed in JSON):
+# JSON.generate(JSON::NaN)
+# # Raises JSON::GeneratorError (917: Infinity not allowed in JSON):
+# JSON.generate(JSON::Infinity)
+# # Raises JSON::GeneratorError (917: -Infinity not allowed in JSON):
+# JSON.generate(JSON::MinusInfinity)
+#
+# Allow:
+# ruby = [Float::NAN, Float::INFINITY, JSON::NaN, JSON::Infinity, JSON::MinusInfinity]
+# JSON.generate(ruby, allow_nan: true) # => '[NaN,Infinity,NaN,Infinity,-Infinity]'
+#
+# ---
+#
+# Option +allow_duplicate_key+ (boolean) specifies whether
+# hashes with duplicate keys should be allowed or produce an error.
+# defaults to emit a deprecation warning.
+#
+# With the default, (not set):
+# Warning[:deprecated] = true
+# JSON.generate({ foo: 1, "foo" => 2 })
+# # warning: detected duplicate key "foo" in {foo: 1, "foo" => 2}.
+# # This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`
+# # => '{"foo":1,"foo":2}'
+#
+# With <tt>false</tt>
+# JSON.generate({ foo: 1, "foo" => 2 }, allow_duplicate_key: false)
+# # detected duplicate key "foo" in {foo: 1, "foo" => 2} (JSON::GeneratorError)
+#
+# In version 3.0, <tt>false</tt> will become the default.
+#
+# ---
+#
+# Option +max_nesting+ (\Integer) specifies the maximum nesting depth
+# in +obj+; defaults to +100+.
+#
+# With the default, +100+:
+# obj = [[[[[[0]]]]]]
+# JSON.generate(obj) # => '[[[[[[0]]]]]]'
+#
+# Too deep:
+# # Raises JSON::NestingError (nesting of 2 is too deep):
+# JSON.generate(obj, max_nesting: 2)
+#
+# ====== Escaping Options
+#
+# Options +script_safe+ (boolean) specifies wether <tt>'\u2028'</tt>, <tt>'\u2029'</tt>
+# and <tt>'/'</tt> should be escaped as to make the JSON object safe to interpolate in script
+# tags.
+#
+# Options +ascii_only+ (boolean) specifies wether all characters outside the ASCII range
+# should be escaped.
+#
+# ====== Output Options
+#
+# The default formatting options generate the most compact
+# \JSON data, all on one line and with no whitespace.
+#
+# You can use these formatting options to generate
+# \JSON data in a more open format, using whitespace.
+# See also JSON.pretty_generate.
+#
+# - Option +array_nl+ (\String) specifies a string (usually a newline)
+# to be inserted after each \JSON array; defaults to the empty \String, <tt>''</tt>.
+# - Option +object_nl+ (\String) specifies a string (usually a newline)
+# to be inserted after each \JSON object; defaults to the empty \String, <tt>''</tt>.
+# - Option +indent+ (\String) specifies the string (usually spaces) to be
+# used for indentation; defaults to the empty \String, <tt>''</tt>;
+# defaults to the empty \String, <tt>''</tt>;
+# has no effect unless options +array_nl+ or +object_nl+ specify newlines.
+# - Option +space+ (\String) specifies a string (usually a space) to be
+# inserted after the colon in each \JSON object's pair;
+# defaults to the empty \String, <tt>''</tt>.
+# - Option +space_before+ (\String) specifies a string (usually a space) to be
+# inserted before the colon in each \JSON object's pair;
+# defaults to the empty \String, <tt>''</tt>.
+#
+# In this example, +obj+ is used first to generate the shortest
+# \JSON data (no whitespace), then again with all formatting options
+# specified:
+#
+# obj = {foo: [:bar, :baz], bat: {bam: 0, bad: 1}}
+# json = JSON.generate(obj)
+# puts 'Compact:', json
+# opts = {
+# array_nl: "\n",
+# object_nl: "\n",
+# indent: ' ',
+# space_before: ' ',
+# space: ' '
+# }
+# puts 'Open:', JSON.generate(obj, opts)
+#
+# Output:
+# Compact:
+# {"foo":["bar","baz"],"bat":{"bam":0,"bad":1}}
+# Open:
+# {
+# "foo" : [
+# "bar",
+# "baz"
+# ],
+# "bat" : {
+# "bam" : 0,
+# "bad" : 1
+# }
+# }
+#
+# == \JSON Additions
+#
+# Note that JSON Additions must only be used with trusted data, and is
+# deprecated.
+#
+# When you "round trip" a non-\String object from Ruby to \JSON and back,
+# you have a new \String, instead of the object you began with:
+# ruby0 = Range.new(0, 2)
+# json = JSON.generate(ruby0)
+# json # => '0..2"'
+# ruby1 = JSON.parse(json)
+# ruby1 # => '0..2'
+# ruby1.class # => String
+#
+# You can use \JSON _additions_ to preserve the original object.
+# The addition is an extension of a ruby class, so that:
+# - \JSON.generate stores more information in the \JSON string.
+# - \JSON.parse, called with option +create_additions+,
+# uses that information to create a proper Ruby object.
+#
+# This example shows a \Range being generated into \JSON
+# and parsed back into Ruby, both without and with
+# the addition for \Range:
+# ruby = Range.new(0, 2)
+# # This passage does not use the addition for Range.
+# json0 = JSON.generate(ruby)
+# ruby0 = JSON.parse(json0)
+# # This passage uses the addition for Range.
+# require 'json/add/range'
+# json1 = JSON.generate(ruby)
+# ruby1 = JSON.parse(json1, create_additions: true)
+# # Make a nice display.
+# display = <<~EOT
+# Generated JSON:
+# Without addition: #{json0} (#{json0.class})
+# With addition: #{json1} (#{json1.class})
+# Parsed JSON:
+# Without addition: #{ruby0.inspect} (#{ruby0.class})
+# With addition: #{ruby1.inspect} (#{ruby1.class})
+# EOT
+# puts display
+#
+# This output shows the different results:
+# Generated JSON:
+# Without addition: "0..2" (String)
+# With addition: {"json_class":"Range","a":[0,2,false]} (String)
+# Parsed JSON:
+# Without addition: "0..2" (String)
+# With addition: 0..2 (Range)
+#
+# The \JSON module includes additions for certain classes.
+# You can also craft custom additions.
+# See {Custom \JSON Additions}[#module-JSON-label-Custom+JSON+Additions].
+#
+# === Built-in Additions
+#
+# The \JSON module includes additions for certain classes.
+# To use an addition, +require+ its source:
+# - BigDecimal: <tt>require 'json/add/bigdecimal'</tt>
+# - Complex: <tt>require 'json/add/complex'</tt>
+# - Date: <tt>require 'json/add/date'</tt>
+# - DateTime: <tt>require 'json/add/date_time'</tt>
+# - Exception: <tt>require 'json/add/exception'</tt>
+# - OpenStruct: <tt>require 'json/add/ostruct'</tt>
+# - Range: <tt>require 'json/add/range'</tt>
+# - Rational: <tt>require 'json/add/rational'</tt>
+# - Regexp: <tt>require 'json/add/regexp'</tt>
+# - Set: <tt>require 'json/add/set'</tt>
+# - Struct: <tt>require 'json/add/struct'</tt>
+# - Symbol: <tt>require 'json/add/symbol'</tt>
+# - Time: <tt>require 'json/add/time'</tt>
+#
+# To reduce punctuation clutter, the examples below
+# show the generated \JSON via +puts+, rather than the usual +inspect+,
+#
+# \BigDecimal:
+# require 'json/add/bigdecimal'
+# ruby0 = BigDecimal(0) # 0.0
+# json = JSON.generate(ruby0) # {"json_class":"BigDecimal","b":"27:0.0"}
+# ruby1 = JSON.parse(json, create_additions: true) # 0.0
+# ruby1.class # => BigDecimal
+#
+# \Complex:
+# require 'json/add/complex'
+# ruby0 = Complex(1+0i) # 1+0i
+# json = JSON.generate(ruby0) # {"json_class":"Complex","r":1,"i":0}
+# ruby1 = JSON.parse(json, create_additions: true) # 1+0i
+# ruby1.class # Complex
+#
+# \Date:
+# require 'json/add/date'
+# ruby0 = Date.today # 2020-05-02
+# json = JSON.generate(ruby0) # {"json_class":"Date","y":2020,"m":5,"d":2,"sg":2299161.0}
+# ruby1 = JSON.parse(json, create_additions: true) # 2020-05-02
+# ruby1.class # Date
+#
+# \DateTime:
+# require 'json/add/date_time'
+# ruby0 = DateTime.now # 2020-05-02T10:38:13-05:00
+# json = JSON.generate(ruby0) # {"json_class":"DateTime","y":2020,"m":5,"d":2,"H":10,"M":38,"S":13,"of":"-5/24","sg":2299161.0}
+# ruby1 = JSON.parse(json, create_additions: true) # 2020-05-02T10:38:13-05:00
+# ruby1.class # DateTime
+#
+# \Exception (and its subclasses including \RuntimeError):
+# require 'json/add/exception'
+# ruby0 = Exception.new('A message') # A message
+# json = JSON.generate(ruby0) # {"json_class":"Exception","m":"A message","b":null}
+# ruby1 = JSON.parse(json, create_additions: true) # A message
+# ruby1.class # Exception
+# ruby0 = RuntimeError.new('Another message') # Another message
+# json = JSON.generate(ruby0) # {"json_class":"RuntimeError","m":"Another message","b":null}
+# ruby1 = JSON.parse(json, create_additions: true) # Another message
+# ruby1.class # RuntimeError
+#
+# \OpenStruct:
+# require 'json/add/ostruct'
+# ruby0 = OpenStruct.new(name: 'Matz', language: 'Ruby') # #<OpenStruct name="Matz", language="Ruby">
+# json = JSON.generate(ruby0) # {"json_class":"OpenStruct","t":{"name":"Matz","language":"Ruby"}}
+# ruby1 = JSON.parse(json, create_additions: true) # #<OpenStruct name="Matz", language="Ruby">
+# ruby1.class # OpenStruct
+#
+# \Range:
+# require 'json/add/range'
+# ruby0 = Range.new(0, 2) # 0..2
+# json = JSON.generate(ruby0) # {"json_class":"Range","a":[0,2,false]}
+# ruby1 = JSON.parse(json, create_additions: true) # 0..2
+# ruby1.class # Range
+#
+# \Rational:
+# require 'json/add/rational'
+# ruby0 = Rational(1, 3) # 1/3
+# json = JSON.generate(ruby0) # {"json_class":"Rational","n":1,"d":3}
+# ruby1 = JSON.parse(json, create_additions: true) # 1/3
+# ruby1.class # Rational
+#
+# \Regexp:
+# require 'json/add/regexp'
+# ruby0 = Regexp.new('foo') # (?-mix:foo)
+# json = JSON.generate(ruby0) # {"json_class":"Regexp","o":0,"s":"foo"}
+# ruby1 = JSON.parse(json, create_additions: true) # (?-mix:foo)
+# ruby1.class # Regexp
+#
+# \Set:
+# require 'json/add/set'
+# ruby0 = Set.new([0, 1, 2]) # #<Set: {0, 1, 2}>
+# json = JSON.generate(ruby0) # {"json_class":"Set","a":[0,1,2]}
+# ruby1 = JSON.parse(json, create_additions: true) # #<Set: {0, 1, 2}>
+# ruby1.class # Set
+#
+# \Struct:
+# require 'json/add/struct'
+# Customer = Struct.new(:name, :address) # Customer
+# ruby0 = Customer.new("Dave", "123 Main") # #<struct Customer name="Dave", address="123 Main">
+# json = JSON.generate(ruby0) # {"json_class":"Customer","v":["Dave","123 Main"]}
+# ruby1 = JSON.parse(json, create_additions: true) # #<struct Customer name="Dave", address="123 Main">
+# ruby1.class # Customer
+#
+# \Symbol:
+# require 'json/add/symbol'
+# ruby0 = :foo # foo
+# json = JSON.generate(ruby0) # {"json_class":"Symbol","s":"foo"}
+# ruby1 = JSON.parse(json, create_additions: true) # foo
+# ruby1.class # Symbol
+#
+# \Time:
+# require 'json/add/time'
+# ruby0 = Time.now # 2020-05-02 11:28:26 -0500
+# json = JSON.generate(ruby0) # {"json_class":"Time","s":1588436906,"n":840560000}
+# ruby1 = JSON.parse(json, create_additions: true) # 2020-05-02 11:28:26 -0500
+# ruby1.class # Time
+#
+#
+# === Custom \JSON Additions
+#
+# In addition to the \JSON additions provided,
+# you can craft \JSON additions of your own,
+# either for Ruby built-in classes or for user-defined classes.
+#
+# Here's a user-defined class +Foo+:
+# class Foo
+# attr_accessor :bar, :baz
+# def initialize(bar, baz)
+# self.bar = bar
+# self.baz = baz
+# end
+# end
+#
+# Here's the \JSON addition for it:
+# # Extend class Foo with JSON addition.
+# class Foo
+# # Serialize Foo object with its class name and arguments
+# def to_json(*args)
+# {
+# JSON.create_id => self.class.name,
+# 'a' => [ bar, baz ]
+# }.to_json(*args)
+# end
+# # Deserialize JSON string by constructing new Foo object with arguments.
+# def self.json_create(object)
+# new(*object['a'])
+# end
+# end
+#
+# Demonstration:
+# require 'json'
+# # This Foo object has no custom addition.
+# foo0 = Foo.new(0, 1)
+# json0 = JSON.generate(foo0)
+# obj0 = JSON.parse(json0)
+# # Lood the custom addition.
+# require_relative 'foo_addition'
+# # This foo has the custom addition.
+# foo1 = Foo.new(0, 1)
+# json1 = JSON.generate(foo1)
+# obj1 = JSON.parse(json1, create_additions: true)
+# # Make a nice display.
+# display = <<~EOT
+# Generated JSON:
+# Without custom addition: #{json0} (#{json0.class})
+# With custom addition: #{json1} (#{json1.class})
+# Parsed JSON:
+# Without custom addition: #{obj0.inspect} (#{obj0.class})
+# With custom addition: #{obj1.inspect} (#{obj1.class})
+# EOT
+# puts display
+#
+# Output:
+#
+# Generated JSON:
+# Without custom addition: "#<Foo:0x0000000006534e80>" (String)
+# With custom addition: {"json_class":"Foo","a":[0,1]} (String)
+# Parsed JSON:
+# Without custom addition: "#<Foo:0x0000000006534e80>" (String)
+# With custom addition: #<Foo:0x0000000006473bb8 @bar=0, @baz=1> (Foo)
+#
module JSON
require 'json/version'
-
- begin
- require 'json/ext'
- rescue LoadError
- require 'json/pure'
- end
+ require 'json/ext'
end
diff --git a/ext/json/lib/json/add/bigdecimal.rb b/ext/json/lib/json/add/bigdecimal.rb
new file mode 100644
index 0000000000..dc84572f31
--- /dev/null
+++ b/ext/json/lib/json/add/bigdecimal.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+begin
+ require 'bigdecimal'
+rescue LoadError
+end
+
+class BigDecimal
+
+ # See #as_json.
+ def self.json_create(object)
+ BigDecimal._load object['b']
+ end
+
+ # Methods <tt>BigDecimal#as_json</tt> and +BigDecimal.json_create+ may be used
+ # to serialize and deserialize a \BigDecimal object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>BigDecimal#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/bigdecimal'
+ # x = BigDecimal(2).as_json # => {"json_class"=>"BigDecimal", "b"=>"27:0.2e1"}
+ # y = BigDecimal(2.0, 4).as_json # => {"json_class"=>"BigDecimal", "b"=>"36:0.2e1"}
+ # z = BigDecimal(Complex(2, 0)).as_json # => {"json_class"=>"BigDecimal", "b"=>"27:0.2e1"}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \BigDecimal object:
+ #
+ # BigDecimal.json_create(x) # => 0.2e1
+ # BigDecimal.json_create(y) # => 0.2e1
+ # BigDecimal.json_create(z) # => 0.2e1
+ #
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'b' => _dump.force_encoding(Encoding::UTF_8),
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/bigdecimal'
+ # puts BigDecimal(2).to_json
+ # puts BigDecimal(2.0, 4).to_json
+ # puts BigDecimal(Complex(2, 0)).to_json
+ #
+ # Output:
+ #
+ # {"json_class":"BigDecimal","b":"27:0.2e1"}
+ # {"json_class":"BigDecimal","b":"36:0.2e1"}
+ # {"json_class":"BigDecimal","b":"27:0.2e1"}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end if defined?(::BigDecimal)
diff --git a/ext/json/lib/json/add/complex.rb b/ext/json/lib/json/add/complex.rb
new file mode 100644
index 0000000000..9e3c6f2d0a
--- /dev/null
+++ b/ext/json/lib/json/add/complex.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+
+class Complex
+
+ # See #as_json.
+ def self.json_create(object)
+ Complex(object['r'], object['i'])
+ end
+
+ # Methods <tt>Complex#as_json</tt> and +Complex.json_create+ may be used
+ # to serialize and deserialize a \Complex object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Complex#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/complex'
+ # x = Complex(2).as_json # => {"json_class"=>"Complex", "r"=>2, "i"=>0}
+ # y = Complex(2.0, 4).as_json # => {"json_class"=>"Complex", "r"=>2.0, "i"=>4}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Complex object:
+ #
+ # Complex.json_create(x) # => (2+0i)
+ # Complex.json_create(y) # => (2.0+4i)
+ #
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'r' => real,
+ 'i' => imag,
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/complex'
+ # puts Complex(2).to_json
+ # puts Complex(2.0, 4).to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Complex","r":2,"i":0}
+ # {"json_class":"Complex","r":2.0,"i":4}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
diff --git a/ext/json/lib/json/add/core.rb b/ext/json/lib/json/add/core.rb
index 4423e7ad75..61ff454212 100644
--- a/ext/json/lib/json/add/core.rb
+++ b/ext/json/lib/json/add/core.rb
@@ -1,135 +1,13 @@
-# This file contains implementations of ruby core's custom objects for
+# frozen_string_literal: true
+# This file requires the implementations of ruby core's custom objects for
# serialisation/deserialisation.
-unless Object.const_defined?(:JSON) and ::JSON.const_defined?(:JSON_LOADED) and
- ::JSON::JSON_LOADED
- require 'json'
-end
-require 'date'
-
-class Time
- def self.json_create(object)
- if usec = object.delete('u') # used to be tv_usec -> tv_nsec
- object['n'] = usec * 1000
- end
- if respond_to?(:tv_nsec)
- at(*object.values_at('s', 'n'))
- else
- at(object['s'], object['n'] / 1000)
- end
- end
-
- def to_json(*args)
- {
- 'json_class' => self.class.name,
- 's' => tv_sec,
- 'n' => respond_to?(:tv_nsec) ? tv_nsec : tv_usec * 1000
- }.to_json(*args)
- end
-end
-
-class Date
- def self.json_create(object)
- civil(*object.values_at('y', 'm', 'd', 'sg'))
- end
-
- alias start sg unless method_defined?(:start)
-
- def to_json(*args)
- {
- 'json_class' => self.class.name,
- 'y' => year,
- 'm' => month,
- 'd' => day,
- 'sg' => start,
- }.to_json(*args)
- end
-end
-
-class DateTime
- def self.json_create(object)
- args = object.values_at('y', 'm', 'd', 'H', 'M', 'S')
- of_a, of_b = object['of'].split('/')
- if of_b and of_b != '0'
- args << Rational(of_a.to_i, of_b.to_i)
- else
- args << of_a
- end
- args << object['sg']
- civil(*args)
- end
-
- alias start sg unless method_defined?(:start)
-
- def to_json(*args)
- {
- 'json_class' => self.class.name,
- 'y' => year,
- 'm' => month,
- 'd' => day,
- 'H' => hour,
- 'M' => min,
- 'S' => sec,
- 'of' => offset.to_s,
- 'sg' => start,
- }.to_json(*args)
- end
-end
-
-class Range
- def self.json_create(object)
- new(*object['a'])
- end
-
- def to_json(*args)
- {
- 'json_class' => self.class.name,
- 'a' => [ first, last, exclude_end? ]
- }.to_json(*args)
- end
-end
-
-class Struct
- def self.json_create(object)
- new(*object['v'])
- end
-
- def to_json(*args)
- klass = self.class.name
- klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
- {
- 'json_class' => klass,
- 'v' => values,
- }.to_json(*args)
- end
-end
-
-class Exception
- def self.json_create(object)
- result = new(object['m'])
- result.set_backtrace object['b']
- result
- end
-
- def to_json(*args)
- {
- 'json_class' => self.class.name,
- 'm' => message,
- 'b' => backtrace,
- }.to_json(*args)
- end
-end
-
-class Regexp
- def self.json_create(object)
- new(object['s'], object['o'])
- end
-
- def to_json(*)
- {
- 'json_class' => self.class.name,
- 'o' => options,
- 's' => source,
- }.to_json
- end
-end
+require 'json/add/date'
+require 'json/add/date_time'
+require 'json/add/exception'
+require 'json/add/range'
+require 'json/add/regexp'
+require 'json/add/string'
+require 'json/add/struct'
+require 'json/add/symbol'
+require 'json/add/time'
diff --git a/ext/json/lib/json/add/date.rb b/ext/json/lib/json/add/date.rb
new file mode 100644
index 0000000000..88a098b637
--- /dev/null
+++ b/ext/json/lib/json/add/date.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+require 'date'
+
+class Date
+
+ # See #as_json.
+ def self.json_create(object)
+ civil(*object.values_at('y', 'm', 'd', 'sg'))
+ end
+
+ alias start sg unless method_defined?(:start)
+
+ # Methods <tt>Date#as_json</tt> and +Date.json_create+ may be used
+ # to serialize and deserialize a \Date object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Date#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/date'
+ # x = Date.today.as_json
+ # # => {"json_class"=>"Date", "y"=>2023, "m"=>11, "d"=>21, "sg"=>2299161.0}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Date object:
+ #
+ # Date.json_create(x)
+ # # => #<Date: 2023-11-21 ((2460270j,0s,0n),+0s,2299161j)>
+ #
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'y' => year,
+ 'm' => month,
+ 'd' => day,
+ 'sg' => start,
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/date'
+ # puts Date.today.to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Date","y":2023,"m":11,"d":21,"sg":2299161.0}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
diff --git a/ext/json/lib/json/add/date_time.rb b/ext/json/lib/json/add/date_time.rb
new file mode 100644
index 0000000000..8b0bb5d181
--- /dev/null
+++ b/ext/json/lib/json/add/date_time.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+require 'date'
+
+class DateTime
+
+ # See #as_json.
+ def self.json_create(object)
+ args = object.values_at('y', 'm', 'd', 'H', 'M', 'S')
+ of_a, of_b = object['of'].split('/')
+ if of_b and of_b != '0'
+ args << Rational(of_a.to_i, of_b.to_i)
+ else
+ args << of_a
+ end
+ args << object['sg']
+ civil(*args)
+ end
+
+ alias start sg unless method_defined?(:start)
+
+ # Methods <tt>DateTime#as_json</tt> and +DateTime.json_create+ may be used
+ # to serialize and deserialize a \DateTime object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>DateTime#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/datetime'
+ # x = DateTime.now.as_json
+ # # => {"json_class"=>"DateTime", "y"=>2023, "m"=>11, "d"=>21, "sg"=>2299161.0}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \DateTime object:
+ #
+ # DateTime.json_create(x) # BUG? Raises Date::Error "invalid date"
+ #
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'y' => year,
+ 'm' => month,
+ 'd' => day,
+ 'H' => hour,
+ 'M' => min,
+ 'S' => sec,
+ 'of' => offset.to_s,
+ 'sg' => start,
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/datetime'
+ # puts DateTime.now.to_json
+ #
+ # Output:
+ #
+ # {"json_class":"DateTime","y":2023,"m":11,"d":21,"sg":2299161.0}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
+
+
diff --git a/ext/json/lib/json/add/exception.rb b/ext/json/lib/json/add/exception.rb
new file mode 100644
index 0000000000..e85d404982
--- /dev/null
+++ b/ext/json/lib/json/add/exception.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+
+class Exception
+
+ # See #as_json.
+ def self.json_create(object)
+ result = new(object['m'])
+ result.set_backtrace object['b']
+ result
+ end
+
+ # Methods <tt>Exception#as_json</tt> and +Exception.json_create+ may be used
+ # to serialize and deserialize a \Exception object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Exception#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/exception'
+ # x = Exception.new('Foo').as_json # => {"json_class"=>"Exception", "m"=>"Foo", "b"=>nil}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Exception object:
+ #
+ # Exception.json_create(x) # => #<Exception: Foo>
+ #
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'm' => message,
+ 'b' => backtrace,
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/exception'
+ # puts Exception.new('Foo').to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Exception","m":"Foo","b":null}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
diff --git a/ext/json/lib/json/add/ostruct.rb b/ext/json/lib/json/add/ostruct.rb
new file mode 100644
index 0000000000..7750498144
--- /dev/null
+++ b/ext/json/lib/json/add/ostruct.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+begin
+ require 'ostruct'
+rescue LoadError
+end
+
+class OpenStruct
+
+ # See #as_json.
+ def self.json_create(object)
+ new(object['t'] || object[:t])
+ end
+
+ # Methods <tt>OpenStruct#as_json</tt> and +OpenStruct.json_create+ may be used
+ # to serialize and deserialize a \OpenStruct object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>OpenStruct#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/ostruct'
+ # x = OpenStruct.new('name' => 'Rowdy', :age => nil).as_json
+ # # => {"json_class"=>"OpenStruct", "t"=>{:name=>'Rowdy', :age=>nil}}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \OpenStruct object:
+ #
+ # OpenStruct.json_create(x)
+ # # => #<OpenStruct name='Rowdy', age=nil>
+ #
+ def as_json(*)
+ klass = self.class.name
+ klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
+ {
+ JSON.create_id => klass,
+ 't' => table,
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/ostruct'
+ # puts OpenStruct.new('name' => 'Rowdy', :age => nil).to_json
+ #
+ # Output:
+ #
+ # {"json_class":"OpenStruct","t":{'name':'Rowdy',"age":null}}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end if defined?(::OpenStruct)
diff --git a/ext/json/lib/json/add/rails.rb b/ext/json/lib/json/add/rails.rb
deleted file mode 100644
index e86ed1aab9..0000000000
--- a/ext/json/lib/json/add/rails.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# This file contains implementations of rails custom objects for
-# serialisation/deserialisation.
-
-unless Object.const_defined?(:JSON) and ::JSON.const_defined?(:JSON_LOADED) and
- ::JSON::JSON_LOADED
- require 'json'
-end
-
-class Object
- def self.json_create(object)
- obj = new
- for key, value in object
- next if key == 'json_class'
- instance_variable_set "@#{key}", value
- end
- obj
- end
-
- def to_json(*a)
- result = {
- 'json_class' => self.class.name
- }
- instance_variables.inject(result) do |r, name|
- r[name[1..-1]] = instance_variable_get name
- r
- end
- result.to_json(*a)
- end
-end
-
-class Symbol
- def to_json(*a)
- to_s.to_json(*a)
- end
-end
-
-module Enumerable
- def to_json(*a)
- to_a.to_json(*a)
- end
-end
-
-# class Regexp
-# def to_json(*)
-# inspect
-# end
-# end
-#
-# The above rails definition has some problems:
-#
-# 1. { 'foo' => /bar/ }.to_json # => "{foo: /bar/}"
-# This isn't valid JSON, because the regular expression syntax is not
-# defined in RFC 4627. (And unquoted strings are disallowed there, too.)
-# Though it is valid Javascript.
-#
-# 2. { 'foo' => /bar/mix }.to_json # => "{foo: /bar/mix}"
-# This isn't even valid Javascript.
-
diff --git a/ext/json/lib/json/add/range.rb b/ext/json/lib/json/add/range.rb
new file mode 100644
index 0000000000..408d2c32f6
--- /dev/null
+++ b/ext/json/lib/json/add/range.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+
+class Range
+
+ # See #as_json.
+ def self.json_create(object)
+ new(*object['a'])
+ end
+
+ # Methods <tt>Range#as_json</tt> and +Range.json_create+ may be used
+ # to serialize and deserialize a \Range object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Range#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/range'
+ # x = (1..4).as_json # => {"json_class"=>"Range", "a"=>[1, 4, false]}
+ # y = (1...4).as_json # => {"json_class"=>"Range", "a"=>[1, 4, true]}
+ # z = ('a'..'d').as_json # => {"json_class"=>"Range", "a"=>["a", "d", false]}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Range object:
+ #
+ # Range.json_create(x) # => 1..4
+ # Range.json_create(y) # => 1...4
+ # Range.json_create(z) # => "a".."d"
+ #
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'a' => [ first, last, exclude_end? ]
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/range'
+ # puts (1..4).to_json
+ # puts (1...4).to_json
+ # puts ('a'..'d').to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Range","a":[1,4,false]}
+ # {"json_class":"Range","a":[1,4,true]}
+ # {"json_class":"Range","a":["a","d",false]}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
diff --git a/ext/json/lib/json/add/rational.rb b/ext/json/lib/json/add/rational.rb
new file mode 100644
index 0000000000..c95812ea8e
--- /dev/null
+++ b/ext/json/lib/json/add/rational.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+
+class Rational
+
+ # See #as_json.
+ def self.json_create(object)
+ Rational(object['n'], object['d'])
+ end
+
+ # Methods <tt>Rational#as_json</tt> and +Rational.json_create+ may be used
+ # to serialize and deserialize a \Rational object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Rational#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/rational'
+ # x = Rational(2, 3).as_json
+ # # => {"json_class"=>"Rational", "n"=>2, "d"=>3}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Rational object:
+ #
+ # Rational.json_create(x)
+ # # => (2/3)
+ #
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'n' => numerator,
+ 'd' => denominator,
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/rational'
+ # puts Rational(2, 3).to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Rational","n":2,"d":3}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
diff --git a/ext/json/lib/json/add/regexp.rb b/ext/json/lib/json/add/regexp.rb
new file mode 100644
index 0000000000..aebfb2db5c
--- /dev/null
+++ b/ext/json/lib/json/add/regexp.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+
+class Regexp
+
+ # See #as_json.
+ def self.json_create(object)
+ new(object['s'], object['o'])
+ end
+
+ # Methods <tt>Regexp#as_json</tt> and +Regexp.json_create+ may be used
+ # to serialize and deserialize a \Regexp object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Regexp#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/regexp'
+ # x = /foo/.as_json
+ # # => {"json_class"=>"Regexp", "o"=>0, "s"=>"foo"}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Regexp object:
+ #
+ # Regexp.json_create(x) # => /foo/
+ #
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'o' => options,
+ 's' => source,
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/regexp'
+ # puts /foo/.to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Regexp","o":0,"s":"foo"}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
diff --git a/ext/json/lib/json/add/set.rb b/ext/json/lib/json/add/set.rb
new file mode 100644
index 0000000000..1918353187
--- /dev/null
+++ b/ext/json/lib/json/add/set.rb
@@ -0,0 +1,48 @@
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+defined?(::Set) or require 'set'
+
+class Set
+
+ # See #as_json.
+ def self.json_create(object)
+ new object['a']
+ end
+
+ # Methods <tt>Set#as_json</tt> and +Set.json_create+ may be used
+ # to serialize and deserialize a \Set object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Set#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/set'
+ # x = Set.new(%w/foo bar baz/).as_json
+ # # => {"json_class"=>"Set", "a"=>["foo", "bar", "baz"]}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Set object:
+ #
+ # Set.json_create(x) # => #<Set: {"foo", "bar", "baz"}>
+ #
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'a' => to_a,
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/set'
+ # puts Set.new(%w/foo bar baz/).to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Set","a":["foo","bar","baz"]}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
+
diff --git a/ext/json/lib/json/add/string.rb b/ext/json/lib/json/add/string.rb
new file mode 100644
index 0000000000..9c3bde27fb
--- /dev/null
+++ b/ext/json/lib/json/add/string.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+
+class String
+ # call-seq: json_create(o)
+ #
+ # 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(object)
+ object["raw"].pack("C*")
+ end
+
+ # call-seq: to_json_raw_object()
+ #
+ # This method creates a raw object hash, that can be nested into
+ # other data structures and will be generated as a raw string. This
+ # method should be used, if you want to convert raw strings to JSON
+ # instead of UTF-8 strings, e. g. binary data.
+ def to_json_raw_object
+ {
+ JSON.create_id => self.class.name,
+ "raw" => unpack("C*"),
+ }
+ end
+
+ # call-seq: to_json_raw(*args)
+ #
+ # This method creates a JSON text from the result of a call to
+ # to_json_raw_object of this String.
+ def to_json_raw(...)
+ to_json_raw_object.to_json(...)
+ end
+end
diff --git a/ext/json/lib/json/add/struct.rb b/ext/json/lib/json/add/struct.rb
new file mode 100644
index 0000000000..6760c3d86c
--- /dev/null
+++ b/ext/json/lib/json/add/struct.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+
+class Struct
+
+ # See #as_json.
+ def self.json_create(object)
+ new(*object['v'])
+ end
+
+ # Methods <tt>Struct#as_json</tt> and +Struct.json_create+ may be used
+ # to serialize and deserialize a \Struct object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Struct#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/struct'
+ # Customer = Struct.new('Customer', :name, :address, :zip)
+ # x = Struct::Customer.new.as_json
+ # # => {"json_class"=>"Struct::Customer", "v"=>[nil, nil, nil]}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Struct object:
+ #
+ # Struct::Customer.json_create(x)
+ # # => #<struct Struct::Customer name=nil, address=nil, zip=nil>
+ #
+ def as_json(*)
+ klass = self.class.name
+ klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
+ {
+ JSON.create_id => klass,
+ 'v' => values,
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/struct'
+ # Customer = Struct.new('Customer', :name, :address, :zip)
+ # puts Struct::Customer.new.to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Struct","t":{'name':'Rowdy',"age":null}}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
diff --git a/ext/json/lib/json/add/symbol.rb b/ext/json/lib/json/add/symbol.rb
new file mode 100644
index 0000000000..806be4f025
--- /dev/null
+++ b/ext/json/lib/json/add/symbol.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+
+class Symbol
+
+ # Methods <tt>Symbol#as_json</tt> and +Symbol.json_create+ may be used
+ # to serialize and deserialize a \Symbol object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Symbol#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/symbol'
+ # x = :foo.as_json
+ # # => {"json_class"=>"Symbol", "s"=>"foo"}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Symbol object:
+ #
+ # Symbol.json_create(x) # => :foo
+ #
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 's' => to_s,
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/symbol'
+ # puts :foo.to_json
+ #
+ # Output:
+ #
+ # # {"json_class":"Symbol","s":"foo"}
+ #
+ def to_json(state = nil, *a)
+ state = ::JSON::State.from_state(state)
+ if state.strict?
+ super
+ else
+ as_json.to_json(state, *a)
+ end
+ end
+
+ # See #as_json.
+ def self.json_create(o)
+ o['s'].to_sym
+ end
+end
diff --git a/ext/json/lib/json/add/time.rb b/ext/json/lib/json/add/time.rb
new file mode 100644
index 0000000000..b03d4ff251
--- /dev/null
+++ b/ext/json/lib/json/add/time.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+
+class Time
+
+ # See #as_json.
+ def self.json_create(object)
+ if usec = object.delete('u') # used to be tv_usec -> tv_nsec
+ object['n'] = usec * 1000
+ end
+ at(object['s'], Rational(object['n'], 1000))
+ end
+
+ # Methods <tt>Time#as_json</tt> and +Time.json_create+ may be used
+ # to serialize and deserialize a \Time object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Time#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/time'
+ # x = Time.now.as_json
+ # # => {"json_class"=>"Time", "s"=>1700931656, "n"=>472846644}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Time object:
+ #
+ # Time.json_create(x)
+ # # => 2023-11-25 11:00:56.472846644 -0600
+ #
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 's' => tv_sec,
+ 'n' => tv_nsec,
+ }
+ end
+
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/time'
+ # puts Time.now.to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Time","s":1700931678,"n":980650786}
+ #
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb
index b816a66cf1..230bf08012 100755..100644
--- a/ext/json/lib/json/common.rb
+++ b/ext/json/lib/json/common.rb
@@ -1,309 +1,1126 @@
+# frozen_string_literal: true
+
require 'json/version'
module JSON
+ autoload :GenericObject, 'json/generic_object'
+
+ module ParserOptions # :nodoc:
+ class << self
+ def prepare(opts)
+ if opts[:object_class] || opts[:array_class]
+ opts = opts.dup
+ on_load = opts[:on_load]
+
+ on_load = object_class_proc(opts[:object_class], on_load) if opts[:object_class]
+ on_load = array_class_proc(opts[:array_class], on_load) if opts[:array_class]
+ opts[:on_load] = on_load
+ end
+
+ if opts.fetch(:create_additions, false) != false
+ opts = create_additions_proc(opts)
+ end
+
+ opts
+ end
+
+ private
+
+ def object_class_proc(object_class, on_load)
+ ->(obj) do
+ if Hash === obj
+ object = object_class.new
+ obj.each { |k, v| object[k] = v }
+ obj = object
+ end
+ on_load.nil? ? obj : on_load.call(obj)
+ end
+ end
+
+ def array_class_proc(array_class, on_load)
+ ->(obj) do
+ if Array === obj
+ array = array_class.new
+ obj.each { |v| array << v }
+ obj = array
+ end
+ on_load.nil? ? obj : on_load.call(obj)
+ end
+ end
+
+ # TODO: extract :create_additions support to another gem for version 3.0
+ def create_additions_proc(opts)
+ if opts[:symbolize_names]
+ raise ArgumentError, "options :symbolize_names and :create_additions cannot be used in conjunction"
+ end
+
+ opts = opts.dup
+ create_additions = opts.fetch(:create_additions, false)
+ on_load = opts[:on_load]
+ object_class = opts[:object_class] || Hash
+
+ opts[:on_load] = ->(object) do
+ case object
+ when String
+ opts[:match_string]&.each do |pattern, klass|
+ if match = pattern.match(object)
+ create_additions_warning if create_additions.nil?
+ object = klass.json_create(object)
+ break
+ end
+ end
+ when object_class
+ if opts[:create_additions] != false
+ if class_path = object[JSON.create_id]
+ klass = begin
+ Object.const_get(class_path)
+ rescue NameError => e
+ raise ArgumentError, "can't get const #{class_path}: #{e}"
+ end
+
+ if klass.respond_to?(:json_creatable?) ? klass.json_creatable? : klass.respond_to?(:json_create)
+ create_additions_warning if create_additions.nil?
+ object = klass.json_create(object)
+ end
+ end
+ end
+ end
+
+ on_load.nil? ? object : on_load.call(object)
+ end
+
+ opts
+ end
+
+ def create_additions_warning
+ JSON.deprecation_warning "JSON.load implicit support for `create_additions: true` is deprecated " \
+ "and will be removed in 3.0, use JSON.unsafe_load or explicitly " \
+ "pass `create_additions: true`"
+ end
+ end
+ end
+
class << self
- # If _object_ is string-like parse the string and return the parsed result
- # as a Ruby data structure. Otherwise generate a JSON text from the Ruby
- # data structure object and return it.
- #
- # The _opts_ argument is passed through to generate/parse respectively, see
- # generate and parse for their documentation.
- def [](object, opts = {})
- if object.respond_to? :to_str
- JSON.parse(object.to_str, opts => {})
+ def deprecation_warning(message, uplevel = 3) # :nodoc:
+ gem_root = File.expand_path("..", __dir__) + "/"
+ caller_locations(uplevel, 10).each do |frame|
+ if frame.path.nil? || frame.path.start_with?(gem_root) || frame.path.end_with?("/truffle/cext_ruby.rb", ".c")
+ uplevel += 1
+ else
+ break
+ end
+ end
+
+ if RUBY_VERSION >= "3.0"
+ warn(message, uplevel: uplevel, category: :deprecated)
else
- JSON.generate(object, opts => {})
+ warn(message, uplevel: uplevel)
end
end
- # Returns the JSON parser class, that is used by JSON. This might be either
- # JSON::Ext::Parser or JSON::Pure::Parser.
+ # :call-seq:
+ # JSON[object] -> new_array or new_string
+ #
+ # If +object+ is a \String,
+ # calls JSON.parse with +object+ and +opts+ (see method #parse):
+ # json = '[0, 1, null]'
+ # JSON[json]# => [0, 1, nil]
+ #
+ # Otherwise, calls JSON.generate with +object+ and +opts+ (see method #generate):
+ # ruby = [0, 1, nil]
+ # JSON[ruby] # => '[0,1,null]'
+ def [](object, opts = nil)
+ if object.is_a?(String)
+ return JSON.parse(object, opts)
+ elsif object.respond_to?(:to_str)
+ str = object.to_str
+ if str.is_a?(String)
+ return JSON.parse(str, opts)
+ end
+ end
+
+ JSON.generate(object, opts)
+ end
+
+ # Returns the JSON parser class that is used by JSON.
attr_reader :parser
# Set the JSON parser class _parser_ to be used by JSON.
def parser=(parser) # :nodoc:
@parser = parser
- remove_const :Parser if const_defined? :Parser
+ remove_const :Parser if const_defined?(:Parser, false)
const_set :Parser, parser
end
- # Return the constant located at _path_. The format of _path_ has to be
- # either ::A::B::C or A::B::C. In any case A has to be located at the top
- # level (absolute namespace path?). If there doesn't exist a constant at
- # the given path, an ArgumentError is raised.
- def deep_const_get(path) # :nodoc:
- path = path.to_s
- path.split(/::/).inject(Object) do |p, c|
- case
- when c.empty? then p
- when p.const_defined?(c) then p.const_get(c)
- else raise ArgumentError, "can't find const #{path}"
- end
- end
- end
-
# Set the module _generator_ to be used by JSON.
def generator=(generator) # :nodoc:
+ old, $VERBOSE = $VERBOSE, nil
@generator = generator
- generator_methods = generator::GeneratorMethods
- for const in generator_methods.constants
- klass = deep_const_get(const)
- modul = generator_methods.const_get(const)
- klass.class_eval do
- instance_methods(false).each do |m|
- m.to_s == 'to_json' and remove_method m
+ if generator.const_defined?(:GeneratorMethods)
+ generator_methods = generator::GeneratorMethods
+ for const in generator_methods.constants
+ klass = const_get(const)
+ modul = generator_methods.const_get(const)
+ klass.class_eval do
+ instance_methods(false).each do |m|
+ m.to_s == 'to_json' and remove_method m
+ end
+ include modul
end
- include modul
end
end
self.state = generator::State
- const_set :State, self.state
+ const_set :State, state
+ ensure
+ $VERBOSE = old
end
- # Returns the JSON generator modul, that is used by JSON. This might be
- # either JSON::Ext::Generator or JSON::Pure::Generator.
+ # Returns the JSON generator module that is used by JSON.
attr_reader :generator
- # Returns the JSON generator state class, that is used by JSON. This might
- # be either JSON::Ext::Generator::State or JSON::Pure::Generator::State.
+ # Sets or Returns the JSON generator state class that is used by JSON.
attr_accessor :state
- # This is create identifier, that is used to decide, if the _json_create_
- # hook of a class should be called. It defaults to 'json_class'.
- attr_accessor :create_id
+ private
+
+ # Called from the extension when a hash has both string and symbol keys
+ def on_mixed_keys_hash(hash, do_raise)
+ set = {}
+ hash.each_key do |key|
+ key_str = key.to_s
+
+ if set[key_str]
+ message = "detected duplicate key #{key_str.inspect} in #{hash.inspect}"
+ if do_raise
+ raise GeneratorError, message
+ else
+ deprecation_warning("#{message}.\nThis will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`")
+ end
+ else
+ set[key_str] = true
+ end
+ end
+ end
+
+ def deprecated_singleton_attr_accessor(*attrs)
+ args = RUBY_VERSION >= "3.0" ? ", category: :deprecated" : ""
+ attrs.each do |attr|
+ singleton_class.class_eval <<~RUBY
+ def #{attr}
+ warn "JSON.#{attr} is deprecated and will be removed in json 3.0.0", uplevel: 1 #{args}
+ @#{attr}
+ end
+
+ def #{attr}=(val)
+ warn "JSON.#{attr}= is deprecated and will be removed in json 3.0.0", uplevel: 1 #{args}
+ @#{attr} = val
+ end
+
+ def _#{attr}
+ @#{attr}
+ end
+ RUBY
+ end
+ end
+ end
+
+ # Sets create identifier, which is used to decide if the _json_create_
+ # hook of a class should be called; initial value is +json_class+:
+ # JSON.create_id # => 'json_class'
+ def self.create_id=(new_value)
+ Thread.current[:"JSON.create_id"] = new_value.dup.freeze
end
- self.create_id = 'json_class'
- NaN = 0.0/0
+ # Returns the current create identifier.
+ # See also JSON.create_id=.
+ def self.create_id
+ Thread.current[:"JSON.create_id"] || 'json_class'
+ end
+
+ NaN = Float::NAN
- Infinity = 1.0/0
+ Infinity = Float::INFINITY
MinusInfinity = -Infinity
# The base exception for JSON errors.
class JSONError < StandardError; end
- # This exception is raised, if a parser error occurs.
- class ParserError < JSONError; end
+ # This exception is raised if a parser error occurs.
+ class ParserError < JSONError
+ attr_reader :line, :column
+ end
- # This exception is raised, if the nesting of parsed datastructures is too
+ # This exception is raised if the nesting of parsed data structures is too
# deep.
class NestingError < ParserError; end
- # This exception is raised, if a generator or unparser error occurs.
- class GeneratorError < JSONError; end
- # For backwards compatibility
- UnparserError = GeneratorError
+ # This exception is raised if a generator or unparser error occurs.
+ class GeneratorError < JSONError
+ attr_reader :invalid_object
- # If a circular data structure is encountered while unparsing
- # this exception is raised.
- class CircularDatastructure < GeneratorError; end
+ def initialize(message, invalid_object = nil)
+ super(message)
+ @invalid_object = invalid_object
+ end
- # This exception is raised, if the required unicode support is missing on the
- # system. Usually this means, that the iconv library is not installed.
- class MissingUnicodeSupport < JSONError; end
+ def detailed_message(...)
+ # Exception#detailed_message doesn't exist until Ruby 3.2
+ super_message = defined?(super) ? super : message
- module_function
+ if @invalid_object.nil?
+ super_message
+ else
+ "#{super_message}\nInvalid object: #{@invalid_object.inspect}"
+ end
+ end
+ end
+
+ # Fragment of JSON document that is to be included as is:
+ # fragment = JSON::Fragment.new("[1, 2, 3]")
+ # JSON.generate({ count: 3, items: fragments })
+ #
+ # This allows to easily assemble multiple JSON fragments that have
+ # been persisted somewhere without having to parse them nor resorting
+ # to string interpolation.
+ #
+ # Note: no validation is performed on the provided string. It is the
+ # responsibility of the caller to ensure the string contains valid JSON.
+ Fragment = Struct.new(:json) do
+ def initialize(json)
+ unless string = String.try_convert(json)
+ raise TypeError, " no implicit conversion of #{json.class} into String"
+ end
- # Parse the JSON string _source_ into a Ruby data structure and return it.
- #
- # _opts_ can have the following
- # keys:
- # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
- # structures. Disable depth checking with :max_nesting => false, it defaults
- # to 19.
- # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
- # defiance of RFC 4627 to be parsed by the Parser. This option defaults
- # to false.
- # * *create_additions*: If set to false, the Parser doesn't create
- # additions even if a matchin class and create_id was found. This option
- # defaults to true.
- def parse(source, opts = {})
- JSON.parser.new(source, opts).parse
+ super(string)
+ end
+
+ def to_json(state = nil, *)
+ json
+ end
end
- # Parse the JSON string _source_ into a Ruby data structure and return it.
- # The bang version of the parse method, defaults to the more dangerous values
- # for the _opts_ hash, so be sure only to parse trusted _source_ strings.
- #
- # _opts_ can have the following keys:
- # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
- # structures. Enable depth checking with :max_nesting => anInteger. The parse!
- # methods defaults to not doing max depth checking: This can be dangerous,
- # if someone wants to fill up your stack.
- # * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in
- # defiance of RFC 4627 to be parsed by the Parser. This option defaults
- # to true.
- # * *create_additions*: If set to false, the Parser doesn't create
- # additions even if a matchin class and create_id was found. This option
- # defaults to true.
- def parse!(source, opts = {})
- opts = {
- :max_nesting => false,
- :allow_nan => true
- }.update(opts)
- JSON.parser.new(source, opts).parse
+ module_function
+
+ # :call-seq:
+ # JSON.parse(source, opts) -> object
+ #
+ # Returns the Ruby objects created by parsing the given +source+.
+ #
+ # Argument +source+ contains the \String to be parsed.
+ #
+ # Argument +opts+, if given, contains a \Hash of options for the parsing.
+ # See {Parsing Options}[#module-JSON-label-Parsing+Options].
+ #
+ # ---
+ #
+ # When +source+ is a \JSON array, returns a Ruby \Array:
+ # source = '["foo", 1.0, true, false, null]'
+ # ruby = JSON.parse(source)
+ # ruby # => ["foo", 1.0, true, false, nil]
+ # ruby.class # => Array
+ #
+ # When +source+ is a \JSON object, returns a Ruby \Hash:
+ # source = '{"a": "foo", "b": 1.0, "c": true, "d": false, "e": null}'
+ # ruby = JSON.parse(source)
+ # ruby # => {"a"=>"foo", "b"=>1.0, "c"=>true, "d"=>false, "e"=>nil}
+ # ruby.class # => Hash
+ #
+ # For examples of parsing for all \JSON data types, see
+ # {Parsing \JSON}[#module-JSON-label-Parsing+JSON].
+ #
+ # Parses nested JSON objects:
+ # source = <<~JSON
+ # {
+ # "name": "Dave",
+ # "age" :40,
+ # "hats": [
+ # "Cattleman's",
+ # "Panama",
+ # "Tophat"
+ # ]
+ # }
+ # JSON
+ # ruby = JSON.parse(source)
+ # ruby # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # ---
+ #
+ # Raises an exception if +source+ is not valid JSON:
+ # # Raises JSON::ParserError (783: unexpected token at ''):
+ # JSON.parse('')
+ #
+ def parse(source, opts = nil)
+ opts = ParserOptions.prepare(opts) unless opts.nil?
+ Parser.parse(source, opts)
end
- # Unparse the Ruby data structure _obj_ into a single line JSON string and
- # return it. _state_ is
- # * a JSON::State object,
- # * or a Hash like object (responding to to_hash),
- # * an object convertible into a hash by a to_h method,
- # that is used as or to configure a State object.
- #
- # It defaults to a state object, that creates the shortest possible JSON text
- # in one line, checks for circular data structures and doesn't allow NaN,
- # Infinity, and -Infinity.
- #
- # A _state_ hash can have the following keys:
- # * *indent*: a string used to indent levels (default: ''),
- # * *space*: a string that is put after, a : or , delimiter (default: ''),
- # * *space_before*: a string that is put before a : pair delimiter (default: ''),
- # * *object_nl*: a string that is put at the end of a JSON object (default: ''),
- # * *array_nl*: a string that is put at the end of a JSON array (default: ''),
- # * *check_circular*: true if checking for circular data structures
- # should be done (the default), false otherwise.
- # * *allow_nan*: true if NaN, Infinity, and -Infinity should be
- # generated, otherwise an exception is thrown, if these values are
- # encountered. This options defaults to false.
- # * *max_nesting*: The maximum depth of nesting allowed in the data
- # structures from which JSON is to be generated. Disable depth checking
- # with :max_nesting => false, it defaults to 19.
- #
- # See also the fast_generate for the fastest creation method with the least
- # amount of sanity checks, and the pretty_generate method for some
- # defaults for a pretty output.
- def generate(obj, state = nil)
- if state
- state = State.from_state(state)
+ PARSE_L_OPTIONS = {
+ max_nesting: false,
+ allow_nan: true,
+ }.freeze
+ private_constant :PARSE_L_OPTIONS
+
+ # :call-seq:
+ # JSON.parse!(source, opts) -> object
+ #
+ # Calls
+ # parse(source, opts)
+ # with +source+ and possibly modified +opts+.
+ #
+ # Differences from JSON.parse:
+ # - Option +max_nesting+, if not provided, defaults to +false+,
+ # which disables checking for nesting depth.
+ # - Option +allow_nan+, if not provided, defaults to +true+.
+ def parse!(source, opts = nil)
+ if opts.nil?
+ parse(source, PARSE_L_OPTIONS)
else
- state = State.new
+ parse(source, PARSE_L_OPTIONS.merge(opts))
end
- obj.to_json(state)
end
- # :stopdoc:
- # I want to deprecate these later, so I'll first be silent about them, and
- # later delete them.
- alias unparse generate
- module_function :unparse
- # :startdoc:
+ # :call-seq:
+ # JSON.load_file(path, opts={}) -> object
+ #
+ # Calls:
+ # parse(File.read(path), opts)
+ #
+ # See method #parse.
+ def load_file(filespec, opts = nil)
+ parse(File.read(filespec, encoding: Encoding::UTF_8), opts)
+ end
- # Unparse the Ruby data structure _obj_ into a single line JSON string and
- # return it. This method disables the checks for circles in Ruby objects, and
- # also generates NaN, Infinity, and, -Infinity float values.
+ # :call-seq:
+ # JSON.load_file!(path, opts = {})
#
- # *WARNING*: Be careful not to pass any Ruby data structures with circles as
- # _obj_ argument, because this will cause JSON to go into an infinite loop.
- def fast_generate(obj)
- obj.to_json(nil)
+ # Calls:
+ # JSON.parse!(File.read(path, opts))
+ #
+ # See method #parse!
+ def load_file!(filespec, opts = nil)
+ parse!(File.read(filespec, encoding: Encoding::UTF_8), opts)
end
- # :stopdoc:
- # I want to deprecate these later, so I'll first be silent about them, and later delete them.
- alias fast_unparse fast_generate
- module_function :fast_unparse
- # :startdoc:
+ # :call-seq:
+ # JSON.generate(obj, opts = nil) -> new_string
+ #
+ # Returns a \String containing the generated \JSON data.
+ #
+ # See also JSON.pretty_generate.
+ #
+ # Argument +obj+ is the Ruby object to be converted to \JSON.
+ #
+ # Argument +opts+, if given, contains a \Hash of options for the generation.
+ # See {Generating Options}[#module-JSON-label-Generating+Options].
+ #
+ # ---
+ #
+ # When +obj+ is an \Array, returns a \String containing a \JSON array:
+ # obj = ["foo", 1.0, true, false, nil]
+ # json = JSON.generate(obj)
+ # json # => '["foo",1.0,true,false,null]'
+ #
+ # When +obj+ is a \Hash, returns a \String containing a \JSON object:
+ # obj = {foo: 0, bar: 's', baz: :bat}
+ # json = JSON.generate(obj)
+ # json # => '{"foo":0,"bar":"s","baz":"bat"}'
+ #
+ # For examples of generating from other Ruby objects, see
+ # {Generating \JSON from Other Objects}[#module-JSON-label-Generating+JSON+from+Other+Objects].
+ #
+ # ---
+ #
+ # Raises an exception if any formatting option is not a \String.
+ #
+ # Raises an exception if +obj+ contains circular references:
+ # a = []; b = []; a.push(b); b.push(a)
+ # # Raises JSON::NestingError (nesting of 100 is too deep):
+ # JSON.generate(a)
+ #
+ def generate(obj, opts = nil)
+ if State === opts
+ opts.generate(obj)
+ else
+ State.generate(obj, opts, nil)
+ end
+ end
+
+ # :call-seq:
+ # JSON.fast_generate(obj, opts) -> new_string
+ #
+ # Arguments +obj+ and +opts+ here are the same as
+ # arguments +obj+ and +opts+ in JSON.generate.
+ #
+ # By default, generates \JSON data without checking
+ # for circular references in +obj+ (option +max_nesting+ set to +false+, disabled).
+ #
+ # Raises an exception if +obj+ contains circular references:
+ # a = []; b = []; a.push(b); b.push(a)
+ # # Raises SystemStackError (stack level too deep):
+ # JSON.fast_generate(a)
+ def fast_generate(obj, opts = nil)
+ if RUBY_VERSION >= "3.0"
+ warn "JSON.fast_generate is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1, category: :deprecated
+ else
+ warn "JSON.fast_generate is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1
+ end
+ generate(obj, opts)
+ end
+
+ PRETTY_GENERATE_OPTIONS = {
+ indent: ' ',
+ space: ' ',
+ object_nl: "\n",
+ array_nl: "\n",
+ }.freeze
+ private_constant :PRETTY_GENERATE_OPTIONS
- # 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.
+ # :call-seq:
+ # JSON.pretty_generate(obj, opts = nil) -> new_string
+ #
+ # Arguments +obj+ and +opts+ here are the same as
+ # arguments +obj+ and +opts+ in JSON.generate.
+ #
+ # Default options are:
+ # {
+ # indent: ' ', # Two spaces
+ # space: ' ', # One space
+ # array_nl: "\n", # Newline
+ # object_nl: "\n" # Newline
+ # }
+ #
+ # Example:
+ # obj = {foo: [:bar, :baz], bat: {bam: 0, bad: 1}}
+ # json = JSON.pretty_generate(obj)
+ # puts json
+ # Output:
+ # {
+ # "foo": [
+ # "bar",
+ # "baz"
+ # ],
+ # "bat": {
+ # "bam": 0,
+ # "bad": 1
+ # }
+ # }
#
- # The _opts_ argument can be used to configure the generator, see the
- # generate method for a more detailed explanation.
def pretty_generate(obj, opts = nil)
- state = JSON.state.new(
- :indent => ' ',
- :space => ' ',
- :object_nl => "\n",
- :array_nl => "\n",
- :check_circular => true
- )
+ return opts.generate(obj) if State === opts
+
+ options = PRETTY_GENERATE_OPTIONS
+
if opts
- if opts.respond_to? :to_hash
- opts = opts.to_hash
- elsif opts.respond_to? :to_h
- opts = opts.to_h
+ unless opts.is_a?(Hash)
+ if opts.respond_to? :to_hash
+ opts = opts.to_hash
+ elsif opts.respond_to? :to_h
+ opts = opts.to_h
+ else
+ raise TypeError, "can't convert #{opts.class} into Hash"
+ end
+ end
+ options = options.merge(opts)
+ end
+
+ State.generate(obj, options, nil)
+ end
+
+ # Sets or returns default options for the JSON.unsafe_load method.
+ # Initially:
+ # opts = JSON.load_default_options
+ # opts # => {:max_nesting=>false, :allow_nan=>true, :allow_blank=>true, :create_additions=>true}
+ deprecated_singleton_attr_accessor :unsafe_load_default_options
+
+ @unsafe_load_default_options = {
+ :max_nesting => false,
+ :allow_nan => true,
+ :allow_blank => true,
+ :create_additions => true,
+ }
+
+ # Sets or returns default options for the JSON.load method.
+ # Initially:
+ # opts = JSON.load_default_options
+ # opts # => {:max_nesting=>false, :allow_nan=>true, :allow_blank=>true, :create_additions=>true}
+ deprecated_singleton_attr_accessor :load_default_options
+
+ @load_default_options = {
+ :allow_nan => true,
+ :allow_blank => true,
+ :create_additions => nil,
+ }
+ # :call-seq:
+ # JSON.unsafe_load(source, options = {}) -> object
+ # JSON.unsafe_load(source, proc = nil, options = {}) -> object
+ #
+ # Returns the Ruby objects created by parsing the given +source+.
+ #
+ # BEWARE: This method is meant to serialise data from trusted user input,
+ # like from your own database server or clients under your control, it could
+ # be dangerous to allow untrusted users to pass JSON sources into it.
+ #
+ # - Argument +source+ must be, or be convertible to, a \String:
+ # - If +source+ responds to instance method +to_str+,
+ # <tt>source.to_str</tt> becomes the source.
+ # - If +source+ responds to instance method +to_io+,
+ # <tt>source.to_io.read</tt> becomes the source.
+ # - If +source+ responds to instance method +read+,
+ # <tt>source.read</tt> becomes the source.
+ # - If both of the following are true, source becomes the \String <tt>'null'</tt>:
+ # - Option +allow_blank+ specifies a truthy value.
+ # - The source, as defined above, is +nil+ or the empty \String <tt>''</tt>.
+ # - Otherwise, +source+ remains the source.
+ # - Argument +proc+, if given, must be a \Proc that accepts one argument.
+ # It will be called recursively with each result (depth-first order).
+ # See details below.
+ # - Argument +opts+, if given, contains a \Hash of options for the parsing.
+ # See {Parsing Options}[#module-JSON-label-Parsing+Options].
+ # The default options can be changed via method JSON.unsafe_load_default_options=.
+ #
+ # ---
+ #
+ # When no +proc+ is given, modifies +source+ as above and returns the result of
+ # <tt>parse(source, opts)</tt>; see #parse.
+ #
+ # Source for following examples:
+ # source = <<~JSON
+ # {
+ # "name": "Dave",
+ # "age" :40,
+ # "hats": [
+ # "Cattleman's",
+ # "Panama",
+ # "Tophat"
+ # ]
+ # }
+ # JSON
+ #
+ # Load a \String:
+ # ruby = JSON.unsafe_load(source)
+ # ruby # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # Load an \IO object:
+ # require 'stringio'
+ # object = JSON.unsafe_load(StringIO.new(source))
+ # object # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # Load a \File object:
+ # path = 't.json'
+ # File.write(path, source)
+ # File.open(path) do |file|
+ # JSON.unsafe_load(file)
+ # end # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # ---
+ #
+ # When +proc+ is given:
+ # - Modifies +source+ as above.
+ # - Gets the +result+ from calling <tt>parse(source, opts)</tt>.
+ # - Recursively calls <tt>proc(result)</tt>.
+ # - Returns the final result.
+ #
+ # Example:
+ # require 'json'
+ #
+ # # Some classes for the example.
+ # class Base
+ # def initialize(attributes)
+ # @attributes = attributes
+ # end
+ # end
+ # class User < Base; end
+ # class Account < Base; end
+ # class Admin < Base; end
+ # # The JSON source.
+ # json = <<-EOF
+ # {
+ # "users": [
+ # {"type": "User", "username": "jane", "email": "jane@example.com"},
+ # {"type": "User", "username": "john", "email": "john@example.com"}
+ # ],
+ # "accounts": [
+ # {"account": {"type": "Account", "paid": true, "account_id": "1234"}},
+ # {"account": {"type": "Account", "paid": false, "account_id": "1235"}}
+ # ],
+ # "admins": {"type": "Admin", "password": "0wn3d"}
+ # }
+ # EOF
+ # # Deserializer method.
+ # def deserialize_obj(obj, safe_types = %w(User Account Admin))
+ # type = obj.is_a?(Hash) && obj["type"]
+ # safe_types.include?(type) ? Object.const_get(type).new(obj) : obj
+ # end
+ # # Call to JSON.unsafe_load
+ # ruby = JSON.unsafe_load(json, proc {|obj|
+ # case obj
+ # when Hash
+ # obj.each {|k, v| obj[k] = deserialize_obj v }
+ # when Array
+ # obj.map! {|v| deserialize_obj v }
+ # end
+ # obj
+ # })
+ # pp ruby
+ # Output:
+ # {"users"=>
+ # [#<User:0x00000000064c4c98
+ # @attributes=
+ # {"type"=>"User", "username"=>"jane", "email"=>"jane@example.com"}>,
+ # #<User:0x00000000064c4bd0
+ # @attributes=
+ # {"type"=>"User", "username"=>"john", "email"=>"john@example.com"}>],
+ # "accounts"=>
+ # [{"account"=>
+ # #<Account:0x00000000064c4928
+ # @attributes={"type"=>"Account", "paid"=>true, "account_id"=>"1234"}>},
+ # {"account"=>
+ # #<Account:0x00000000064c4680
+ # @attributes={"type"=>"Account", "paid"=>false, "account_id"=>"1235"}>}],
+ # "admins"=>
+ # #<Admin:0x00000000064c41f8
+ # @attributes={"type"=>"Admin", "password"=>"0wn3d"}>}
+ #
+ def unsafe_load(source, proc = nil, options = nil)
+ opts = if options.nil?
+ if proc && proc.is_a?(Hash)
+ options, proc = proc, nil
+ options
else
- raise TypeError, "can't convert #{opts.class} into Hash"
+ _unsafe_load_default_options
+ end
+ else
+ _unsafe_load_default_options.merge(options)
+ end
+
+ unless source.is_a?(String)
+ if source.respond_to? :to_str
+ source = source.to_str
+ elsif source.respond_to? :to_io
+ source = source.to_io.read
+ elsif source.respond_to?(:read)
+ source = source.read
+ end
+ end
+
+ if opts[:allow_blank] && (source.nil? || source.empty?)
+ source = 'null'
+ end
+
+ if proc
+ opts = opts.dup
+ opts[:on_load] = proc.to_proc
+ end
+
+ parse(source, opts)
+ end
+
+ # :call-seq:
+ # JSON.load(source, options = {}) -> object
+ # JSON.load(source, proc = nil, options = {}) -> object
+ #
+ # Returns the Ruby objects created by parsing the given +source+.
+ #
+ # BEWARE: This method is meant to serialise data from trusted user input,
+ # like from your own database server or clients under your control, it could
+ # be dangerous to allow untrusted users to pass JSON sources into it.
+ # If you must use it, use JSON.unsafe_load instead to make it clear.
+ #
+ # Since JSON version 2.8.0, `load` emits a deprecation warning when a
+ # non native type is deserialized, without `create_additions` being explicitly
+ # enabled, and in JSON version 3.0, `load` will have `create_additions` disabled
+ # by default.
+ #
+ # - Argument +source+ must be, or be convertible to, a \String:
+ # - If +source+ responds to instance method +to_str+,
+ # <tt>source.to_str</tt> becomes the source.
+ # - If +source+ responds to instance method +to_io+,
+ # <tt>source.to_io.read</tt> becomes the source.
+ # - If +source+ responds to instance method +read+,
+ # <tt>source.read</tt> becomes the source.
+ # - If both of the following are true, source becomes the \String <tt>'null'</tt>:
+ # - Option +allow_blank+ specifies a truthy value.
+ # - The source, as defined above, is +nil+ or the empty \String <tt>''</tt>.
+ # - Otherwise, +source+ remains the source.
+ # - Argument +proc+, if given, must be a \Proc that accepts one argument.
+ # It will be called recursively with each result (depth-first order).
+ # See details below.
+ # - Argument +opts+, if given, contains a \Hash of options for the parsing.
+ # See {Parsing Options}[#module-JSON-label-Parsing+Options].
+ # The default options can be changed via method JSON.load_default_options=.
+ #
+ # ---
+ #
+ # When no +proc+ is given, modifies +source+ as above and returns the result of
+ # <tt>parse(source, opts)</tt>; see #parse.
+ #
+ # Source for following examples:
+ # source = <<~JSON
+ # {
+ # "name": "Dave",
+ # "age" :40,
+ # "hats": [
+ # "Cattleman's",
+ # "Panama",
+ # "Tophat"
+ # ]
+ # }
+ # JSON
+ #
+ # Load a \String:
+ # ruby = JSON.load(source)
+ # ruby # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # Load an \IO object:
+ # require 'stringio'
+ # object = JSON.load(StringIO.new(source))
+ # object # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # Load a \File object:
+ # path = 't.json'
+ # File.write(path, source)
+ # File.open(path) do |file|
+ # JSON.load(file)
+ # end # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # ---
+ #
+ # When +proc+ is given:
+ # - Modifies +source+ as above.
+ # - Gets the +result+ from calling <tt>parse(source, opts)</tt>.
+ # - Recursively calls <tt>proc(result)</tt>.
+ # - Returns the final result.
+ #
+ # Example:
+ # require 'json'
+ #
+ # # Some classes for the example.
+ # class Base
+ # def initialize(attributes)
+ # @attributes = attributes
+ # end
+ # end
+ # class User < Base; end
+ # class Account < Base; end
+ # class Admin < Base; end
+ # # The JSON source.
+ # json = <<-EOF
+ # {
+ # "users": [
+ # {"type": "User", "username": "jane", "email": "jane@example.com"},
+ # {"type": "User", "username": "john", "email": "john@example.com"}
+ # ],
+ # "accounts": [
+ # {"account": {"type": "Account", "paid": true, "account_id": "1234"}},
+ # {"account": {"type": "Account", "paid": false, "account_id": "1235"}}
+ # ],
+ # "admins": {"type": "Admin", "password": "0wn3d"}
+ # }
+ # EOF
+ # # Deserializer method.
+ # def deserialize_obj(obj, safe_types = %w(User Account Admin))
+ # type = obj.is_a?(Hash) && obj["type"]
+ # safe_types.include?(type) ? Object.const_get(type).new(obj) : obj
+ # end
+ # # Call to JSON.load
+ # ruby = JSON.load(json, proc {|obj|
+ # case obj
+ # when Hash
+ # obj.each {|k, v| obj[k] = deserialize_obj v }
+ # when Array
+ # obj.map! {|v| deserialize_obj v }
+ # end
+ # obj
+ # })
+ # pp ruby
+ # Output:
+ # {"users"=>
+ # [#<User:0x00000000064c4c98
+ # @attributes=
+ # {"type"=>"User", "username"=>"jane", "email"=>"jane@example.com"}>,
+ # #<User:0x00000000064c4bd0
+ # @attributes=
+ # {"type"=>"User", "username"=>"john", "email"=>"john@example.com"}>],
+ # "accounts"=>
+ # [{"account"=>
+ # #<Account:0x00000000064c4928
+ # @attributes={"type"=>"Account", "paid"=>true, "account_id"=>"1234"}>},
+ # {"account"=>
+ # #<Account:0x00000000064c4680
+ # @attributes={"type"=>"Account", "paid"=>false, "account_id"=>"1235"}>}],
+ # "admins"=>
+ # #<Admin:0x00000000064c41f8
+ # @attributes={"type"=>"Admin", "password"=>"0wn3d"}>}
+ #
+ def load(source, proc = nil, options = nil)
+ if proc && options.nil? && proc.is_a?(Hash)
+ options = proc
+ proc = nil
+ end
+
+ opts = if options.nil?
+ if proc && proc.is_a?(Hash)
+ options, proc = proc, nil
+ options
+ else
+ _load_default_options
+ end
+ else
+ _load_default_options.merge(options)
+ end
+
+ unless source.is_a?(String)
+ if source.respond_to? :to_str
+ source = source.to_str
+ elsif source.respond_to? :to_io
+ source = source.to_io.read
+ elsif source.respond_to?(:read)
+ source = source.read
+ end
+ end
+
+ if opts[:allow_blank] && (source.nil? || (String === source && source.empty?))
+ source = 'null'
+ end
+
+ if proc
+ opts = opts.dup
+ opts[:on_load] = proc.to_proc
+ end
+
+ parse(source, opts)
+ end
+
+ # Sets or returns the default options for the JSON.dump method.
+ # Initially:
+ # opts = JSON.dump_default_options
+ # opts # => {:max_nesting=>false, :allow_nan=>true}
+ deprecated_singleton_attr_accessor :dump_default_options
+ @dump_default_options = {
+ :max_nesting => false,
+ :allow_nan => true,
+ }
+
+ # :call-seq:
+ # JSON.dump(obj, io = nil, limit = nil)
+ #
+ # Dumps +obj+ as a \JSON string, i.e. calls generate on the object and returns the result.
+ #
+ # The default options can be changed via method JSON.dump_default_options.
+ #
+ # - Argument +io+, if given, should respond to method +write+;
+ # the \JSON \String is written to +io+, and +io+ is returned.
+ # If +io+ is not given, the \JSON \String is returned.
+ # - Argument +limit+, if given, is passed to JSON.generate as option +max_nesting+.
+ #
+ # ---
+ #
+ # When argument +io+ is not given, returns the \JSON \String generated from +obj+:
+ # obj = {foo: [0, 1], bar: {baz: 2, bat: 3}, bam: :bad}
+ # json = JSON.dump(obj)
+ # json # => "{\"foo\":[0,1],\"bar\":{\"baz\":2,\"bat\":3},\"bam\":\"bad\"}"
+ #
+ # When argument +io+ is given, writes the \JSON \String to +io+ and returns +io+:
+ # path = 't.json'
+ # File.open(path, 'w') do |file|
+ # JSON.dump(obj, file)
+ # end # => #<File:t.json (closed)>
+ # puts File.read(path)
+ # Output:
+ # {"foo":[0,1],"bar":{"baz":2,"bat":3},"bam":"bad"}
+ def dump(obj, anIO = nil, limit = nil, kwargs = nil)
+ if kwargs.nil?
+ if limit.nil?
+ if anIO.is_a?(Hash)
+ kwargs = anIO
+ anIO = nil
+ end
+ elsif limit.is_a?(Hash)
+ kwargs = limit
+ limit = nil
end
- state.configure(opts)
end
- obj.to_json(state)
+
+ unless anIO.nil?
+ if anIO.respond_to?(:to_io)
+ anIO = anIO.to_io
+ elsif limit.nil? && !anIO.respond_to?(:write)
+ anIO, limit = nil, anIO
+ end
+ end
+
+ opts = JSON._dump_default_options
+ opts = opts.merge(:max_nesting => limit) if limit
+ opts = opts.merge(kwargs) if kwargs
+
+ begin
+ State.generate(obj, opts, anIO)
+ rescue JSON::NestingError
+ raise ArgumentError, "exceed depth limit"
+ end
end
# :stopdoc:
- # I want to deprecate these later, so I'll first be silent about them, and later delete them.
- alias pretty_unparse pretty_generate
- module_function :pretty_unparse
- # :startdoc:
+ # All these were meant to be deprecated circa 2009, but were just set as undocumented
+ # so usage still exist in the wild.
+ def unparse(...)
+ if RUBY_VERSION >= "3.0"
+ warn "JSON.unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1, category: :deprecated
+ else
+ warn "JSON.unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1
+ end
+ generate(...)
+ end
+ module_function :unparse
- # Load a ruby data structure from a JSON _source_ and return it. A source can
- # either be a string-like object, an IO like object, or an object responding
- # to the read method. If _proc_ was given, it will be called with any nested
- # Ruby object as an argument recursively in depth first order.
- #
- # This method is part of the implementation of the load/dump interface of
- # Marshal and YAML.
- def load(source, proc = nil)
- if source.respond_to? :to_str
- source = source.to_str
- elsif source.respond_to? :to_io
- source = source.to_io.read
+ def fast_unparse(...)
+ if RUBY_VERSION >= "3.0"
+ warn "JSON.fast_unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1, category: :deprecated
else
- source = source.read
+ warn "JSON.fast_unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1
end
- result = parse(source, :max_nesting => false, :allow_nan => true)
- recurse_proc(result, &proc) if proc
- result
+ generate(...)
end
+ module_function :fast_unparse
- def recurse_proc(result, &proc)
- case result
- when Array
- result.each { |x| recurse_proc x, &proc }
- proc.call result
- when Hash
- result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc }
- proc.call result
+ def pretty_unparse(...)
+ if RUBY_VERSION >= "3.0"
+ warn "JSON.pretty_unparse is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1, category: :deprecated
else
- proc.call result
+ warn "JSON.pretty_unparse is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1
end
+ pretty_generate(...)
end
+ module_function :fast_unparse
- alias restore load
+ def restore(...)
+ if RUBY_VERSION >= "3.0"
+ warn "JSON.restore is deprecated and will be removed in json 3.0.0, just use JSON.load", uplevel: 1, category: :deprecated
+ else
+ warn "JSON.restore is deprecated and will be removed in json 3.0.0, just use JSON.load", uplevel: 1
+ end
+ load(...)
+ end
module_function :restore
- # Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns
- # the result.
+ class << self
+ private
+
+ def const_missing(const_name)
+ case const_name
+ when :PRETTY_STATE_PROTOTYPE
+ if RUBY_VERSION >= "3.0"
+ warn "JSON::PRETTY_STATE_PROTOTYPE is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1, category: :deprecated
+ else
+ warn "JSON::PRETTY_STATE_PROTOTYPE is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1
+ end
+ state.new(PRETTY_GENERATE_OPTIONS)
+ else
+ super
+ end
+ end
+ end
+ # :startdoc:
+
+ # JSON::Coder holds a parser and generator configuration.
#
- # If anIO (an IO like object or an object that responds to the write method)
- # was given, the resulting JSON is written to it.
+ # module MyApp
+ # JSONC_CODER = JSON::Coder.new(
+ # allow_trailing_comma: true
+ # )
+ # end
#
- # If the number of nested arrays or objects exceeds _limit_ an ArgumentError
- # exception is raised. This argument is similar (but not exactly the
- # same!) to the _limit_ argument in Marshal.dump.
+ # MyApp::JSONC_CODER.load(document)
#
- # This method is part of the implementation of the load/dump interface of
- # Marshal and YAML.
- def dump(obj, anIO = nil, limit = nil)
- if anIO and limit.nil?
- anIO = anIO.to_io if anIO.respond_to?(:to_io)
- unless anIO.respond_to?(:write)
- limit = anIO
- anIO = nil
+ class Coder
+ # :call-seq:
+ # JSON.new(options = nil, &block)
+ #
+ # Argument +options+, if given, contains a \Hash of options for both parsing and generating.
+ # See {Parsing Options}[rdoc-ref:JSON@Parsing+Options],
+ # and {Generating Options}[rdoc-ref:JSON@Generating+Options].
+ #
+ # For generation, the <tt>strict: true</tt> option is always set. When a Ruby object with no native \JSON counterpart is
+ # encountered, the block provided to the initialize method is invoked, and must return a Ruby object that has a native
+ # \JSON counterpart:
+ #
+ # module MyApp
+ # API_JSON_CODER = JSON::Coder.new do |object|
+ # case object
+ # when Time
+ # object.iso8601(3)
+ # else
+ # object # Unknown type, will raise
+ # end
+ # end
+ # end
+ #
+ # puts MyApp::API_JSON_CODER.dump(Time.now.utc) # => "2025-01-21T08:41:44.286Z"
+ #
+ def initialize(options = nil, &as_json)
+ if options.nil?
+ options = { strict: true }
+ else
+ options = options.dup
+ options[:strict] = true
end
+ options[:as_json] = as_json if as_json
+
+ @state = State.new(options).freeze
+ @parser_config = Ext::Parser::Config.new(ParserOptions.prepare(options)).freeze
end
- limit ||= 0
- result = generate(obj, :allow_nan => true, :max_nesting => limit)
- if anIO
- anIO.write result
- anIO
- else
- result
+
+ # call-seq:
+ # dump(object) -> String
+ # dump(object, io) -> io
+ #
+ # Serialize the given object into a \JSON document.
+ def dump(object, io = nil)
+ @state.generate(object, io)
+ end
+ alias_method :generate, :dump
+
+ # call-seq:
+ # load(string) -> Object
+ #
+ # Parse the given \JSON document and return an equivalent Ruby object.
+ def load(source)
+ @parser_config.parse(source)
+ end
+ alias_method :parse, :load
+
+ # call-seq:
+ # load(path) -> Object
+ #
+ # Parse the given \JSON document and return an equivalent Ruby object.
+ def load_file(path)
+ load(File.read(path, encoding: Encoding::UTF_8))
+ end
+ end
+
+ module GeneratorMethods
+ # call-seq: to_json(*)
+ #
+ # Converts this object into a JSON string.
+ # If this object doesn't directly maps to a JSON native type,
+ # first convert it to a string (calling #to_s), then 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.
+ def to_json(state = nil, *)
+ obj = case self
+ when nil, false, true, Integer, Float, Array, Hash
+ self
+ else
+ "#{self}"
+ end
+
+ if state.nil?
+ JSON::State._generate_no_fallback(obj, nil, nil)
+ else
+ JSON::State.from_state(state)._generate_no_fallback(obj)
+ end
end
- rescue JSON::NestingError
- raise ArgumentError, "exceed depth limit"
end
end
@@ -313,42 +1130,44 @@ module ::Kernel
# Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in
# one line.
def j(*objs)
+ if RUBY_VERSION >= "3.0"
+ warn "Kernel#j is deprecated and will be removed in json 3.0.0", uplevel: 1, category: :deprecated
+ else
+ warn "Kernel#j is deprecated and will be removed in json 3.0.0", uplevel: 1
+ end
+
objs.each do |obj|
- puts JSON::generate(obj, :allow_nan => true, :max_nesting => false)
+ puts JSON.generate(obj, :allow_nan => true, :max_nesting => false)
end
nil
end
- # Ouputs _objs_ to STDOUT as JSON strings in a pretty format, with
+ # Outputs _objs_ to STDOUT as JSON strings in a pretty format, with
# indentation and over many lines.
def jj(*objs)
+ if RUBY_VERSION >= "3.0"
+ warn "Kernel#jj is deprecated and will be removed in json 3.0.0", uplevel: 1, category: :deprecated
+ else
+ warn "Kernel#jj is deprecated and will be removed in json 3.0.0", uplevel: 1
+ end
+
objs.each do |obj|
- puts JSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false)
+ puts JSON.pretty_generate(obj, :allow_nan => true, :max_nesting => false)
end
nil
end
- # If _object_ is string-like parse the string and return the parsed result as
- # a Ruby data structure. Otherwise generate a JSON text from the Ruby data
+ # If _object_ is string-like, parse the string and return the parsed result as
+ # a Ruby data structure. Otherwise, generate a JSON text from the Ruby data
# structure object and return it.
#
- # The _opts_ argument is passed through to generate/parse respectively, see
+ # The _opts_ argument is passed through to generate/parse respectively. See
# generate and parse for their documentation.
- def JSON(object, opts = {})
- if object.respond_to? :to_str
- JSON.parse(object.to_str, opts)
- else
- JSON.generate(object, opts)
- end
+ def JSON(object, opts = nil)
+ JSON[object, opts]
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
+class Object
+ include JSON::GeneratorMethods
end
diff --git a/ext/json/lib/json/editor.rb b/ext/json/lib/json/editor.rb
deleted file mode 100755
index 9e05f44b5b..0000000000
--- a/ext/json/lib/json/editor.rb
+++ /dev/null
@@ -1,1371 +0,0 @@
-# To use the GUI JSON editor, start the edit_json.rb executable script. It
-# requires ruby-gtk to be installed.
-
-require 'gtk2'
-require 'iconv'
-require 'json'
-require 'rbconfig'
-require 'open-uri'
-
-module JSON
- module Editor
- include Gtk
-
- # Beginning of the editor window title
- TITLE = 'JSON Editor'.freeze
-
- # Columns constants
- ICON_COL, TYPE_COL, CONTENT_COL = 0, 1, 2
-
- # JSON primitive types (Containers)
- CONTAINER_TYPES = %w[Array Hash].sort
- # All JSON primitive types
- ALL_TYPES = (%w[TrueClass FalseClass Numeric String NilClass] +
- CONTAINER_TYPES).sort
-
- # The Nodes necessary for the tree representation of a JSON document
- ALL_NODES = (ALL_TYPES + %w[Key]).sort
-
- DEFAULT_DIALOG_KEY_PRESS_HANDLER = lambda do |dialog, event|
- case event.keyval
- when Gdk::Keyval::GDK_Return
- dialog.response Dialog::RESPONSE_ACCEPT
- when Gdk::Keyval::GDK_Escape
- dialog.response Dialog::RESPONSE_REJECT
- end
- end
-
- # Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
- def Editor.fetch_icon(name)
- @icon_cache ||= {}
- unless @icon_cache.key?(name)
- path = File.dirname(__FILE__)
- @icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
- end
- @icon_cache[name]
- end
-
- # Opens an error dialog on top of _window_ showing the error message
- # _text_.
- def Editor.error_dialog(window, text)
- dialog = MessageDialog.new(window, Dialog::MODAL,
- MessageDialog::ERROR,
- MessageDialog::BUTTONS_CLOSE, text)
- dialog.show_all
- dialog.run
- rescue TypeError
- dialog = MessageDialog.new(Editor.window, Dialog::MODAL,
- MessageDialog::ERROR,
- MessageDialog::BUTTONS_CLOSE, text)
- dialog.show_all
- dialog.run
- ensure
- dialog.destroy if dialog
- end
-
- # Opens a yes/no question dialog on top of _window_ showing the error
- # message _text_. If yes was answered _true_ is returned, otherwise
- # _false_.
- def Editor.question_dialog(window, text)
- dialog = MessageDialog.new(window, Dialog::MODAL,
- MessageDialog::QUESTION,
- MessageDialog::BUTTONS_YES_NO, text)
- dialog.show_all
- dialog.run do |response|
- return Gtk::Dialog::RESPONSE_YES === response
- end
- ensure
- dialog.destroy if dialog
- end
-
- # Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby
- # data structure and return it.
- def Editor.model2data(iter)
- return nil if iter.nil?
- case iter.type
- when 'Hash'
- hash = {}
- iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
- hash
- when 'Array'
- array = Array.new(iter.n_children)
- iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
- array
- when 'Key'
- iter.content
- when 'String'
- iter.content
- when 'Numeric'
- content = iter.content
- if /\./.match(content)
- content.to_f
- else
- content.to_i
- end
- when 'TrueClass'
- true
- when 'FalseClass'
- false
- when 'NilClass'
- nil
- else
- fail "Unknown type found in model: #{iter.type}"
- end
- end
-
- # Convert the Ruby data structure _data_ into tree model data for Gtk and
- # returns the whole model. If the parameter _model_ wasn't given a new
- # Gtk::TreeStore is created as the model. The _parent_ parameter specifies
- # the parent node (iter, Gtk:TreeIter instance) to which the data is
- # appended, alternativeley the result of the yielded block is used as iter.
- def Editor.data2model(data, model = nil, parent = nil)
- model ||= TreeStore.new(Gdk::Pixbuf, String, String)
- iter = if block_given?
- yield model
- else
- model.append(parent)
- end
- case data
- when Hash
- iter.type = 'Hash'
- data.sort.each do |key, value|
- pair_iter = model.append(iter)
- pair_iter.type = 'Key'
- pair_iter.content = key.to_s
- Editor.data2model(value, model, pair_iter)
- end
- when Array
- iter.type = 'Array'
- data.each do |value|
- Editor.data2model(value, model, iter)
- end
- when Numeric
- iter.type = 'Numeric'
- iter.content = data.to_s
- when String, true, false, nil
- iter.type = data.class.name
- iter.content = data.nil? ? 'null' : data.to_s
- else
- iter.type = 'String'
- iter.content = data.to_s
- end
- model
- end
-
- # The Gtk::TreeIter class is reopened and some auxiliary methods are added.
- class Gtk::TreeIter
- include Enumerable
-
- # Traverse each of this Gtk::TreeIter instance's children
- # and yield to them.
- def each
- n_children.times { |i| yield nth_child(i) }
- end
-
- # Recursively traverse all nodes of this Gtk::TreeIter's subtree
- # (including self) and yield to them.
- def recursive_each(&block)
- yield self
- each do |i|
- i.recursive_each(&block)
- end
- end
-
- # Remove the subtree of this Gtk::TreeIter instance from the
- # model _model_.
- def remove_subtree(model)
- while current = first_child
- model.remove(current)
- end
- end
-
- # Returns the type of this node.
- def type
- self[TYPE_COL]
- end
-
- # Sets the type of this node to _value_. This implies setting
- # the respective icon accordingly.
- def type=(value)
- self[TYPE_COL] = value
- self[ICON_COL] = Editor.fetch_icon(value)
- end
-
- # Returns the content of this node.
- def content
- self[CONTENT_COL]
- end
-
- # Sets the content of this node to _value_.
- def content=(value)
- self[CONTENT_COL] = value
- end
- end
-
- # This module bundles some method, that can be used to create a menu. It
- # should be included into the class in question.
- module MenuExtension
- include Gtk
-
- # Creates a Menu, that includes MenuExtension. _treeview_ is the
- # Gtk::TreeView, on which it operates.
- def initialize(treeview)
- @treeview = treeview
- @menu = Menu.new
- end
-
- # Returns the Gtk::TreeView of this menu.
- attr_reader :treeview
-
- # Returns the menu.
- attr_reader :menu
-
- # Adds a Gtk::SeparatorMenuItem to this instance's #menu.
- def add_separator
- menu.append SeparatorMenuItem.new
- end
-
- # Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label
- # string, _klass_ is the item type, and _callback_ is the procedure, that
- # is called if the _item_ is activated.
- def add_item(label, keyval = nil, klass = MenuItem, &callback)
- label = "#{label} (C-#{keyval.chr})" if keyval
- item = klass.new(label)
- item.signal_connect(:activate, &callback)
- if keyval
- self.signal_connect(:'key-press-event') do |item, event|
- if event.state & Gdk::Window::ModifierType::CONTROL_MASK != 0 and
- event.keyval == keyval
- callback.call item
- end
- end
- end
- menu.append item
- item
- end
-
- # This method should be implemented in subclasses to create the #menu of
- # this instance. It has to be called after an instance of this class is
- # created, to build the menu.
- def create
- raise NotImplementedError
- end
-
- def method_missing(*a, &b)
- treeview.__send__(*a, &b)
- end
- end
-
- # This class creates the popup menu, that opens when clicking onto the
- # treeview.
- class PopUpMenu
- include MenuExtension
-
- # Change the type or content of the selected node.
- def change_node(item)
- if current = selection.selected
- parent = current.parent
- old_type, old_content = current.type, current.content
- if ALL_TYPES.include?(old_type)
- @clipboard_data = Editor.model2data(current)
- type, content = ask_for_element(parent, current.type,
- current.content)
- if type
- current.type, current.content = type, content
- current.remove_subtree(model)
- toplevel.display_status("Changed a node in tree.")
- window.change
- end
- else
- toplevel.display_status(
- "Cannot change node of type #{old_type} in tree!")
- end
- end
- end
-
- # Cut the selected node and its subtree, and save it into the
- # clipboard.
- def cut_node(item)
- if current = selection.selected
- if current and current.type == 'Key'
- @clipboard_data = {
- current.content => Editor.model2data(current.first_child)
- }
- else
- @clipboard_data = Editor.model2data(current)
- end
- model.remove(current)
- window.change
- toplevel.display_status("Cut a node from tree.")
- end
- end
-
- # Copy the selected node and its subtree, and save it into the
- # clipboard.
- def copy_node(item)
- if current = selection.selected
- if current and current.type == 'Key'
- @clipboard_data = {
- current.content => Editor.model2data(current.first_child)
- }
- else
- @clipboard_data = Editor.model2data(current)
- end
- window.change
- toplevel.display_status("Copied a node from tree.")
- end
- end
-
- # Paste the data in the clipboard into the selected Array or Hash by
- # appending it.
- def paste_node_appending(item)
- if current = selection.selected
- if @clipboard_data
- case current.type
- when 'Array'
- Editor.data2model(@clipboard_data, model, current)
- expand_collapse(current)
- when 'Hash'
- if @clipboard_data.is_a? Hash
- parent = current.parent
- hash = Editor.model2data(current)
- model.remove(current)
- hash.update(@clipboard_data)
- Editor.data2model(hash, model, parent)
- if parent
- expand_collapse(parent)
- elsif @expanded
- expand_all
- end
- window.change
- else
- toplevel.display_status(
- "Cannot paste non-#{current.type} data into '#{current.type}'!")
- end
- else
- toplevel.display_status(
- "Cannot paste node below '#{current.type}'!")
- end
- else
- toplevel.display_status("Nothing to paste in clipboard!")
- end
- else
- toplevel.display_status("Append a node into the root first!")
- end
- end
-
- # Paste the data in the clipboard into the selected Array inserting it
- # before the selected element.
- def paste_node_inserting_before(item)
- if current = selection.selected
- if @clipboard_data
- parent = current.parent or return
- parent_type = parent.type
- if parent_type == 'Array'
- selected_index = parent.each_with_index do |c, i|
- break i if c == current
- end
- Editor.data2model(@clipboard_data, model, parent) do |m|
- m.insert_before(parent, current)
- end
- expand_collapse(current)
- toplevel.display_status("Inserted an element to " +
- "'#{parent_type}' before index #{selected_index}.")
- window.change
- else
- toplevel.display_status(
- "Cannot insert node below '#{parent_type}'!")
- end
- else
- toplevel.display_status("Nothing to paste in clipboard!")
- end
- else
- toplevel.display_status("Append a node into the root first!")
- end
- end
-
- # Append a new node to the selected Hash or Array.
- def append_new_node(item)
- if parent = selection.selected
- parent_type = parent.type
- case parent_type
- when 'Hash'
- key, type, content = ask_for_hash_pair(parent)
- key or return
- iter = create_node(parent, 'Key', key)
- iter = create_node(iter, type, content)
- toplevel.display_status(
- "Added a (key, value)-pair to '#{parent_type}'.")
- window.change
- when 'Array'
- type, content = ask_for_element(parent)
- type or return
- iter = create_node(parent, type, content)
- window.change
- toplevel.display_status("Appendend an element to '#{parent_type}'.")
- else
- toplevel.display_status("Cannot append to '#{parent_type}'!")
- end
- else
- type, content = ask_for_element
- type or return
- iter = create_node(nil, type, content)
- window.change
- end
- end
-
- # Insert a new node into an Array before the selected element.
- def insert_new_node(item)
- if current = selection.selected
- parent = current.parent or return
- parent_parent = parent.parent
- parent_type = parent.type
- if parent_type == 'Array'
- selected_index = parent.each_with_index do |c, i|
- break i if c == current
- end
- type, content = ask_for_element(parent)
- type or return
- iter = model.insert_before(parent, current)
- iter.type, iter.content = type, content
- toplevel.display_status("Inserted an element to " +
- "'#{parent_type}' before index #{selected_index}.")
- window.change
- else
- toplevel.display_status(
- "Cannot insert node below '#{parent_type}'!")
- end
- else
- toplevel.display_status("Append a node into the root first!")
- end
- end
-
- # Recursively collapse/expand a subtree starting from the selected node.
- def collapse_expand(item)
- if current = selection.selected
- if row_expanded?(current.path)
- collapse_row(current.path)
- else
- expand_row(current.path, true)
- end
- else
- toplevel.display_status("Append a node into the root first!")
- end
- end
-
- # Create the menu.
- def create
- add_item("Change node", ?n, &method(:change_node))
- add_separator
- add_item("Cut node", ?X, &method(:cut_node))
- add_item("Copy node", ?C, &method(:copy_node))
- add_item("Paste node (appending)", ?A, &method(:paste_node_appending))
- add_item("Paste node (inserting before)", ?I,
- &method(:paste_node_inserting_before))
- add_separator
- add_item("Append new node", ?a, &method(:append_new_node))
- add_item("Insert new node before", ?i, &method(:insert_new_node))
- add_separator
- add_item("Collapse/Expand node (recursively)", ?e,
- &method(:collapse_expand))
-
- menu.show_all
- signal_connect(:button_press_event) do |widget, event|
- if event.kind_of? Gdk::EventButton and event.button == 3
- menu.popup(nil, nil, event.button, event.time)
- end
- end
- signal_connect(:popup_menu) do
- menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
- end
- end
- end
-
- # This class creates the File pulldown menu.
- class FileMenu
- include MenuExtension
-
- # Clear the model and filename, but ask to save the JSON document, if
- # unsaved changes have occured.
- def new(item)
- window.clear
- end
-
- # Open a file and load it into the editor. Ask to save the JSON document
- # first, if unsaved changes have occured.
- def open(item)
- window.file_open
- end
-
- def open_location(item)
- window.location_open
- end
-
- # Revert the current JSON document in the editor to the saved version.
- def revert(item)
- window.instance_eval do
- @filename and file_open(@filename)
- end
- end
-
- # Save the current JSON document.
- def save(item)
- window.file_save
- end
-
- # Save the current JSON document under the given filename.
- def save_as(item)
- window.file_save_as
- end
-
- # Quit the editor, after asking to save any unsaved changes first.
- def quit(item)
- window.quit
- end
-
- # Create the menu.
- def create
- title = MenuItem.new('File')
- title.submenu = menu
- add_item('New', &method(:new))
- add_item('Open', ?o, &method(:open))
- add_item('Open location', ?l, &method(:open_location))
- add_item('Revert', &method(:revert))
- add_separator
- add_item('Save', ?s, &method(:save))
- add_item('Save As', ?S, &method(:save_as))
- add_separator
- add_item('Quit', ?q, &method(:quit))
- title
- end
- end
-
- # This class creates the Edit pulldown menu.
- class EditMenu
- include MenuExtension
-
- # Copy data from model into primary clipboard.
- def copy(item)
- data = Editor.model2data(model.iter_first)
- json = JSON.pretty_generate(data, :max_nesting => false)
- c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
- c.text = json
- end
-
- # Copy json text from primary clipboard into model.
- def paste(item)
- c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
- if json = c.wait_for_text
- window.ask_save if @changed
- begin
- window.edit json
- rescue JSON::ParserError
- window.clear
- end
- end
- end
-
- # Find a string in all nodes' contents and select the found node in the
- # treeview.
- def find(item)
- @search = ask_for_find_term(@search) or return
- iter = model.get_iter('0') or return
- iter.recursive_each do |i|
- if @iter
- if @iter != i
- next
- else
- @iter = nil
- next
- end
- elsif @search.match(i[CONTENT_COL])
- set_cursor(i.path, nil, false)
- @iter = i
- break
- end
- end
- end
-
- # Repeat the last search given by #find.
- def find_again(item)
- @search or return
- iter = model.get_iter('0')
- iter.recursive_each do |i|
- if @iter
- if @iter != i
- next
- else
- @iter = nil
- next
- end
- elsif @search.match(i[CONTENT_COL])
- set_cursor(i.path, nil, false)
- @iter = i
- break
- end
- end
- end
-
- # Sort (Reverse sort) all elements of the selected array by the given
- # expression. _x_ is the element in question.
- def sort(item)
- if current = selection.selected
- if current.type == 'Array'
- parent = current.parent
- ary = Editor.model2data(current)
- order, reverse = ask_for_order
- order or return
- begin
- block = eval "lambda { |x| #{order} }"
- if reverse
- ary.sort! { |a,b| block[b] <=> block[a] }
- else
- ary.sort! { |a,b| block[a] <=> block[b] }
- end
- rescue => e
- Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
- else
- Editor.data2model(ary, model, parent) do |m|
- m.insert_before(parent, current)
- end
- model.remove(current)
- expand_collapse(parent)
- window.change
- toplevel.display_status("Array has been sorted.")
- end
- else
- toplevel.display_status("Only Array nodes can be sorted!")
- end
- else
- toplevel.display_status("Select an Array to sort first!")
- end
- end
-
- # Create the menu.
- def create
- title = MenuItem.new('Edit')
- title.submenu = menu
- add_item('Copy', ?c, &method(:copy))
- add_item('Paste', ?v, &method(:paste))
- add_separator
- add_item('Find', ?f, &method(:find))
- add_item('Find Again', ?g, &method(:find_again))
- add_separator
- add_item('Sort', ?S, &method(:sort))
- title
- end
- end
-
- class OptionsMenu
- include MenuExtension
-
- # Collapse/Expand all nodes by default.
- def collapsed_nodes(item)
- if expanded
- self.expanded = false
- collapse_all
- else
- self.expanded = true
- expand_all
- end
- end
-
- # Toggle pretty saving mode on/off.
- def pretty_saving(item)
- @pretty_item.toggled
- window.change
- end
-
- attr_reader :pretty_item
-
- # Create the menu.
- def create
- title = MenuItem.new('Options')
- title.submenu = menu
- add_item('Collapsed nodes', nil, CheckMenuItem, &method(:collapsed_nodes))
- @pretty_item = add_item('Pretty saving', nil, CheckMenuItem,
- &method(:pretty_saving))
- @pretty_item.active = true
- window.unchange
- title
- end
- end
-
- # This class inherits from Gtk::TreeView, to configure it and to add a lot
- # of behaviour to it.
- class JSONTreeView < Gtk::TreeView
- include Gtk
-
- # Creates a JSONTreeView instance, the parameter _window_ is
- # a MainWindow instance and used for self delegation.
- def initialize(window)
- @window = window
- super(TreeStore.new(Gdk::Pixbuf, String, String))
- self.selection.mode = SELECTION_BROWSE
-
- @expanded = false
- self.headers_visible = false
- add_columns
- add_popup_menu
- end
-
- # Returns the MainWindow instance of this JSONTreeView.
- attr_reader :window
-
- # Returns true, if nodes are autoexpanding, false otherwise.
- attr_accessor :expanded
-
- private
-
- def add_columns
- cell = CellRendererPixbuf.new
- column = TreeViewColumn.new('Icon', cell,
- 'pixbuf' => ICON_COL
- )
- append_column(column)
-
- cell = CellRendererText.new
- column = TreeViewColumn.new('Type', cell,
- 'text' => TYPE_COL
- )
- append_column(column)
-
- cell = CellRendererText.new
- cell.editable = true
- column = TreeViewColumn.new('Content', cell,
- 'text' => CONTENT_COL
- )
- cell.signal_connect(:edited, &method(:cell_edited))
- append_column(column)
- end
-
- def unify_key(iter, key)
- return unless iter.type == 'Key'
- parent = iter.parent
- if parent.any? { |c| c != iter and c.content == key }
- old_key = key
- i = 0
- begin
- key = sprintf("%s.%d", old_key, i += 1)
- end while parent.any? { |c| c != iter and c.content == key }
- end
- iter.content = key
- end
-
- def cell_edited(cell, path, value)
- iter = model.get_iter(path)
- case iter.type
- when 'Key'
- unify_key(iter, value)
- toplevel.display_status('Key has been changed.')
- when 'FalseClass'
- value.downcase!
- if value == 'true'
- iter.type, iter.content = 'TrueClass', 'true'
- end
- when 'TrueClass'
- value.downcase!
- if value == 'false'
- iter.type, iter.content = 'FalseClass', 'false'
- end
- when 'Numeric'
- iter.content =
- if value == 'Infinity'
- value
- else
- (Integer(value) rescue Float(value) rescue 0).to_s
- end
- when 'String'
- iter.content = value
- when 'Hash', 'Array'
- return
- else
- fail "Unknown type found in model: #{iter.type}"
- end
- window.change
- end
-
- def configure_value(value, type)
- value.editable = false
- case type
- when 'Array', 'Hash'
- value.text = ''
- when 'TrueClass'
- value.text = 'true'
- when 'FalseClass'
- value.text = 'false'
- when 'NilClass'
- value.text = 'null'
- when 'Numeric', 'String'
- value.text ||= ''
- value.editable = true
- else
- raise ArgumentError, "unknown type '#{type}' encountered"
- end
- end
-
- def add_popup_menu
- menu = PopUpMenu.new(self)
- menu.create
- end
-
- public
-
- # Create a _type_ node with content _content_, and add it to _parent_
- # in the model. If _parent_ is nil, create a new model and put it into
- # the editor treeview.
- def create_node(parent, type, content)
- iter = if parent
- model.append(parent)
- else
- new_model = Editor.data2model(nil)
- toplevel.view_new_model(new_model)
- new_model.iter_first
- end
- iter.type, iter.content = type, content
- expand_collapse(parent) if parent
- iter
- end
-
- # Ask for a hash key, value pair to be added to the Hash node _parent_.
- def ask_for_hash_pair(parent)
- key_input = type_input = value_input = nil
-
- dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
- )
- dialog.width_request = 640
-
- hbox = HBox.new(false, 5)
- hbox.pack_start(Label.new("Key:"), false)
- hbox.pack_start(key_input = Entry.new)
- key_input.text = @key || ''
- dialog.vbox.pack_start(hbox, false)
- key_input.signal_connect(:activate) do
- if parent.any? { |c| c.content == key_input.text }
- toplevel.display_status('Key already exists in Hash!')
- key_input.text = ''
- else
- toplevel.display_status('Key has been changed.')
- end
- end
-
- hbox = HBox.new(false, 5)
- hbox.pack_start(Label.new("Type:"), false)
- hbox.pack_start(type_input = ComboBox.new(true))
- ALL_TYPES.each { |t| type_input.append_text(t) }
- type_input.active = @type || 0
- dialog.vbox.pack_start(hbox, false)
-
- type_input.signal_connect(:changed) do
- value_input.editable = false
- case ALL_TYPES[type_input.active]
- when 'Array', 'Hash'
- value_input.text = ''
- when 'TrueClass'
- value_input.text = 'true'
- when 'FalseClass'
- value_input.text = 'false'
- when 'NilClass'
- value_input.text = 'null'
- else
- value_input.text = ''
- value_input.editable = true
- end
- end
-
- hbox = HBox.new(false, 5)
- hbox.pack_start(Label.new("Value:"), false)
- hbox.pack_start(value_input = Entry.new)
- value_input.width_chars = 60
- value_input.text = @value || ''
- dialog.vbox.pack_start(hbox, false)
-
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
- dialog.show_all
- self.focus = dialog
- dialog.run do |response|
- if response == Dialog::RESPONSE_ACCEPT
- @key = key_input.text
- type = ALL_TYPES[@type = type_input.active]
- content = value_input.text
- return @key, type, content
- end
- end
- return
- ensure
- dialog.destroy
- end
-
- # Ask for an element to be appended _parent_.
- def ask_for_element(parent = nil, default_type = nil, value_text = @content)
- type_input = value_input = nil
-
- dialog = Dialog.new(
- "New element into #{parent ? parent.type : 'root'}",
- nil, nil,
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
- )
- hbox = HBox.new(false, 5)
- hbox.pack_start(Label.new("Type:"), false)
- hbox.pack_start(type_input = ComboBox.new(true))
- default_active = 0
- types = parent ? ALL_TYPES : CONTAINER_TYPES
- types.each_with_index do |t, i|
- type_input.append_text(t)
- if t == default_type
- default_active = i
- end
- end
- type_input.active = default_active
- dialog.vbox.pack_start(hbox, false)
- type_input.signal_connect(:changed) do
- configure_value(value_input, types[type_input.active])
- end
-
- hbox = HBox.new(false, 5)
- hbox.pack_start(Label.new("Value:"), false)
- hbox.pack_start(value_input = Entry.new)
- value_input.width_chars = 60
- value_input.text = value_text if value_text
- configure_value(value_input, types[type_input.active])
-
- dialog.vbox.pack_start(hbox, false)
-
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
- dialog.show_all
- self.focus = dialog
- dialog.run do |response|
- if response == Dialog::RESPONSE_ACCEPT
- type = types[type_input.active]
- @content = case type
- when 'Numeric'
- if (t = value_input.text) == 'Infinity'
- 1 / 0.0
- else
- Integer(t) rescue Float(t) rescue 0
- end
- else
- value_input.text
- end.to_s
- return type, @content
- end
- end
- return
- ensure
- dialog.destroy if dialog
- end
-
- # Ask for an order criteria for sorting, using _x_ for the element in
- # question. Returns the order criterium, and true/false for reverse
- # sorting.
- def ask_for_order
- dialog = Dialog.new(
- "Give an order criterium for 'x'.",
- nil, nil,
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
- )
- hbox = HBox.new(false, 5)
-
- hbox.pack_start(Label.new("Order:"), false)
- hbox.pack_start(order_input = Entry.new)
- order_input.text = @order || 'x'
- order_input.width_chars = 60
-
- hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'), false)
-
- dialog.vbox.pack_start(hbox, false)
-
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
- dialog.show_all
- self.focus = dialog
- dialog.run do |response|
- if response == Dialog::RESPONSE_ACCEPT
- return @order = order_input.text, reverse_checkbox.active?
- end
- end
- return
- ensure
- dialog.destroy if dialog
- end
-
- # Ask for a find term to search for in the tree. Returns the term as a
- # string.
- def ask_for_find_term(search = nil)
- dialog = Dialog.new(
- "Find a node matching regex in tree.",
- nil, nil,
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
- )
- hbox = HBox.new(false, 5)
-
- hbox.pack_start(Label.new("Regex:"), false)
- hbox.pack_start(regex_input = Entry.new)
- hbox.pack_start(icase_checkbox = CheckButton.new('Icase'), false)
- regex_input.width_chars = 60
- if search
- regex_input.text = search.source
- icase_checkbox.active = search.casefold?
- end
-
- dialog.vbox.pack_start(hbox, false)
-
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
- dialog.show_all
- self.focus = dialog
- dialog.run do |response|
- if response == Dialog::RESPONSE_ACCEPT
- begin
- return Regexp.new(regex_input.text, icase_checkbox.active? ? Regexp::IGNORECASE : 0)
- rescue => e
- Editor.error_dialog(self, "Evaluation of regex /#{regex_input.text}/ failed: #{e}!")
- return
- end
- end
- end
- return
- ensure
- dialog.destroy if dialog
- end
-
- # Expand or collapse row pointed to by _iter_ according
- # to the #expanded attribute.
- def expand_collapse(iter)
- if expanded
- expand_row(iter.path, true)
- else
- collapse_row(iter.path)
- end
- end
- end
-
- # The editor main window
- class MainWindow < Gtk::Window
- include Gtk
-
- def initialize(encoding)
- @changed = false
- @encoding = encoding
- super(TOPLEVEL)
- display_title
- set_default_size(800, 600)
- signal_connect(:delete_event) { quit }
-
- vbox = VBox.new(false, 0)
- add(vbox)
- #vbox.border_width = 0
-
- @treeview = JSONTreeView.new(self)
- @treeview.signal_connect(:'cursor-changed') do
- display_status('')
- end
-
- menu_bar = create_menu_bar
- vbox.pack_start(menu_bar, false, false, 0)
-
- sw = ScrolledWindow.new(nil, nil)
- sw.shadow_type = SHADOW_ETCHED_IN
- sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
- vbox.pack_start(sw, true, true, 0)
- sw.add(@treeview)
-
- @status_bar = Statusbar.new
- vbox.pack_start(@status_bar, false, false, 0)
-
- @filename ||= nil
- if @filename
- data = read_data(@filename)
- view_new_model Editor.data2model(data)
- end
-
- signal_connect(:button_release_event) do |_,event|
- if event.button == 2
- c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
- if url = c.wait_for_text
- location_open url
- end
- false
- else
- true
- end
- end
- end
-
- # Creates the menu bar with the pulldown menus and returns it.
- def create_menu_bar
- menu_bar = MenuBar.new
- @file_menu = FileMenu.new(@treeview)
- menu_bar.append @file_menu.create
- @edit_menu = EditMenu.new(@treeview)
- menu_bar.append @edit_menu.create
- @options_menu = OptionsMenu.new(@treeview)
- menu_bar.append @options_menu.create
- menu_bar
- end
-
- # Sets editor status to changed, to indicate that the edited data
- # containts unsaved changes.
- def change
- @changed = true
- display_title
- end
-
- # Sets editor status to unchanged, to indicate that the edited data
- # doesn't containt unsaved changes.
- def unchange
- @changed = false
- display_title
- end
-
- # Puts a new model _model_ into the Gtk::TreeView to be edited.
- def view_new_model(model)
- @treeview.model = model
- @treeview.expanded = true
- @treeview.expand_all
- unchange
- end
-
- # Displays _text_ in the status bar.
- def display_status(text)
- @cid ||= nil
- @status_bar.pop(@cid) if @cid
- @cid = @status_bar.get_context_id('dummy')
- @status_bar.push(@cid, text)
- end
-
- # Opens a dialog, asking, if changes should be saved to a file.
- def ask_save
- if Editor.question_dialog(self,
- "Unsaved changes to JSON model. Save?")
- if @filename
- file_save
- else
- file_save_as
- end
- end
- end
-
- # Quit this editor, that is, leave this editor's main loop.
- def quit
- ask_save if @changed
- if Gtk.main_level > 0
- destroy
- Gtk.main_quit
- end
- nil
- end
-
- # Display the new title according to the editor's current state.
- def display_title
- title = TITLE.dup
- title << ": #@filename" if @filename
- title << " *" if @changed
- self.title = title
- end
-
- # Clear the current model, after asking to save all unsaved changes.
- def clear
- ask_save if @changed
- @filename = nil
- self.view_new_model nil
- end
-
- def check_pretty_printed(json)
- pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
- @options_menu.pretty_item.active = pretty
- end
- private :check_pretty_printed
-
- # Open the data at the location _uri_, if given. Otherwise open a dialog
- # to ask for the _uri_.
- def location_open(uri = nil)
- uri = ask_for_location unless uri
- uri or return
- ask_save if @changed
- data = load_location(uri) or return
- view_new_model Editor.data2model(data)
- end
-
- # Open the file _filename_ or call the #select_file method to ask for a
- # filename.
- def file_open(filename = nil)
- filename = select_file('Open as a JSON file') unless filename
- data = load_file(filename) or return
- view_new_model Editor.data2model(data)
- end
-
- # Edit the string _json_ in the editor.
- def edit(json)
- if json.respond_to? :read
- json = json.read
- end
- data = parse_json json
- view_new_model Editor.data2model(data)
- end
-
- # Save the current file.
- def file_save
- if @filename
- store_file(@filename)
- else
- file_save_as
- end
- end
-
- # Save the current file as the filename
- def file_save_as
- filename = select_file('Save as a JSON file')
- store_file(filename)
- end
-
- # Store the current JSON document to _path_.
- def store_file(path)
- if path
- data = Editor.model2data(@treeview.model.iter_first)
- File.open(path + '.tmp', 'wb') do |output|
- data or break
- if @options_menu.pretty_item.active?
- output.puts JSON.pretty_generate(data, :max_nesting => false)
- else
- output.write JSON.generate(data, :max_nesting => false)
- end
- end
- File.rename path + '.tmp', path
- @filename = path
- toplevel.display_status("Saved data to '#@filename'.")
- unchange
- end
- rescue SystemCallError => e
- Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
- end
-
- # Load the file named _filename_ into the editor as a JSON document.
- def load_file(filename)
- if filename
- if File.directory?(filename)
- Editor.error_dialog(self, "Try to select a JSON file!")
- nil
- else
- @filename = filename
- if data = read_data(filename)
- toplevel.display_status("Loaded data from '#@filename'.")
- end
- display_title
- data
- end
- end
- end
-
- # Load the data at location _uri_ into the editor as a JSON document.
- def load_location(uri)
- data = read_data(uri) or return
- @filename = nil
- toplevel.display_status("Loaded data from '#{uri}'.")
- display_title
- data
- end
-
- def parse_json(json)
- check_pretty_printed(json)
- if @encoding && !/^utf8$/i.match(@encoding)
- iconverter = Iconv.new('utf8', @encoding)
- json = iconverter.iconv(json)
- end
- JSON::parse(json, :max_nesting => false, :create_additions => false)
- end
- private :parse_json
-
- # Read a JSON document from the file named _filename_, parse it into a
- # ruby data structure, and return the data.
- def read_data(filename)
- open(filename) do |f|
- json = f.read
- return parse_json(json)
- end
- rescue => e
- Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
- return
- end
-
- # Open a file selecton dialog, displaying _message_, and return the
- # selected filename or nil, if no file was selected.
- def select_file(message)
- filename = nil
- fs = FileSelection.new(message)
- fs.set_modal(true)
- @default_dir = File.join(Dir.pwd, '') unless @default_dir
- fs.set_filename(@default_dir)
- fs.set_transient_for(self)
- fs.signal_connect(:destroy) { Gtk.main_quit }
- fs.ok_button.signal_connect(:clicked) do
- filename = fs.filename
- @default_dir = File.join(File.dirname(filename), '')
- fs.destroy
- Gtk.main_quit
- end
- fs.cancel_button.signal_connect(:clicked) do
- fs.destroy
- Gtk.main_quit
- end
- fs.show_all
- Gtk.main
- filename
- end
-
- # Ask for location URI a to load data from. Returns the URI as a string.
- def ask_for_location
- dialog = Dialog.new(
- "Load data from location...",
- nil, nil,
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
- )
- hbox = HBox.new(false, 5)
-
- hbox.pack_start(Label.new("Location:"), false)
- hbox.pack_start(location_input = Entry.new)
- location_input.width_chars = 60
- location_input.text = @location || ''
-
- dialog.vbox.pack_start(hbox, false)
-
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
- dialog.show_all
- dialog.run do |response|
- if response == Dialog::RESPONSE_ACCEPT
- return @location = location_input.text
- end
- end
- return
- ensure
- dialog.destroy if dialog
- end
- end
-
- class << self
- # Starts a JSON Editor. If a block was given, it yields
- # to the JSON::Editor::MainWindow instance.
- def start(encoding = 'utf8') # :yield: window
- Gtk.init
- @window = Editor::MainWindow.new(encoding)
- @window.icon_list = [ Editor.fetch_icon('json') ]
- yield @window if block_given?
- @window.show_all
- Gtk.main
- end
-
- # Edit the string _json_ with encoding _encoding_ in the editor.
- def edit(json, encoding = 'utf8')
- start(encoding) do |window|
- window.edit json
- end
- end
-
- attr_reader :window
- end
- end
-end
diff --git a/ext/json/lib/json/ext.rb b/ext/json/lib/json/ext.rb
index 719e56025c..5bacc5e371 100644
--- a/ext/json/lib/json/ext.rb
+++ b/ext/json/lib/json/ext.rb
@@ -1,15 +1,45 @@
+# frozen_string_literal: true
+
require 'json/common'
module JSON
# This module holds all the modules/classes that implement JSON's
# functionality as C extensions.
module Ext
+ class Parser
+ class << self
+ def parse(...)
+ new(...).parse
+ end
+ alias_method :parse, :parse # Allow redefinition by extensions
+ end
+
+ def initialize(source, opts = nil)
+ @source = source
+ @config = Config.new(opts)
+ end
+
+ def source
+ @source.dup
+ end
+
+ def parse
+ @config.parse(@source)
+ end
+ end
+
require 'json/ext/parser'
- require 'json/ext/generator'
- $DEBUG and warn "Using c extension for JSON."
- JSON.parser = Parser
- JSON.generator = Generator
+ Ext::Parser::Config = Ext::ParserConfig
+ JSON.parser = Ext::Parser
+
+ if RUBY_ENGINE == 'truffleruby'
+ require 'json/truffle_ruby/generator'
+ JSON.generator = JSON::TruffleRuby::Generator
+ else
+ require 'json/ext/generator'
+ JSON.generator = Generator
+ end
end
- JSON_LOADED = true
+ JSON_LOADED = true unless defined?(JSON::JSON_LOADED)
end
diff --git a/ext/json/lib/json/ext/generator/state.rb b/ext/json/lib/json/ext/generator/state.rb
new file mode 100644
index 0000000000..e4f425af6a
--- /dev/null
+++ b/ext/json/lib/json/ext/generator/state.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+module JSON
+ module Ext
+ module Generator
+ class State
+ # call-seq: new(opts = {})
+ #
+ # Instantiates a new State object, configured by _opts_.
+ #
+ # Argument +opts+, if given, contains a \Hash of options for the generation.
+ # See {Generating Options}[rdoc-ref:JSON@Generating+Options].
+ def initialize(opts = nil)
+ if opts && !opts.empty?
+ configure(opts)
+ end
+ end
+
+ # call-seq: configure(opts)
+ #
+ # Configure this State instance with the Hash _opts_, and return
+ # itself.
+ def configure(opts)
+ unless opts.is_a?(Hash)
+ if opts.respond_to?(:to_hash)
+ opts = opts.to_hash
+ elsif opts.respond_to?(:to_h)
+ opts = opts.to_h
+ else
+ raise TypeError, "can't convert #{opts.class} into Hash"
+ end
+ end
+ _configure(opts)
+ end
+
+ alias_method :merge, :configure
+
+ # call-seq: to_h
+ #
+ # Returns the configuration instance variables as a hash, that can be
+ # passed to the configure method.
+ def to_h
+ result = {
+ indent: indent,
+ space: space,
+ space_before: space_before,
+ object_nl: object_nl,
+ array_nl: array_nl,
+ as_json: as_json,
+ allow_nan: allow_nan?,
+ ascii_only: ascii_only?,
+ max_nesting: max_nesting,
+ script_safe: script_safe?,
+ strict: strict?,
+ depth: depth,
+ buffer_initial_length: buffer_initial_length,
+ }
+
+ allow_duplicate_key = allow_duplicate_key?
+ unless allow_duplicate_key.nil?
+ result[:allow_duplicate_key] = allow_duplicate_key
+ end
+
+ instance_variables.each do |iv|
+ iv = iv.to_s[1..-1]
+ result[iv.to_sym] = self[iv]
+ end
+
+ result
+ end
+
+ alias_method :to_hash, :to_h
+
+ # call-seq: [](name)
+ #
+ # Returns the value returned by method +name+.
+ def [](name)
+ ::JSON.deprecation_warning("JSON::State#[] is deprecated and will be removed in json 3.0.0")
+
+ if respond_to?(name)
+ __send__(name)
+ else
+ instance_variable_get("@#{name}") if
+ instance_variables.include?("@#{name}".to_sym) # avoid warning
+ end
+ end
+
+ # call-seq: []=(name, value)
+ #
+ # Sets the attribute name to value.
+ def []=(name, value)
+ ::JSON.deprecation_warning("JSON::State#[]= is deprecated and will be removed in json 3.0.0")
+
+ if respond_to?(name_writer = "#{name}=")
+ __send__ name_writer, value
+ else
+ instance_variable_set "@#{name}", value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/ext/json/lib/json/generic_object.rb b/ext/json/lib/json/generic_object.rb
new file mode 100644
index 0000000000..5c8ace354b
--- /dev/null
+++ b/ext/json/lib/json/generic_object.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+begin
+ require 'ostruct'
+rescue LoadError
+ warn "JSON::GenericObject requires 'ostruct'. Please install it with `gem install ostruct`."
+end
+
+module JSON
+ class GenericObject < OpenStruct
+ class << self
+ alias [] new
+
+ def json_creatable?
+ @json_creatable
+ end
+
+ attr_writer :json_creatable
+
+ def json_create(data)
+ data = data.dup
+ data.delete JSON.create_id
+ self[data]
+ end
+
+ def from_hash(object)
+ case
+ when object.respond_to?(:to_hash)
+ result = new
+ object.to_hash.each do |key, value|
+ result[key] = from_hash(value)
+ end
+ result
+ when object.respond_to?(:to_ary)
+ object.to_ary.map { |a| from_hash(a) }
+ else
+ object
+ end
+ end
+
+ def load(source, proc = nil, opts = {})
+ result = ::JSON.load(source, proc, opts.merge(:object_class => self))
+ result.nil? ? new : result
+ end
+
+ def dump(obj, *args)
+ ::JSON.dump(obj, *args)
+ end
+ end
+ self.json_creatable = false
+
+ def to_hash
+ table
+ end
+
+ def |(other)
+ self.class[other.to_hash.merge(to_hash)]
+ end
+
+ def as_json(*)
+ { JSON.create_id => self.class.name }.merge to_hash
+ end
+
+ def to_json(*a)
+ as_json.to_json(*a)
+ end
+ end if defined?(::OpenStruct)
+end
diff --git a/ext/json/lib/json/version.rb b/ext/json/lib/json/version.rb
index 28bea76b19..30c0a71d2f 100644
--- a/ext/json/lib/json/version.rb
+++ b/ext/json/lib/json/version.rb
@@ -1,8 +1,5 @@
+# frozen_string_literal: true
+
module JSON
- # JSON version
- VERSION = '1.1.8'
- VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc:
- VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
- VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
- VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
+ VERSION = '2.19.8'
end