diff options
Diffstat (limited to 'ext/json/lib')
| -rw-r--r-- | ext/json/lib/json.rb | 140 | ||||
| -rw-r--r-- | ext/json/lib/json/add/bigdecimal.rb | 49 | ||||
| -rw-r--r-- | ext/json/lib/json/add/complex.rb | 36 | ||||
| -rw-r--r-- | ext/json/lib/json/add/core.rb | 3 | ||||
| -rw-r--r-- | ext/json/lib/json/add/date.rb | 34 | ||||
| -rw-r--r-- | ext/json/lib/json/add/date_time.rb | 35 | ||||
| -rw-r--r-- | ext/json/lib/json/add/exception.rb | 32 | ||||
| -rw-r--r-- | ext/json/lib/json/add/ostruct.rb | 41 | ||||
| -rw-r--r-- | ext/json/lib/json/add/range.rb | 41 | ||||
| -rw-r--r-- | ext/json/lib/json/add/rational.rb | 35 | ||||
| -rw-r--r-- | ext/json/lib/json/add/regexp.rb | 34 | ||||
| -rw-r--r-- | ext/json/lib/json/add/set.rb | 31 | ||||
| -rw-r--r-- | ext/json/lib/json/add/string.rb | 35 | ||||
| -rw-r--r-- | ext/json/lib/json/add/struct.rb | 36 | ||||
| -rw-r--r-- | ext/json/lib/json/add/symbol.rb | 41 | ||||
| -rw-r--r-- | ext/json/lib/json/add/time.rb | 44 | ||||
| -rw-r--r-- | ext/json/lib/json/common.rb | 1005 | ||||
| -rw-r--r-- | ext/json/lib/json/ext.rb | 40 | ||||
| -rw-r--r-- | ext/json/lib/json/ext/generator/state.rb | 103 | ||||
| -rw-r--r-- | ext/json/lib/json/generic_object.rb | 18 | ||||
| -rw-r--r-- | ext/json/lib/json/version.rb | 10 |
21 files changed, 1431 insertions, 412 deletions
diff --git a/ext/json/lib/json.rb b/ext/json/lib/json.rb index aeb9774ee9..26d601926f 100644 --- a/ext/json/lib/json.rb +++ b/ext/json/lib/json.rb @@ -1,4 +1,4 @@ -#frozen_string_literal: false +# frozen_string_literal: true require 'json/common' ## @@ -6,6 +6,15 @@ require 'json/common' # # \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+. @@ -127,6 +136,24 @@ require 'json/common' # # --- # +# 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+. @@ -143,8 +170,47 @@ require 'json/common' # 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). @@ -269,8 +335,27 @@ require 'json/common' # JSON.generate(JSON::MinusInfinity) # # Allow: -# ruby = [Float::NaN, Float::Infinity, Float::MinusInfinity] -# JSON.generate(ruby, allow_nan: true) # => '[NaN,Infinity,-Infinity]' +# 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. # # --- # @@ -285,6 +370,15 @@ require 'json/common' # # 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 @@ -342,6 +436,9 @@ require 'json/common' # # == \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) @@ -369,13 +466,13 @@ require 'json/common' # 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}) +# 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 # @@ -493,7 +590,7 @@ require 'json/common' # 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 @@ -553,13 +650,13 @@ require 'json/common' # 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}) +# 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 # @@ -574,10 +671,5 @@ require 'json/common' # 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 index c8b4f567cb..dc84572f31 100644 --- a/ext/json/lib/json/add/bigdecimal.rb +++ b/ext/json/lib/json/add/bigdecimal.rb @@ -1,29 +1,58 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end -defined?(::BigDecimal) or require 'bigdecimal' +begin + require 'bigdecimal' +rescue LoadError +end class BigDecimal - # Import a JSON Marshalled object. - # - # method used for JSON marshalling support. + + # See #as_json. def self.json_create(object) BigDecimal._load object['b'] end - # Marshal the object to JSON. + # 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 # - # method used for JSON marshalling support. def as_json(*) { JSON.create_id => self.class.name, - 'b' => _dump, + 'b' => _dump.force_encoding(Encoding::UTF_8), } end - # return the JSON value + # 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 +end if defined?(::BigDecimal) diff --git a/ext/json/lib/json/add/complex.rb b/ext/json/lib/json/add/complex.rb index 4d977e7589..9e3c6f2d0a 100644 --- a/ext/json/lib/json/add/complex.rb +++ b/ext/json/lib/json/add/complex.rb @@ -1,19 +1,31 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end -defined?(::Complex) or require 'complex' class Complex - # Deserializes JSON string by converting Real value <tt>r</tt>, imaginary - # value <tt>i</tt>, to a Complex object. + # See #as_json. def self.json_create(object) Complex(object['r'], object['i']) end - # Returns a hash, that will be turned into a JSON object and represent this - # object. + # 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, @@ -22,7 +34,17 @@ class Complex } end - # Stores class name (Complex) along with real value <tt>r</tt> and imaginary value <tt>i</tt> as JSON string + # 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 diff --git a/ext/json/lib/json/add/core.rb b/ext/json/lib/json/add/core.rb index bfb017c460..61ff454212 100644 --- a/ext/json/lib/json/add/core.rb +++ b/ext/json/lib/json/add/core.rb @@ -1,4 +1,4 @@ -#frozen_string_literal: false +# frozen_string_literal: true # This file requires the implementations of ruby core's custom objects for # serialisation/deserialisation. @@ -7,6 +7,7 @@ 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 index 25523561a5..88a098b637 100644 --- a/ext/json/lib/json/add/date.rb +++ b/ext/json/lib/json/add/date.rb @@ -1,4 +1,4 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end @@ -6,16 +6,29 @@ require 'date' class Date - # Deserializes JSON string by converting Julian year <tt>y</tt>, month - # <tt>m</tt>, day <tt>d</tt> and Day of Calendar Reform <tt>sg</tt> to 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) - # Returns a hash, that will be turned into a JSON object and represent this - # object. + # 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, @@ -26,8 +39,15 @@ class Date } end - # Stores class name (Date) with Julian year <tt>y</tt>, month <tt>m</tt>, day - # <tt>d</tt> and Day of Calendar Reform <tt>sg</tt> as JSON string + # 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 diff --git a/ext/json/lib/json/add/date_time.rb b/ext/json/lib/json/add/date_time.rb index 38b0e86ab8..8b0bb5d181 100644 --- a/ext/json/lib/json/add/date_time.rb +++ b/ext/json/lib/json/add/date_time.rb @@ -1,4 +1,4 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end @@ -6,9 +6,7 @@ require 'date' class DateTime - # Deserializes JSON string by converting year <tt>y</tt>, month <tt>m</tt>, - # day <tt>d</tt>, hour <tt>H</tt>, minute <tt>M</tt>, second <tt>S</tt>, - # offset <tt>of</tt> and Day of Calendar Reform <tt>sg</tt> to 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('/') @@ -23,8 +21,21 @@ class DateTime alias start sg unless method_defined?(:start) - # Returns a hash, that will be turned into a JSON object and represent this - # object. + # 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, @@ -39,9 +50,15 @@ class DateTime } end - # Stores class name (DateTime) with Julian year <tt>y</tt>, month <tt>m</tt>, - # day <tt>d</tt>, hour <tt>H</tt>, minute <tt>M</tt>, second <tt>S</tt>, - # offset <tt>of</tt> and Day of Calendar Reform <tt>sg</tt> as JSON string + # 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 diff --git a/ext/json/lib/json/add/exception.rb b/ext/json/lib/json/add/exception.rb index a107e5b3c4..e85d404982 100644 --- a/ext/json/lib/json/add/exception.rb +++ b/ext/json/lib/json/add/exception.rb @@ -1,20 +1,31 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end class Exception - # Deserializes JSON string by constructing new Exception object with message - # <tt>m</tt> and backtrace <tt>b</tt> serialized with <tt>to_json</tt> + # See #as_json. def self.json_create(object) result = new(object['m']) result.set_backtrace object['b'] result end - # Returns a hash, that will be turned into a JSON object and represent this - # object. + # 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, @@ -23,8 +34,15 @@ class Exception } end - # Stores class name (Exception) with message <tt>m</tt> and backtrace array - # <tt>b</tt> as JSON string + # 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 diff --git a/ext/json/lib/json/add/ostruct.rb b/ext/json/lib/json/add/ostruct.rb index 686cf0025d..7750498144 100644 --- a/ext/json/lib/json/add/ostruct.rb +++ b/ext/json/lib/json/add/ostruct.rb @@ -1,19 +1,35 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end -require 'ostruct' +begin + require 'ostruct' +rescue LoadError +end class OpenStruct - # Deserializes JSON string by constructing new Struct object with values - # <tt>t</tt> serialized by <tt>to_json</tt>. + # See #as_json. def self.json_create(object) new(object['t'] || object[:t]) end - # Returns a hash, that will be turned into a JSON object and represent this - # object. + # 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!" @@ -23,9 +39,16 @@ class OpenStruct } end - # Stores class name (OpenStruct) with this struct's values <tt>t</tt> as a - # JSON string. + # 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 +end if defined?(::OpenStruct) diff --git a/ext/json/lib/json/add/range.rb b/ext/json/lib/json/add/range.rb index 93529fb1c4..408d2c32f6 100644 --- a/ext/json/lib/json/add/range.rb +++ b/ext/json/lib/json/add/range.rb @@ -1,18 +1,33 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end class Range - # Deserializes JSON string by constructing new Range object with arguments - # <tt>a</tt> serialized by <tt>to_json</tt>. + # See #as_json. def self.json_create(object) new(*object['a']) end - # Returns a hash, that will be turned into a JSON object and represent this - # object. + # 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, @@ -20,9 +35,19 @@ class Range } end - # Stores class name (Range) with JSON array of arguments <tt>a</tt> which - # include <tt>first</tt> (integer), <tt>last</tt> (integer), and - # <tt>exclude_end?</tt> (boolean) as JSON string. + # 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 diff --git a/ext/json/lib/json/add/rational.rb b/ext/json/lib/json/add/rational.rb index 6be4034581..c95812ea8e 100644 --- a/ext/json/lib/json/add/rational.rb +++ b/ext/json/lib/json/add/rational.rb @@ -1,18 +1,31 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end -defined?(::Rational) or require 'rational' class Rational - # Deserializes JSON string by converting numerator value <tt>n</tt>, - # denominator value <tt>d</tt>, to a Rational object. + + # See #as_json. def self.json_create(object) Rational(object['n'], object['d']) end - # Returns a hash, that will be turned into a JSON object and represent this - # object. + # 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, @@ -21,7 +34,15 @@ class Rational } end - # Stores class name (Rational) along with numerator value <tt>n</tt> and denominator value <tt>d</tt> as JSON string + # 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 diff --git a/ext/json/lib/json/add/regexp.rb b/ext/json/lib/json/add/regexp.rb index 39d69fede7..aebfb2db5c 100644 --- a/ext/json/lib/json/add/regexp.rb +++ b/ext/json/lib/json/add/regexp.rb @@ -1,19 +1,30 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end class Regexp - # Deserializes JSON string by constructing new Regexp object with source - # <tt>s</tt> (Regexp or String) and options <tt>o</tt> serialized by - # <tt>to_json</tt> + # See #as_json. def self.json_create(object) new(object['s'], object['o']) end - # Returns a hash, that will be turned into a JSON object and represent this - # object. + # 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, @@ -22,8 +33,15 @@ class Regexp } end - # Stores class name (Regexp) with options <tt>o</tt> and source <tt>s</tt> - # (Regexp or String) as JSON string + # 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 diff --git a/ext/json/lib/json/add/set.rb b/ext/json/lib/json/add/set.rb index 71e2a0ac8b..1918353187 100644 --- a/ext/json/lib/json/add/set.rb +++ b/ext/json/lib/json/add/set.rb @@ -4,16 +4,27 @@ end defined?(::Set) or require 'set' class Set - # Import a JSON Marshalled object. - # - # method used for JSON marshalling support. + + # See #as_json. def self.json_create(object) new object['a'] end - # Marshal the object to JSON. + # 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"}> # - # method used for JSON marshalling support. def as_json(*) { JSON.create_id => self.class.name, @@ -21,7 +32,15 @@ class Set } end - # return the JSON value + # 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 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 index e8395ed42f..6760c3d86c 100644 --- a/ext/json/lib/json/add/struct.rb +++ b/ext/json/lib/json/add/struct.rb @@ -1,18 +1,32 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end class Struct - # Deserializes JSON string by constructing new Struct object with values - # <tt>v</tt> serialized by <tt>to_json</tt>. + # See #as_json. def self.json_create(object) new(*object['v']) end - # Returns a hash, that will be turned into a JSON object and represent this - # object. + # 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!" @@ -22,8 +36,16 @@ class Struct } end - # Stores class name (Struct) with Struct values <tt>v</tt> as a JSON string. - # Only named structs are supported. + # 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 diff --git a/ext/json/lib/json/add/symbol.rb b/ext/json/lib/json/add/symbol.rb index 74b13a423f..806be4f025 100644 --- a/ext/json/lib/json/add/symbol.rb +++ b/ext/json/lib/json/add/symbol.rb @@ -1,11 +1,25 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end class Symbol - # Returns a hash, that will be turned into a JSON object and represent this - # object. + + # 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, @@ -13,12 +27,25 @@ class Symbol } end - # Stores class name (Symbol) with String representation of Symbol as a JSON string. - def to_json(*a) - as_json.to_json(*a) + # 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 - # Deserializes JSON string by converting the <tt>string</tt> value stored in the object to a Symbol + # See #as_json. def self.json_create(o) o['s'].to_sym end diff --git a/ext/json/lib/json/add/time.rb b/ext/json/lib/json/add/time.rb index b73acc4086..b03d4ff251 100644 --- a/ext/json/lib/json/add/time.rb +++ b/ext/json/lib/json/add/time.rb @@ -1,37 +1,51 @@ -#frozen_string_literal: false +# frozen_string_literal: true unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED require 'json' end class Time - # Deserializes JSON string by converting time since epoch to 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 - if method_defined?(:tv_nsec) - at(object['s'], Rational(object['n'], 1000)) - else - at(object['s'], object['n'] / 1000) - end + at(object['s'], Rational(object['n'], 1000)) end - # Returns a hash, that will be turned into a JSON object and represent this - # object. + # 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(*) - nanoseconds = [ tv_usec * 1000 ] - respond_to?(:tv_nsec) and nanoseconds << tv_nsec - nanoseconds = nanoseconds.max { JSON.create_id => self.class.name, 's' => tv_sec, - 'n' => nanoseconds, + 'n' => tv_nsec, } end - # Stores class name (Time) with number of seconds since epoch and number of - # microseconds for Time as JSON string + # 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 diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb index 111d70c332..230bf08012 100644 --- a/ext/json/lib/json/common.rb +++ b/ext/json/lib/json/common.rb @@ -1,9 +1,123 @@ -#frozen_string_literal: false +# frozen_string_literal: true + require 'json/version' -require 'json/generic_object' 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 + 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 + warn(message, uplevel: uplevel) + end + end + # :call-seq: # JSON[object] -> new_array or new_string # @@ -15,17 +129,20 @@ module JSON # Otherwise, calls JSON.generate with +object+ and +opts+ (see method #generate): # ruby = [0, 1, nil] # JSON[ruby] # => '[0,1,null]' - def [](object, opts = {}) - if object.respond_to? :to_str - JSON.parse(object.to_str, opts) - else - JSON.generate(object, opts) + 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. This is either - # JSON::Ext::Parser or JSON::Pure::Parser: - # JSON.parser # => JSON::Ext::Parser + # Returns the JSON parser class that is used by JSON. attr_reader :parser # Set the JSON parser class _parser_ to be used by JSON. @@ -35,111 +152,153 @@ module JSON 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.to_s.split(/::/).inject(Object) do |p, c| - case - when c.empty? then p - when p.const_defined?(c, true) then p.const_get(c) - else - begin - p.const_missing(c) - rescue NameError => e - raise ArgumentError, "can't get const #{path}: #{e}" - end - 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 :SAFE_STATE_PROTOTYPE, State.new - const_set :FAST_STATE_PROTOTYPE, State.new( - :indent => '', - :space => '', - :object_nl => "", - :array_nl => "", - :max_nesting => false - ) - const_set :PRETTY_STATE_PROTOTYPE, State.new( - :indent => ' ', - :space => ' ', - :object_nl => "\n", - :array_nl => "\n" - ) + const_set :State, state ensure $VERBOSE = old end - # Returns the JSON generator module that is used by JSON. This is - # either JSON::Ext::Generator or JSON::Pure::Generator: - # JSON.generator # => JSON::Ext::Generator + # Returns the JSON generator module that is used by JSON. attr_reader :generator - # Sets or Returns the JSON generator state class that is used by JSON. This is - # either JSON::Ext::Generator::State or JSON::Pure::Generator::State: - # JSON.state # => JSON::Ext::Generator::State + # Sets or Returns the JSON generator state class that is used by JSON. attr_accessor :state - # Sets or returns 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' - 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 - def self.wrap(exception) - obj = new("Wrapped(#{exception.class}): #{exception.message.inspect}") - obj.set_backtrace exception.backtrace - obj - end - end + class JSONError < StandardError; end # This exception is raised if a parser error occurs. - class ParserError < JSONError; end + class ParserError < JSONError + attr_reader :line, :column + end # This exception is raised if the nesting of parsed data structures is too # deep. class NestingError < ParserError; end - # :stopdoc: - class CircularDatastructure < NestingError; end - # :startdoc: - # This exception is raised if a generator or unparser error occurs. - class GeneratorError < JSONError; end - # For backwards compatibility - UnparserError = GeneratorError # :nodoc: + class GeneratorError < JSONError + attr_reader :invalid_object + + def initialize(message, invalid_object = nil) + super(message) + @invalid_object = invalid_object + end + + def detailed_message(...) + # Exception#detailed_message doesn't exist until Ruby 3.2 + super_message = defined?(super) ? super : message + + 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 + + super(string) + 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 to_json(state = nil, *) + json + end + end module_function @@ -171,17 +330,17 @@ module JSON # {Parsing \JSON}[#module-JSON-label-Parsing+JSON]. # # Parses nested JSON objects: - # source = <<-EOT - # { - # "name": "Dave", - # "age" :40, - # "hats": [ - # "Cattleman's", - # "Panama", - # "Tophat" - # ] - # } - # EOT + # 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"]} # @@ -191,10 +350,17 @@ module JSON # # Raises JSON::ParserError (783: unexpected token at ''): # JSON.parse('') # - def parse(source, opts = {}) - Parser.new(source, **(opts||{})).parse + def parse(source, opts = nil) + opts = ParserOptions.prepare(opts) unless opts.nil? + Parser.parse(source, opts) end + PARSE_L_OPTIONS = { + max_nesting: false, + allow_nan: true, + }.freeze + private_constant :PARSE_L_OPTIONS + # :call-seq: # JSON.parse!(source, opts) -> object # @@ -206,12 +372,12 @@ module JSON # - 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 = {}) - opts = { - :max_nesting => false, - :allow_nan => true - }.merge(opts) - Parser.new(source, **(opts||{})).parse + def parse!(source, opts = nil) + if opts.nil? + parse(source, PARSE_L_OPTIONS) + else + parse(source, PARSE_L_OPTIONS.merge(opts)) + end end # :call-seq: @@ -221,8 +387,8 @@ module JSON # parse(File.read(path), opts) # # See method #parse. - def load_file(filespec, opts = {}) - parse(File.read(filespec), opts) + def load_file(filespec, opts = nil) + parse(File.read(filespec, encoding: Encoding::UTF_8), opts) end # :call-seq: @@ -232,8 +398,8 @@ module JSON # JSON.parse!(File.read(path, opts)) # # See method #parse! - def load_file!(filespec, opts = {}) - parse!(File.read(filespec), opts) + def load_file!(filespec, opts = nil) + parse!(File.read(filespec, encoding: Encoding::UTF_8), opts) end # :call-seq: @@ -241,7 +407,7 @@ module JSON # # Returns a \String containing the generated \JSON data. # - # See also JSON.fast_generate, JSON.pretty_generate. + # See also JSON.pretty_generate. # # Argument +obj+ is the Ruby object to be converted to \JSON. # @@ -274,30 +440,12 @@ module JSON # def generate(obj, opts = nil) if State === opts - state, opts = opts, nil + opts.generate(obj) else - state = SAFE_STATE_PROTOTYPE.dup - end - if opts - 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 - state = state.configure(opts) + State.generate(obj, opts, nil) end - state.generate(obj) 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.fast_generate(obj, opts) -> new_string # @@ -312,29 +460,21 @@ module JSON # # Raises SystemStackError (stack level too deep): # JSON.fast_generate(a) def fast_generate(obj, opts = nil) - if State === opts - state, opts = 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 - state = FAST_STATE_PROTOTYPE.dup + warn "JSON.fast_generate is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1 end - if opts - 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 - state.configure(opts) - end - state.generate(obj) + generate(obj, 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: + PRETTY_GENERATE_OPTIONS = { + indent: ' ', + space: ' ', + object_nl: "\n", + array_nl: "\n", + }.freeze + private_constant :PRETTY_GENERATE_OPTIONS # :call-seq: # JSON.pretty_generate(obj, opts = nil) -> new_string @@ -367,49 +507,60 @@ module JSON # } # def pretty_generate(obj, opts = nil) - if State === opts - state, opts = opts, nil - else - state = PRETTY_STATE_PROTOTYPE.dup - end + 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 - else - raise TypeError, "can't convert #{opts.class} into Hash" + 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 - state.configure(opts) + options = options.merge(opts) end - state.generate(obj) + + State.generate(obj, options, nil) 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: + # 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 - class << self - # 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} - attr_accessor :load_default_options - end - self.load_default_options = { + @unsafe_load_default_options = { :max_nesting => false, :allow_nan => true, - :allow_blank => 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.load(source, proc = nil, options = {}) -> object + # 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. @@ -424,12 +575,9 @@ module JSON # - 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. - # 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 +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=. + # The default options can be changed via method JSON.unsafe_load_default_options=. # # --- # @@ -437,17 +585,188 @@ module JSON # <tt>parse(source, opts)</tt>; see #parse. # # Source for following examples: - # source = <<-EOT + # 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 # { - # "name": "Dave", - # "age" :40, - # "hats": [ - # "Cattleman's", - # "Panama", - # "Tophat" - # ] + # "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"} # } - # EOT + # 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 + _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) @@ -512,6 +831,7 @@ module JSON # when Array # obj.map! {|v| deserialize_obj v } # end + # obj # }) # pp ruby # Output: @@ -533,51 +853,53 @@ module JSON # #<Admin:0x00000000064c41f8 # @attributes={"type"=>"Admin", "password"=>"0wn3d"}>} # - def load(source, proc = nil, options = {}) - opts = load_default_options.merge options - 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 + def load(source, proc = nil, options = nil) + if proc && options.nil? && proc.is_a?(Hash) + options = proc + proc = nil end - if opts[:allow_blank] && (source.nil? || source.empty?) - source = 'null' - end - result = parse(source, opts) - recurse_proc(result, &proc) if proc - result - end - # Recursively calls passed _Proc_ if the parsed data structure is an _Array_ or _Hash_ - def recurse_proc(result, &proc) # :nodoc: - 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 + opts = if options.nil? + if proc && proc.is_a?(Hash) + options, proc = proc, nil + options + else + _load_default_options + end else - proc.call result + _load_default_options.merge(options) end - end - alias restore load - module_function :restore + 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 - class << self - # Sets or returns the default options for the JSON.dump method. - # Initially: - # opts = JSON.dump_default_options - # opts # => {:max_nesting=>false, :allow_nan=>true, :escape_slash=>false} - attr_accessor :dump_default_options + 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 - self.dump_default_options = { + + # 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, - :escape_slash => false, } # :call-seq: @@ -607,30 +929,198 @@ module JSON # puts File.read(path) # Output: # {"foo":[0,1],"bar":{"baz":2,"bat":3},"bam":"bad"} - 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 + 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 + end + + 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 = JSON._dump_default_options opts = opts.merge(:max_nesting => limit) if limit - result = generate(obj, opts) - if anIO - anIO.write result - anIO + opts = opts.merge(kwargs) if kwargs + + begin + State.generate(obj, opts, anIO) + rescue JSON::NestingError + raise ArgumentError, "exceed depth limit" + end + end + + # :stopdoc: + # 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 + + 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 + warn "JSON.fast_unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1 + end + generate(...) + end + module_function :fast_unparse + + 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 + 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 + + 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 - result + 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 + + 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. + # + # module MyApp + # JSONC_CODER = JSON::Coder.new( + # allow_trailing_comma: true + # ) + # end + # + # MyApp::JSONC_CODER.load(document) + # + 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 + + # 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 - rescue JSON::NestingError - raise ArgumentError, "exceed depth limit" end - # Encodes string using String.encode. - def self.iconv(to, from, string) - string.encode(to, from) + 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 end end @@ -640,8 +1130,14 @@ 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 @@ -649,8 +1145,14 @@ module ::Kernel # 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 @@ -661,22 +1163,11 @@ module ::Kernel # # The _opts_ argument is passed through to generate/parse respectively. See # generate and parse for their documentation. - def JSON(object, *args) - if object.respond_to? :to_str - JSON.parse(object.to_str, args.first) - else - JSON.generate(object, args.first) - end + def JSON(object, opts = nil) + JSON[object, opts] end end -# Extends any Class to include _json_creatable?_ method. -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. The hash - # should include the required data. - def json_creatable? - respond_to?(:json_create) - end +class Object + include JSON::GeneratorMethods end diff --git a/ext/json/lib/json/ext.rb b/ext/json/lib/json/ext.rb index 7264a857fa..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 Ext 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 unless defined?(::JSON::JSON_LOADED) + 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 index 108309db26..5c8ace354b 100644 --- a/ext/json/lib/json/generic_object.rb +++ b/ext/json/lib/json/generic_object.rb @@ -1,5 +1,9 @@ -#frozen_string_literal: false -require 'ostruct' +# 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 @@ -48,14 +52,6 @@ module JSON table end - def [](name) - __send__(name) - end unless method_defined?(:[]) - - def []=(name, value) - __send__("#{name}=", value) - end unless method_defined?(:[]=) - def |(other) self.class[other.to_hash.merge(to_hash)] end @@ -67,5 +63,5 @@ module JSON def to_json(*a) as_json.to_json(*a) end - end + end if defined?(::OpenStruct) end diff --git a/ext/json/lib/json/version.rb b/ext/json/lib/json/version.rb index e4dd3e3367..a69590ff9c 100644 --- a/ext/json/lib/json/version.rb +++ b/ext/json/lib/json/version.rb @@ -1,9 +1,5 @@ -# frozen_string_literal: false +# frozen_string_literal: true + module JSON - # JSON version - VERSION = '2.3.1' - 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.7' end |
