diff options
Diffstat (limited to 'ext/json/lib')
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 |
