├── .rspec
├── .document
├── .yardopts
├── README.md
├── gemfiles
└── gemfile-2-jruby
├── benchmark.rb
├── lib
├── multi_json
│ ├── adapters
│ │ ├── json_gem.rb
│ │ ├── json_pure.rb
│ │ ├── gson.rb
│ │ ├── yajl.rb
│ │ ├── jr_jackson.rb
│ │ ├── ok_json.rb
│ │ ├── json_common.rb
│ │ └── oj.rb
│ ├── adapter_error.rb
│ ├── version.rb
│ ├── parse_error.rb
│ ├── options_cache.rb
│ ├── options.rb
│ ├── convertible_hash_keys.rb
│ ├── adapter.rb
│ └── vendor
│ │ └── okjson.rb
└── multi_json.rb
├── spec
├── multi_json
│ ├── adapters
│ │ ├── gson_spec.rb
│ │ ├── yajl_spec.rb
│ │ ├── ok_json_spec.rb
│ │ ├── jr_jackson_spec.rb
│ │ ├── json_gem_spec.rb
│ │ ├── json_pure_spec.rb
│ │ └── oj_spec.rb
│ └── options_cache_spec.rb
├── shared
│ ├── json_common_adapter.rb
│ ├── options.rb
│ └── adapter.rb
├── spec_helper.rb
└── multi_json_spec.rb
├── .gitignore
├── Gemfile
├── Rakefile
├── .github
└── workflows
│ └── ci.yml
├── LICENSE.md
├── multi_json.gemspec
├── .rubocop.yml
├── CONTRIBUTING.md
└── CHANGELOG.md
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --order random
3 |
--------------------------------------------------------------------------------
/.document:
--------------------------------------------------------------------------------
1 | LICENSE.md
2 | README.md
3 | bin/*
4 | features/**/*.feature
5 | lib/**/*.rb
6 |
--------------------------------------------------------------------------------
/.yardopts:
--------------------------------------------------------------------------------
1 | --markup markdown
2 | -
3 | CHANGELOG.md
4 | CONTRIBUTING.md
5 | LICENSE.md
6 | README.md
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MultiJSON has moved
2 |
3 | This project has moved to https://github.com/sferik/multi_json
4 |
--------------------------------------------------------------------------------
/gemfiles/gemfile-2-jruby:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "json", "~> 2.0", require: false
4 | gem "json_pure", "~> 2.0", require: false
5 | gem "gson", ">= 0.6", require: false
6 |
7 | gemspec path: ".."
8 |
--------------------------------------------------------------------------------
/benchmark.rb:
--------------------------------------------------------------------------------
1 | require "oj"
2 | require "multi_json"
3 | require "benchmark/ips"
4 |
5 | MultiJson.use :oj
6 |
7 | Benchmark.ips do |x|
8 | x.time = 10
9 | x.warmup = 1
10 | x.report { MultiJson.load(MultiJson.dump(a: 1, b: 2, c: 3)) }
11 | end
12 |
--------------------------------------------------------------------------------
/lib/multi_json/adapters/json_gem.rb:
--------------------------------------------------------------------------------
1 | require "json/ext"
2 | require "multi_json/adapters/json_common"
3 |
4 | module MultiJson
5 | module Adapters
6 | # Use the JSON gem to dump/load.
7 | class JsonGem < JsonCommon
8 | ParseError = ::JSON::ParserError
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/multi_json/adapters/json_pure.rb:
--------------------------------------------------------------------------------
1 | require "json/pure"
2 | require "multi_json/adapters/json_common"
3 |
4 | module MultiJson
5 | module Adapters
6 | # Use JSON pure to dump/load.
7 | class JsonPure < JsonCommon
8 | ParseError = ::JSON::ParserError
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/multi_json/adapters/gson_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | return unless RSpec.configuration.gson?
3 |
4 | require "shared/adapter"
5 | require "multi_json/adapters/gson"
6 |
7 | RSpec.describe MultiJson::Adapters::Gson, :gson do
8 | it_behaves_like "an adapter", described_class
9 | end
10 |
--------------------------------------------------------------------------------
/spec/multi_json/adapters/yajl_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | return unless RSpec.configuration.yajl?
3 |
4 | require "shared/adapter"
5 | require "multi_json/adapters/yajl"
6 |
7 | RSpec.describe MultiJson::Adapters::Yajl, :yajl do
8 | it_behaves_like "an adapter", described_class
9 | end
10 |
--------------------------------------------------------------------------------
/spec/multi_json/adapters/ok_json_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | return unless RSpec.configuration.ok_json?
3 |
4 | require "shared/adapter"
5 | require "multi_json/adapters/ok_json"
6 |
7 | RSpec.describe MultiJson::Adapters::OkJson, :ok_json do
8 | it_behaves_like "an adapter", described_class
9 | end
10 |
--------------------------------------------------------------------------------
/spec/multi_json/adapters/jr_jackson_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | return unless RSpec.configuration.jrjackson?
3 |
4 | require "shared/adapter"
5 | require "multi_json/adapters/jr_jackson"
6 |
7 | RSpec.describe MultiJson::Adapters::JrJackson, :jrjackson do
8 | it_behaves_like "an adapter", described_class
9 | end
10 |
--------------------------------------------------------------------------------
/spec/multi_json/adapters/json_gem_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "shared/adapter"
3 | require "shared/json_common_adapter"
4 | require "multi_json/adapters/json_gem"
5 |
6 | RSpec.describe MultiJson::Adapters::JsonGem, :json do
7 | it_behaves_like "an adapter", described_class
8 | it_behaves_like "JSON-like adapter", described_class
9 | end
10 |
--------------------------------------------------------------------------------
/spec/multi_json/adapters/json_pure_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "shared/adapter"
3 | require "shared/json_common_adapter"
4 | require "multi_json/adapters/json_pure"
5 |
6 | RSpec.describe MultiJson::Adapters::JsonPure, :json_pure do
7 | it_behaves_like "an adapter", described_class
8 | it_behaves_like "JSON-like adapter", described_class
9 | end
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## TEXTMATE
2 | *.tmproj
3 | tmtags
4 |
5 | ## EMACS
6 | *~
7 | \#*
8 | .\#*
9 |
10 | ## VIM
11 | *.swp
12 |
13 | ## PROJECT::GENERAL
14 | .yardoc
15 | coverage
16 | doc
17 | rdoc
18 | log
19 |
20 | ## BUNDLER
21 | Gemfile.lock
22 | *.gem
23 | .bundle
24 | pkg
25 | gemfiles/*.lock
26 |
27 | ## RBENV
28 | .ruby-version
29 | .rbenv*
30 |
31 | ## RCOV
32 | coverage.data
33 |
34 | tmp
35 |
36 | ## RUBINIUS
37 | *.rbc
38 |
--------------------------------------------------------------------------------
/lib/multi_json/adapter_error.rb:
--------------------------------------------------------------------------------
1 | module MultiJson
2 | class AdapterError < ArgumentError
3 | attr_reader :cause
4 |
5 | def self.build(original_exception)
6 | message = "Did not recognize your adapter specification (#{original_exception.message})."
7 | new(message).tap do |exception|
8 | exception.instance_eval do
9 | @cause = original_exception
10 | set_backtrace original_exception.backtrace
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/multi_json/version.rb:
--------------------------------------------------------------------------------
1 | module MultiJson
2 | class Version
3 | MAJOR = 1 unless defined? MultiJson::Version::MAJOR
4 | MINOR = 15 unless defined? MultiJson::Version::MINOR
5 | PATCH = 0 unless defined? MultiJson::Version::PATCH
6 | PRE = nil unless defined? MultiJson::Version::PRE
7 |
8 | class << self
9 | # @return [String]
10 | def to_s
11 | [MAJOR, MINOR, PATCH, PRE].compact.join(".")
12 | end
13 | end
14 | end
15 |
16 | VERSION = Version.to_s.freeze
17 | end
18 |
--------------------------------------------------------------------------------
/lib/multi_json/parse_error.rb:
--------------------------------------------------------------------------------
1 | module MultiJson
2 | class ParseError < StandardError
3 | attr_reader :data, :cause
4 |
5 | def self.build(original_exception, data)
6 | new(original_exception.message).tap do |exception|
7 | exception.instance_eval do
8 | @cause = original_exception
9 | set_backtrace original_exception.backtrace
10 | @data = data
11 | end
12 | end
13 | end
14 | end
15 |
16 | DecodeError = LoadError = ParseError # Legacy support
17 | end
18 |
--------------------------------------------------------------------------------
/lib/multi_json/adapters/gson.rb:
--------------------------------------------------------------------------------
1 | require "gson"
2 | require "stringio"
3 | require "multi_json/adapter"
4 |
5 | module MultiJson
6 | module Adapters
7 | # Use the gson.rb library to dump/load.
8 | class Gson < Adapter
9 | ParseError = ::Gson::DecodeError
10 |
11 | def load(string, options = {})
12 | ::Gson::Decoder.new(options).decode(string)
13 | end
14 |
15 | def dump(object, options = {})
16 | ::Gson::Encoder.new(options).encode(object)
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/multi_json/adapters/yajl.rb:
--------------------------------------------------------------------------------
1 | require "yajl"
2 | require "multi_json/adapter"
3 |
4 | module MultiJson
5 | module Adapters
6 | # Use the Yajl-Ruby library to dump/load.
7 | class Yajl < Adapter
8 | ParseError = ::Yajl::ParseError
9 |
10 | def load(string, options = {})
11 | ::Yajl::Parser.new(symbolize_keys: options[:symbolize_keys]).parse(string)
12 | end
13 |
14 | def dump(object, options = {})
15 | ::Yajl::Encoder.encode(object, options)
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/multi_json/options_cache_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe MultiJson::OptionsCache do
4 | before { described_class.reset }
5 |
6 | it "doesn't leak memory" do
7 | described_class::MAX_CACHE_SIZE.succ.times do |i|
8 | described_class.fetch(:dump, key: i) do
9 | {foo: i}
10 | end
11 |
12 | described_class.fetch(:load, key: i) do
13 | {foo: i}
14 | end
15 | end
16 |
17 | expect(described_class.instance_variable_get(:@dump_cache).length).to eq(1)
18 | expect(described_class.instance_variable_get(:@load_cache).length).to eq(1)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "json", "~> 2.0", require: false
4 | gem "json_pure", "~> 2.0", require: false
5 |
6 | gem "rake", ">= 13.1"
7 | gem "rspec", ">= 3.13"
8 | gem "rubocop", ">= 1.62.1"
9 | gem "rubocop-performance", ">= 1.20.2"
10 | gem "rubocop-rake", ">= 0.6.0"
11 | gem "rubocop-rspec", ">= 2.27.1"
12 | gem "standard", ">= 1.35.1"
13 |
14 | gem "gson", ">= 0.6", platforms: [:jruby], require: false
15 | gem "jrjackson", ">= 0.4.18", platforms: [:jruby], require: false
16 | gem "oj", "~> 3.0", platforms: %i[ruby windows], require: false
17 | gem "yajl-ruby", "~> 1.3", platforms: %i[ruby windows], require: false
18 |
19 | gemspec
20 |
--------------------------------------------------------------------------------
/lib/multi_json/adapters/jr_jackson.rb:
--------------------------------------------------------------------------------
1 | require "jrjackson" unless defined?(JrJackson)
2 | require "multi_json/adapter"
3 |
4 | module MultiJson
5 | module Adapters
6 | # Use the jrjackson.rb library to dump/load.
7 | class JrJackson < Adapter
8 | ParseError = ::JrJackson::ParseError
9 |
10 | def load(string, options = {}) # :nodoc:
11 | ::JrJackson::Json.load(string, options)
12 | end
13 |
14 | if ::JrJackson::Json.method(:dump).arity == 1
15 | def dump(object, _)
16 | ::JrJackson::Json.dump(object)
17 | end
18 | else
19 | def dump(object, options = {})
20 | ::JrJackson::Json.dump(object, options)
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/multi_json/adapters/ok_json.rb:
--------------------------------------------------------------------------------
1 | require "multi_json/adapter"
2 | require "multi_json/convertible_hash_keys"
3 | require "multi_json/vendor/okjson"
4 |
5 | module MultiJson
6 | module Adapters
7 | class OkJson < Adapter
8 | include ConvertibleHashKeys
9 | ParseError = ::MultiJson::OkJson::Error
10 |
11 | def load(string, options = {})
12 | result = ::MultiJson::OkJson.decode("[#{string}]").first
13 | options[:symbolize_keys] ? symbolize_keys(result) : result
14 | rescue ArgumentError # invalid byte sequence in UTF-8
15 | raise ParseError
16 | end
17 |
18 | def dump(object, _ = {})
19 | ::MultiJson::OkJson.valenc(stringify_keys(object))
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/multi_json/adapters/json_common.rb:
--------------------------------------------------------------------------------
1 | require "multi_json/adapter"
2 |
3 | module MultiJson
4 | module Adapters
5 | class JsonCommon < Adapter
6 | defaults :load, create_additions: false, quirks_mode: true
7 |
8 | def load(string, options = {})
9 | string = string.dup.force_encoding(::Encoding::ASCII_8BIT) if string.respond_to?(:force_encoding)
10 |
11 | options[:symbolize_names] = true if options.delete(:symbolize_keys)
12 | ::JSON.parse(string, options)
13 | end
14 |
15 | def dump(object, options = {})
16 | if options.delete(:pretty)
17 | options.merge!({
18 | indent: " ",
19 | space: " ",
20 | object_nl: "\n",
21 | array_nl: "\n"
22 | })
23 | end
24 |
25 | object.to_json(options)
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler"
2 | Bundler::GemHelper.install_tasks
3 |
4 | require "standard/rake"
5 | require "rubocop/rake_task"
6 |
7 | require "rspec/core/rake_task"
8 | RSpec::Core::RakeTask.new(:base_spec) do |task|
9 | task.pattern = "spec/{multi_json,options_cache}_spec.rb"
10 | end
11 |
12 | namespace :adapters do
13 | Dir["spec/multi_json/adapters/*_spec.rb"].each do |adapter_spec|
14 | adapter_name = adapter_spec[/(\w+)_spec/, 1]
15 | desc "Run #{adapter_name} adapter specs"
16 | RSpec::Core::RakeTask.new(adapter_name) do |task|
17 | task.pattern = adapter_spec
18 | end
19 | end
20 | end
21 |
22 | task spec: %w[
23 | base_spec
24 | adapters:oj
25 | adapters:yajl
26 | adapters:json_gem
27 | adapters:json_pure
28 | adapters:ok_json
29 | adapters:gson
30 | adapters:jr_jackson
31 | ]
32 |
33 | task default: :spec
34 | task test: :spec
35 |
--------------------------------------------------------------------------------
/lib/multi_json/options_cache.rb:
--------------------------------------------------------------------------------
1 | module MultiJson
2 | module OptionsCache
3 | extend self
4 |
5 | def reset
6 | @dump_cache = {}
7 | @load_cache = {}
8 | end
9 |
10 | def fetch(type, key, &block)
11 | cache = instance_variable_get(:"@#{type}_cache")
12 | cache&.key?(key) ? cache[key] : write(cache, key, &block)
13 | end
14 |
15 | private
16 |
17 | # Normally MultiJson is used with a few option sets for both dump/load
18 | # methods. When options are generated dynamically though, every call would
19 | # cause a cache miss and the cache would grow indefinitely. To prevent
20 | # this, we just reset the cache every time the number of keys outgrows
21 | # 1000.
22 | MAX_CACHE_SIZE = 1000
23 |
24 | def write(cache, key)
25 | reset unless cache
26 | cache.clear if cache.length >= MAX_CACHE_SIZE
27 | cache[key] = yield
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Ruby
2 |
3 | on:
4 | push:
5 | branches: [main, master] # TODO: rename and get rid of 'master'
6 | paths-ignore:
7 | - '**/*.md'
8 | pull_request:
9 | branches: [main, master] # TODO: rename and get rid of 'master'
10 | paths-ignore:
11 | - '**/*.md'
12 | workflow_dispatch:
13 |
14 | jobs:
15 | test:
16 | strategy:
17 | matrix:
18 | ruby-version: ['3.0', '3.1', '3.2', 'jruby']
19 | platform: [ubuntu-latest, macos-latest, windows-latest]
20 | runs-on: ${{ matrix.platform }}
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Set up Ruby
25 | uses: ruby/setup-ruby@v1
26 | with:
27 | ruby-version: ${{ matrix.ruby-version }}
28 | bundler-cache: false # Do not bundle install yet, need recent versions for Windows
29 | - name: Run tests
30 | run: |
31 | gem install bundler
32 | bundle install
33 | bundle exec rake
34 |
35 |
--------------------------------------------------------------------------------
/spec/shared/json_common_adapter.rb:
--------------------------------------------------------------------------------
1 | shared_examples_for "JSON-like adapter" do |adapter|
2 | before { MultiJson.use adapter }
3 |
4 | describe ".dump" do
5 | before { MultiJson.dump_options = MultiJson.adapter.dump_options = nil }
6 |
7 | describe "with :pretty option set to true" do
8 | it "passes default pretty options" do
9 | object = "foo"
10 | expect(object).to receive(:to_json).with({
11 | indent: " ",
12 | space: " ",
13 | object_nl: "\n",
14 | array_nl: "\n"
15 | })
16 | MultiJson.dump(object, pretty: true)
17 | end
18 | end
19 |
20 | describe "with :indent option" do
21 | it "passes it on dump" do
22 | object = "foo"
23 | expect(object).to receive(:to_json).with({indent: "\t"})
24 | MultiJson.dump(object, indent: "\t")
25 | end
26 | end
27 | end
28 |
29 | describe ".load" do
30 | it "passes :quirks_mode option" do
31 | expect(JSON).to receive(:parse).with("[123]", {quirks_mode: false, create_additions: false})
32 | MultiJson.load("[123]", quirks_mode: false)
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010-2024 Michael Bleigh, Josh Kalderimis, Erik Berlin, Pavel Pravosud
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/spec/multi_json/adapters/oj_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | return unless RSpec.configuration.oj?
3 |
4 | require "shared/adapter"
5 | require "multi_json/adapters/oj"
6 |
7 | RSpec.describe MultiJson::Adapters::Oj, :oj do
8 | it_behaves_like "an adapter", described_class
9 |
10 | describe ".dump" do
11 | describe "#dump_options" do
12 | around { |example| with_default_options(&example) }
13 |
14 | it "ensures indent is a Fixnum" do
15 | expect { MultiJson.dump(42, indent: "") }.not_to raise_error
16 | end
17 | end
18 | end
19 |
20 | it "Oj does not create symbols on parse" do
21 | MultiJson.load('{"json_class":"ZOMG"}')
22 |
23 | expect do
24 | MultiJson.load('{"json_class":"OMG"}')
25 | end.not_to(change { Symbol.all_symbols.count })
26 | end
27 |
28 | context "with Oj.default_settings" do
29 | around do |example|
30 | options = Oj.default_options
31 | Oj.default_options = {symbol_keys: true}
32 | example.call
33 | Oj.default_options = options
34 | end
35 |
36 | it "ignores global settings" do
37 | example = '{"a": 1, "b": 2}'
38 | expected = {"a" => 1, "b" => 2}
39 | expect(MultiJson.load(example)).to eq(expected)
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/multi_json.gemspec:
--------------------------------------------------------------------------------
1 | require File.expand_path("lib/multi_json/version.rb", __dir__)
2 |
3 | Gem::Specification.new do |spec|
4 | spec.authors = ["Michael Bleigh", "Josh Kalderimis", "Erik Berlin", "Pavel Pravosud"]
5 | spec.description = "A common interface to multiple JSON libraries, including Oj, Yajl, the JSON gem (with C-extensions), the pure-Ruby JSON gem, gson, JrJackson, and OkJson."
6 | spec.email = %w[michael@intridea.com josh.kalderimis@gmail.com sferik@gmail.com pavel@pravosud.com]
7 | spec.files = Dir["*.md", "lib/**/*"]
8 | spec.homepage = "https://github.com/intridea/multi_json"
9 | spec.license = "MIT"
10 | spec.name = "multi_json"
11 | spec.require_path = "lib"
12 | spec.required_ruby_version = ">= 3.0"
13 | spec.summary = "A common interface to multiple JSON libraries."
14 | spec.version = MultiJson::Version
15 |
16 | spec.metadata = {
17 | "bug_tracker_uri" => "https://github.com/intridea/multi_json/issues",
18 | "changelog_uri" => "https://github.com/intridea/multi_json/blob/v#{spec.version}/CHANGELOG.md",
19 | "documentation_uri" => "https://www.rubydoc.info/gems/multi_json/#{spec.version}",
20 | "rubygems_mfa_required" => "true",
21 | "source_code_uri" => "https://github.com/intridea/multi_json/tree/v#{spec.version}",
22 | "wiki_uri" => "https://github.com/intridea/multi_json/wiki"
23 | }
24 | end
25 |
--------------------------------------------------------------------------------
/lib/multi_json/options.rb:
--------------------------------------------------------------------------------
1 | module MultiJson
2 | module Options
3 | def load_options=(options)
4 | OptionsCache.reset
5 | @load_options = options
6 | end
7 |
8 | def dump_options=(options)
9 | OptionsCache.reset
10 | @dump_options = options
11 | end
12 |
13 | def load_options(*args)
14 | (defined?(@load_options) && get_options(@load_options, *args)) || default_load_options
15 | end
16 |
17 | def dump_options(*args)
18 | (defined?(@dump_options) && get_options(@dump_options, *args)) || default_dump_options
19 | end
20 |
21 | def default_load_options
22 | @default_load_options ||= {}
23 | end
24 |
25 | def default_dump_options
26 | @default_dump_options ||= {}
27 | end
28 |
29 | private
30 |
31 | def get_options(options, *args)
32 | return handle_callable_options(options, *args) if options_responds_to_call?(options)
33 |
34 | handle_hashable_options(options)
35 | end
36 |
37 | def options_responds_to_call?(options)
38 | options.respond_to?(:call)
39 | end
40 |
41 | def handle_callable_options(options, *args)
42 | options.arity.zero? ? options[] : options[*args]
43 | end
44 |
45 | def handle_hashable_options(options)
46 | options.respond_to?(:to_hash) ? options.to_hash : nil
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/multi_json/convertible_hash_keys.rb:
--------------------------------------------------------------------------------
1 | module MultiJson
2 | module ConvertibleHashKeys
3 | private
4 |
5 | def symbolize_keys(hash)
6 | prepare_hash(hash) do |key|
7 | key.respond_to?(:to_sym) ? key.to_sym : key
8 | end
9 | end
10 |
11 | def stringify_keys(hash)
12 | prepare_hash(hash) do |key|
13 | key.respond_to?(:to_s) ? key.to_s : key
14 | end
15 | end
16 |
17 | def prepare_hash(hash, &key_modifier)
18 | return handle_simple_objects(hash) unless hash.is_a?(Array) || hash.is_a?(Hash)
19 | return handle_array(hash, &key_modifier) if hash.is_a?(Array)
20 |
21 | handle_hash(hash, &key_modifier)
22 | end
23 |
24 | def handle_simple_objects(obj)
25 | return obj if simple_object?(obj) || obj.respond_to?(:to_json)
26 |
27 | obj.respond_to?(:to_s) ? obj.to_s : obj
28 | end
29 |
30 | def handle_array(array, &key_modifier)
31 | array.map { |value| prepare_hash(value, &key_modifier) }
32 | end
33 |
34 | def handle_hash(original_hash, &key_modifier)
35 | original_hash.each_with_object({}) do |(key, value), result|
36 | modified_key = yield(key)
37 | result[modified_key] = prepare_hash(value, &key_modifier)
38 | end
39 | end
40 |
41 | def simple_object?(obj)
42 | obj.is_a?(String) || obj.is_a?(Numeric) || obj == true || obj == false || obj.nil?
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/multi_json/adapter.rb:
--------------------------------------------------------------------------------
1 | require "singleton"
2 | require "multi_json/options"
3 |
4 | module MultiJson
5 | class Adapter
6 | extend Options
7 | include Singleton
8 |
9 | class << self
10 | def defaults(action, value)
11 | metaclass = class << self; self; end
12 |
13 | metaclass.instance_eval do
14 | define_method(:"default_#{action}_options") { value }
15 | end
16 | end
17 |
18 | def load(string, options = {})
19 | string = string.read if string.respond_to?(:read)
20 | raise self::ParseError if blank?(string)
21 |
22 | instance.load(string, cached_load_options(options))
23 | end
24 |
25 | def dump(object, options = {})
26 | instance.dump(object, cached_dump_options(options))
27 | end
28 |
29 | private
30 |
31 | def blank?(input)
32 | input.nil? || /\A\s*\z/.match?(input)
33 | rescue ArgumentError # invalid byte sequence in UTF-8
34 | false
35 | end
36 |
37 | def cached_dump_options(options)
38 | OptionsCache.fetch(:dump, options) do
39 | dump_options(options).merge(MultiJson.dump_options(options)).merge!(options)
40 | end
41 | end
42 |
43 | def cached_load_options(options)
44 | OptionsCache.fetch(:load, options) do
45 | load_options(options).merge(MultiJson.load_options(options)).merge!(options)
46 | end
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | require:
2 | - standard
3 |
4 | plugins:
5 | - rubocop-performance
6 | - rubocop-rake
7 | - rubocop-rspec
8 | - standard-performance
9 |
10 | AllCops:
11 | Exclude:
12 | - "lib/multi_json/vendor/okjson.rb"
13 | NewCops: enable
14 | TargetRubyVersion: 3.0
15 |
16 | Layout/ArgumentAlignment:
17 | EnforcedStyle: with_fixed_indentation
18 | IndentationWidth: 2
19 |
20 | Layout/CaseIndentation:
21 | EnforcedStyle: end
22 |
23 | Layout/EndAlignment:
24 | EnforcedStyleAlignWith: start_of_line
25 |
26 | Layout/ExtraSpacing:
27 | AllowForAlignment: false
28 |
29 | Layout/LineLength:
30 | Max: 140
31 |
32 | Layout/MultilineMethodCallIndentation:
33 | EnforcedStyle: indented
34 |
35 | Layout/ParameterAlignment:
36 | EnforcedStyle: with_fixed_indentation
37 | IndentationWidth: 2
38 |
39 | Layout/SpaceInsideHashLiteralBraces:
40 | EnforcedStyle: no_space
41 |
42 | Metrics/ParameterLists:
43 | CountKeywordArgs: false
44 |
45 | Style/Alias:
46 | EnforcedStyle: prefer_alias_method
47 |
48 | Style/Documentation:
49 | Enabled: false
50 |
51 | Style/FrozenStringLiteralComment:
52 | EnforcedStyle: never
53 |
54 | Style/OpenStructUse:
55 | Enabled: false
56 |
57 | Style/RescueStandardError:
58 | EnforcedStyle: implicit
59 |
60 | Style/StringLiterals:
61 | EnforcedStyle: double_quotes
62 |
63 | Style/StringLiteralsInInterpolation:
64 | EnforcedStyle: double_quotes
65 |
66 | Style/TernaryParentheses:
67 | EnforcedStyle: require_parentheses_when_complex
68 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 | In the spirit of [free software][free-sw], **everyone** is encouraged to help
3 | improve this project.
4 |
5 | [free-sw]: http://www.fsf.org/licensing/essays/free-sw.html
6 |
7 | Here are some ways *you* can contribute:
8 |
9 | * by using alpha, beta, and prerelease versions
10 | * by reporting bugs
11 | * by suggesting new features
12 | * by writing or editing documentation
13 | * by writing specifications
14 | * by writing code (**no patch is too small**: fix typos, add comments, clean up
15 | inconsistent whitespace)
16 | * by refactoring code
17 | * by closing [issues][]
18 | * by reviewing patches
19 |
20 | [issues]: https://github.com/intridea/multi_json/issues
21 |
22 | ## Submitting an Issue
23 | We use the [GitHub issue tracker][issues] to track bugs and features. Before
24 | submitting a bug report or feature request, check to make sure it hasn't
25 | already been submitted. When submitting a bug report, please include a [Gist][]
26 | that includes a stack trace and any details that may be necessary to reproduce
27 | the bug, including your gem version, Ruby version, and operating system.
28 | Ideally, a bug report should include a pull request with failing specs.
29 |
30 | [gist]: https://gist.github.com/
31 |
32 | ## Submitting a Pull Request
33 | 1. [Fork the repository.][fork]
34 | 2. [Create a topic branch.][branch]
35 | 3. Add specs for your unimplemented feature or bug fix.
36 | 4. Run `bundle exec rake spec`. If your specs pass, return to step 3.
37 | 5. Implement your feature or bug fix.
38 | 6. Run `bundle exec rake spec`. If your specs fail, return to step 5.
39 | 7. Run `open coverage/index.html`. If your changes are not completely covered
40 | by your tests, return to step 3.
41 | 8. Add, commit, and push your changes.
42 | 9. [Submit a pull request.][pr]
43 |
44 | [fork]: http://help.github.com/fork-a-repo/
45 | [branch]: http://learn.github.com/p/branching.html
46 | [pr]: http://help.github.com/send-pull-requests/
47 |
--------------------------------------------------------------------------------
/lib/multi_json/adapters/oj.rb:
--------------------------------------------------------------------------------
1 | require "set"
2 | require "oj"
3 | require "multi_json/adapter"
4 |
5 | module MultiJson
6 | module Adapters
7 | # Use the Oj library to dump/load.
8 | class Oj < Adapter
9 | defaults :load, mode: :strict, symbolize_keys: false
10 | defaults :dump, mode: :compat, time_format: :ruby, use_to_json: true
11 |
12 | # In certain cases OJ gem may throw JSON::ParserError exception instead
13 | # of its own class. Also, we can't expect ::JSON::ParserError and
14 | # ::Oj::ParseError to always be defined, since it's often not the case.
15 | # Because of this, we can't reference those classes directly and have to
16 | # do string comparison instead. This will not catch subclasses, but it
17 | # shouldn't be a problem since the library is not known to be using it
18 | # (at least for now).
19 | class ParseError < ::SyntaxError
20 | WRAPPED_CLASSES = %w[Oj::ParseError JSON::ParserError].to_set.freeze
21 |
22 | def self.===(exception)
23 | case exception
24 | when ::SyntaxError
25 | true
26 | else
27 | WRAPPED_CLASSES.include?(exception.class.to_s)
28 | end
29 | end
30 | end
31 |
32 | def load(string, options = {})
33 | options[:symbol_keys] = options[:symbolize_keys]
34 | ::Oj.load(string, options)
35 | end
36 |
37 | case ::Oj::VERSION
38 | when /\A2\./
39 | def dump(object, options = {})
40 | options[:indent] = 2 if options[:pretty]
41 | options[:indent] = options[:indent].to_i if options[:indent]
42 | ::Oj.dump(object, options)
43 | end
44 | when /\A3\./
45 | PRETTY_STATE_PROTOTYPE = {
46 | indent: " ",
47 | space: " ",
48 | space_before: "",
49 | object_nl: "\n",
50 | array_nl: "\n",
51 | ascii_only: false
52 | }.freeze
53 |
54 | def dump(object, options = {})
55 | options.merge!(PRETTY_STATE_PROTOTYPE.dup) if options.delete(:pretty)
56 | ::Oj.dump(object, options)
57 | end
58 | else
59 | raise "Unsupported Oj version: #{::Oj::VERSION}"
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require "multi_json"
2 | require "rspec"
3 |
4 | RSpec.configure do |config|
5 | config.expect_with :rspec do |c|
6 | c.syntax = :expect
7 | end
8 |
9 | # You must run 'bundle exec rake' for this to work properly
10 | loaded_specs = Gem.loaded_specs
11 |
12 | config.add_setting :java, default: RUBY_PLATFORM == "java"
13 | config.add_setting :gson, default: loaded_specs.key?("gson")
14 | config.add_setting :json, default: loaded_specs.key?("json")
15 | config.add_setting :json_pure, default: loaded_specs.key?("json_pure")
16 | config.add_setting :jrjackson, default: loaded_specs.key?("jrjackson")
17 | config.add_setting :ok_json, default: loaded_specs.key?("ok_json")
18 | config.add_setting :oj, default: loaded_specs.key?("oj")
19 | config.add_setting :yajl, default: loaded_specs.key?("yajl")
20 |
21 | config.filter_run_excluding(:jrjackson) unless config.jrjackson?
22 | config.filter_run_excluding(:json) unless config.json?
23 | config.filter_run_excluding(:json_pure) unless config.json_pure?
24 | config.filter_run_excluding(:ok_json) unless config.ok_json?
25 | config.filter_run_excluding(:oj) unless config.oj?
26 | config.filter_run_excluding(:yajl) unless config.yajl?
27 |
28 | unless config.java?
29 | config.filter_run_excluding(:java)
30 | config.filter_run_excluding(:gson) unless config.gson?
31 | end
32 | end
33 |
34 | def silence_warnings
35 | old_verbose = $VERBOSE
36 | $VERBOSE = nil
37 | yield
38 | ensure
39 | $VERBOSE = old_verbose
40 | end
41 |
42 | def undefine_constants(*consts)
43 | values = {}
44 | consts.each do |const|
45 | if Object.const_defined?(const)
46 | values[const] = Object.const_get(const)
47 | Object.send :remove_const, const
48 | end
49 | end
50 |
51 | yield
52 | ensure
53 | values.each do |const, value|
54 | Object.const_set const, value
55 | end
56 | end
57 |
58 | def break_requirements
59 | requirements = MultiJson::REQUIREMENT_MAP
60 | MultiJson::REQUIREMENT_MAP.each do |adapter, library|
61 | MultiJson::REQUIREMENT_MAP[adapter] = "foo/#{library}"
62 | end
63 |
64 | yield
65 | ensure
66 | requirements.each do |adapter, library|
67 | MultiJson::REQUIREMENT_MAP[adapter] = library
68 | end
69 | end
70 |
71 | def simulate_no_adapters(&block)
72 | break_requirements do
73 | undefine_constants :JSON, :Oj, :Yajl, :Gson, :JrJackson, &block
74 | end
75 | end
76 |
77 | def get_exception(exception_class = StandardError)
78 | yield
79 | rescue exception_class => e
80 | e
81 | end
82 |
83 | def with_default_options
84 | adapter = MultiJson.adapter
85 | adapter.load_options = adapter.dump_options = MultiJson.load_options = MultiJson.dump_options = nil
86 | yield
87 | ensure
88 | adapter.load_options = adapter.dump_options = MultiJson.load_options = MultiJson.dump_options = nil
89 | end
90 |
--------------------------------------------------------------------------------
/spec/shared/options.rb:
--------------------------------------------------------------------------------
1 | shared_examples_for "has options" do |object|
2 | if object.respond_to?(:call)
3 | subject { object.call }
4 | else
5 | subject { object }
6 | end
7 |
8 | describe "dump options" do
9 | before do
10 | subject.dump_options = nil
11 | end
12 |
13 | after do
14 | subject.dump_options = nil
15 | end
16 |
17 | it "returns default options if not set" do
18 | expect(subject.dump_options).to eq(subject.default_dump_options)
19 | end
20 |
21 | it "allows hashes" do
22 | subject.dump_options = {foo: "bar"}
23 | expect(subject.dump_options).to eq(foo: "bar")
24 | end
25 |
26 | it "allows objects that implement #to_hash" do
27 | value = Class.new do
28 | def to_hash
29 | {foo: "bar"}
30 | end
31 | end.new
32 |
33 | subject.dump_options = value
34 | expect(subject.dump_options).to eq(foo: "bar")
35 | end
36 |
37 | it "evaluates lambda returning options (with args)" do
38 | subject.dump_options = ->(a1, a2) { {a1 => a2} }
39 | expect(subject.dump_options("1", "2")).to eq("1" => "2")
40 | end
41 |
42 | it "evaluates lambda returning options (with no args)" do
43 | subject.dump_options = -> { {foo: "bar"} }
44 | expect(subject.dump_options).to eq(foo: "bar")
45 | end
46 |
47 | it "returns empty hash in all other cases" do
48 | subject.dump_options = true
49 | expect(subject.dump_options).to eq(subject.default_dump_options)
50 |
51 | subject.dump_options = false
52 | expect(subject.dump_options).to eq(subject.default_dump_options)
53 |
54 | subject.dump_options = 10
55 | expect(subject.dump_options).to eq(subject.default_dump_options)
56 |
57 | subject.dump_options = nil
58 | expect(subject.dump_options).to eq(subject.default_dump_options)
59 | end
60 | end
61 |
62 | describe "load options" do
63 | before do
64 | subject.load_options = nil
65 | end
66 |
67 | after do
68 | subject.load_options = nil
69 | end
70 |
71 | it "returns default options if not set" do
72 | expect(subject.load_options).to eq(subject.default_load_options)
73 | end
74 |
75 | it "allows hashes" do
76 | subject.load_options = {foo: "bar"}
77 | expect(subject.load_options).to eq(foo: "bar")
78 | end
79 |
80 | it "allows objects that implement #to_hash" do
81 | value = Class.new do
82 | def to_hash
83 | {foo: "bar"}
84 | end
85 | end.new
86 |
87 | subject.load_options = value
88 | expect(subject.load_options).to eq(foo: "bar")
89 | end
90 |
91 | it "evaluates lambda returning options (with args)" do
92 | subject.load_options = ->(a1, a2) { {a1 => a2} }
93 | expect(subject.load_options("1", "2")).to eq("1" => "2")
94 | end
95 |
96 | it "evaluates lambda returning options (with no args)" do
97 | subject.load_options = -> { {foo: "bar"} }
98 | expect(subject.load_options).to eq(foo: "bar")
99 | end
100 |
101 | it "returns empty hash in all other cases" do
102 | subject.load_options = true
103 | expect(subject.load_options).to eq(subject.default_load_options)
104 |
105 | subject.load_options = false
106 | expect(subject.load_options).to eq(subject.default_load_options)
107 |
108 | subject.load_options = 10
109 | expect(subject.load_options).to eq(subject.default_load_options)
110 |
111 | subject.load_options = nil
112 | expect(subject.load_options).to eq(subject.default_load_options)
113 | end
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/lib/multi_json.rb:
--------------------------------------------------------------------------------
1 | require "multi_json/options"
2 | require "multi_json/version"
3 | require "multi_json/adapter_error"
4 | require "multi_json/parse_error"
5 | require "multi_json/options_cache"
6 |
7 | module MultiJson
8 | include Options
9 | extend self
10 |
11 | def default_options=(value)
12 | Kernel.warn "MultiJson.default_options setter is deprecated\nUse MultiJson.load_options and MultiJson.dump_options instead"
13 |
14 | self.load_options = self.dump_options = value
15 | end
16 |
17 | def default_options
18 | Kernel.warn "MultiJson.default_options is deprecated\nUse MultiJson.load_options or MultiJson.dump_options instead"
19 |
20 | load_options
21 | end
22 |
23 | %w[cached_options reset_cached_options!].each do |method_name|
24 | define_method method_name do |*|
25 | Kernel.warn "MultiJson.#{method_name} method is deprecated and no longer used."
26 | end
27 | end
28 |
29 | ALIASES = {"jrjackson" => "jr_jackson"}.freeze
30 |
31 | REQUIREMENT_MAP = {
32 | oj: "oj",
33 | yajl: "yajl",
34 | jr_jackson: "jrjackson",
35 | json_gem: "json/ext",
36 | gson: "gson",
37 | json_pure: "json/pure"
38 | }
39 |
40 | # The default adapter based on what you currently
41 | # have loaded and installed.
42 | def default_adapter
43 | adapter = loaded_adapter || installable_adapter
44 | return adapter if adapter
45 |
46 | Kernel.warn "[WARNING] MultiJson is using the default adapter (ok_json). We recommend loading a different JSON library to improve performance."
47 |
48 | :ok_json
49 | end
50 |
51 | alias_method :default_engine, :default_adapter
52 |
53 | # Get the current adapter class.
54 | def adapter
55 | return @adapter if defined?(@adapter) && @adapter
56 |
57 | use nil # load default adapter
58 |
59 | @adapter
60 | end
61 | alias_method :engine, :adapter
62 |
63 | # Set the JSON parser utilizing a symbol, string, or class.
64 | # Supported by default are:
65 | #
66 | # * :oj
67 | # * :json_gem
68 | # * :json_pure
69 | # * :ok_json
70 | # * :yajl
71 | # * :gson (JRuby only)
72 | # * :jr_jackson (JRuby only)
73 | def use(new_adapter)
74 | @adapter = load_adapter(new_adapter)
75 | ensure
76 | OptionsCache.reset
77 | end
78 | alias_method :adapter=, :use
79 | alias_method :engine=, :use
80 |
81 | def load_adapter(new_adapter)
82 | case new_adapter
83 | when String, Symbol
84 | load_adapter_from_string_name new_adapter.to_s
85 | when NilClass, FalseClass
86 | load_adapter default_adapter
87 | when Class, Module
88 | new_adapter
89 | else
90 | raise ::LoadError, new_adapter
91 | end
92 | rescue ::LoadError => e
93 | raise AdapterError.build(e)
94 | end
95 |
96 | # Decode a JSON string into Ruby.
97 | #
98 | # Options
99 | #
100 | # :symbolize_keys :: If true, will use symbols instead of strings for the keys.
101 | # :adapter :: If set, the selected adapter will be used for this call.
102 | def load(string, options = {})
103 | adapter = current_adapter(options)
104 | begin
105 | adapter.load(string, options)
106 | rescue adapter::ParseError => e
107 | raise ParseError.build(e, string)
108 | end
109 | end
110 | alias_method :decode, :load
111 |
112 | def current_adapter(options = {})
113 | if (new_adapter = options[:adapter])
114 | load_adapter(new_adapter)
115 | else
116 | adapter
117 | end
118 | end
119 |
120 | # Encodes a Ruby object as JSON.
121 | def dump(object, options = {})
122 | current_adapter(options).dump(object, options)
123 | end
124 | alias_method :encode, :dump
125 |
126 | # Executes passed block using specified adapter.
127 | def with_adapter(new_adapter)
128 | old_adapter = adapter
129 | self.adapter = new_adapter
130 | yield
131 | ensure
132 | self.adapter = old_adapter
133 | end
134 | alias_method :with_engine, :with_adapter
135 |
136 | private
137 |
138 | # Checks for already loaded adapters and returns the first match
139 | def loaded_adapter
140 | return :oj if defined?(::Oj)
141 | return :yajl if defined?(::Yajl)
142 | return :jr_jackson if defined?(::JrJackson)
143 | return :json_gem if defined?(::JSON::Ext::Parser)
144 | return :gson if defined?(::Gson)
145 |
146 | nil
147 | end
148 |
149 | # Attempts to load and return the first installable adapter
150 | def installable_adapter
151 | REQUIREMENT_MAP.each do |adapter, library|
152 | require library
153 | return adapter
154 | rescue ::LoadError
155 | next
156 | end
157 | nil
158 | end
159 |
160 | def load_adapter_from_string_name(name)
161 | name = ALIASES.fetch(name, name)
162 | require "multi_json/adapters/#{name.downcase}"
163 | klass_name = name.to_s.split("_").map(&:capitalize) * ""
164 | MultiJson::Adapters.const_get(klass_name)
165 | end
166 | end
167 |
--------------------------------------------------------------------------------
/spec/multi_json_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "shared/options"
3 |
4 | RSpec.describe MultiJson do
5 | let(:config) { RSpec.configuration }
6 |
7 | before(:all) do
8 | # make sure all available libs are required
9 | MultiJson::REQUIREMENT_MAP.each_value do |library|
10 | require library
11 | rescue LoadError
12 | next
13 | end
14 | end
15 |
16 | context "when no other json implementations are available" do
17 | around do |example|
18 | simulate_no_adapters { example.call }
19 | end
20 |
21 | it "defaults to ok_json if no other json implementions are available" do
22 | silence_warnings do
23 | expect(described_class.default_adapter).to eq(:ok_json)
24 | end
25 | end
26 |
27 | it "prints a warning" do
28 | expect(Kernel).to receive(:warn).with(/warning/i)
29 | described_class.default_adapter
30 | end
31 | end
32 |
33 | context "when JSON pure is already loaded" do
34 | it "default_adapter tries to require each adapter in turn and does not assume :json_gem is already loaded" do
35 | require "json/pure"
36 | expect(JSON::JSON_LOADED).to be_truthy
37 |
38 | undefine_constants :Oj, :Yajl, :Gson, :JrJackson do
39 | # simulate that the json_gem is not loaded
40 | ext = defined?(JSON::Ext::Parser) ? JSON::Ext.send(:remove_const, :Parser) : nil
41 | begin
42 | expect(described_class).to receive(:require)
43 | described_class.default_adapter
44 | ensure
45 | stub_const("JSON::Ext::Parser", ext) if ext
46 | end
47 | end
48 | end
49 | end
50 |
51 | context "when caching" do
52 | before { described_class.use adapter }
53 |
54 | let(:adapter) { MultiJson::Adapters::JsonGem }
55 | let(:json_string) { '{"abc":"def"}' }
56 |
57 | it "busts caches on global options change" do
58 | described_class.load_options = {symbolize_keys: true}
59 | expect(described_class.load(json_string)).to eq(abc: "def")
60 | described_class.load_options = nil
61 | expect(described_class.load(json_string)).to eq("abc" => "def")
62 | end
63 |
64 | it "busts caches on per-adapter options change" do
65 | adapter.load_options = {symbolize_keys: true}
66 | expect(described_class.load(json_string)).to eq(abc: "def")
67 | adapter.load_options = nil
68 | expect(described_class.load(json_string)).to eq("abc" => "def")
69 | end
70 | end
71 |
72 | context "automatic adapter loading" do
73 | before do
74 | described_class.send(:remove_instance_variable, :@adapter) if described_class.instance_variable_defined?(:@adapter)
75 | end
76 |
77 | it "defaults to the best available gem" do
78 | if config.java? && config.jrjackson?
79 | expect(described_class.adapter.to_s).to eq("MultiJson::Adapters::JrJackson")
80 | elsif config.java? && config.json?
81 | expect(described_class.adapter.to_s).to eq("MultiJson::Adapters::JsonGem")
82 | else
83 | expect(described_class.adapter.to_s).to eq("MultiJson::Adapters::Oj")
84 | end
85 | end
86 |
87 | it "looks for adapter even if @adapter variable is nil" do
88 | allow(described_class).to receive(:default_adapter).and_return(:ok_json)
89 | expect(described_class.adapter).to eq(MultiJson::Adapters::OkJson)
90 | end
91 | end
92 |
93 | it "is settable via a symbol" do
94 | described_class.use :json_gem
95 | expect(described_class.adapter).to eq(MultiJson::Adapters::JsonGem)
96 | end
97 |
98 | it "is settable via a case-insensitive string" do
99 | described_class.use "Json_Gem"
100 | expect(described_class.adapter).to eq(MultiJson::Adapters::JsonGem)
101 | end
102 |
103 | it "is settable via a class" do
104 | adapter = Class.new
105 | described_class.use adapter
106 | expect(described_class.adapter).to eq(adapter)
107 | end
108 |
109 | it "is settable via a module" do
110 | adapter = Module.new
111 | described_class.use adapter
112 | expect(described_class.adapter).to eq(adapter)
113 | end
114 |
115 | it "throws AdapterError on bad input" do
116 | expect { described_class.use "bad adapter" }.to raise_error(MultiJson::AdapterError, /bad adapter/)
117 | end
118 |
119 | it "gives access to original error when raising AdapterError" do
120 | exception = get_exception(MultiJson::AdapterError) { described_class.use "foobar" }
121 | expect(exception.cause).to be_instance_of(LoadError)
122 | expect(exception.message).to include("-- multi_json/adapters/foobar")
123 | expect(exception.message).to include("Did not recognize your adapter specification")
124 | end
125 |
126 | context "with one-shot parser" do
127 | it "uses the defined parser just for the call" do
128 | expect(MultiJson::Adapters::JsonPure).to receive(:dump).once.and_return("dump_something")
129 | expect(MultiJson::Adapters::JsonPure).to receive(:load).once.and_return("load_something")
130 | described_class.use :json_gem
131 | expect(described_class.dump("", adapter: :json_pure)).to eq("dump_something")
132 | expect(described_class.load("", adapter: :json_pure)).to eq("load_something")
133 | expect(described_class.adapter).to eq(MultiJson::Adapters::JsonGem)
134 | end
135 | end
136 |
137 | it "can set adapter for a block" do
138 | described_class.use :ok_json
139 | described_class.with_adapter(:json_pure) do
140 | described_class.with_engine(:json_gem) do
141 | expect(described_class.adapter).to eq(MultiJson::Adapters::JsonGem)
142 | end
143 | expect(described_class.adapter).to eq(MultiJson::Adapters::JsonPure)
144 | end
145 | expect(described_class.adapter).to eq(MultiJson::Adapters::OkJson)
146 | end
147 |
148 | it "JSON gem does not create symbols on parse" do
149 | skip "java based implementations" if config.java?
150 |
151 | described_class.with_engine(:json_gem) do
152 | described_class.load('{"json_class":"ZOMG"}')
153 |
154 | expect do
155 | described_class.load('{"json_class":"OMG"}')
156 | end.not_to(change { Symbol.all_symbols.count })
157 | end
158 | end
159 |
160 | describe "default options" do
161 | after(:all) { described_class.load_options = described_class.dump_options = nil }
162 |
163 | it "is deprecated" do
164 | expect(Kernel).to receive(:warn).with(/deprecated/i)
165 | silence_warnings { described_class.default_options = {foo: "bar"} }
166 | end
167 |
168 | it "sets both load and dump options" do
169 | expect(described_class).to receive(:dump_options=).with({foo: "bar"})
170 | expect(described_class).to receive(:load_options=).with({foo: "bar"})
171 | silence_warnings { described_class.default_options = {foo: "bar"} }
172 | end
173 | end
174 |
175 | it_behaves_like "has options", described_class
176 |
177 | describe "aliases", :jrjackson do
178 | describe "jrjackson" do
179 | it "allows jrjackson alias as symbol" do
180 | expect { described_class.use :jrjackson }.not_to raise_error
181 | expect(described_class.adapter).to eq(MultiJson::Adapters::JrJackson)
182 | end
183 |
184 | it "allows jrjackson alias as string" do
185 | expect { described_class.use "jrjackson" }.not_to raise_error
186 | expect(described_class.adapter).to eq(MultiJson::Adapters::JrJackson)
187 | end
188 | end
189 | end
190 | end
191 |
--------------------------------------------------------------------------------
/spec/shared/adapter.rb:
--------------------------------------------------------------------------------
1 | require "shared/options"
2 | require "stringio"
3 |
4 | shared_examples_for "an adapter" do |adapter|
5 | before { MultiJson.use adapter }
6 |
7 | it_behaves_like "has options", adapter
8 |
9 | it "does not modify argument hashes" do
10 | options = {symbolize_keys: true, pretty: false, adapter: :ok_json}
11 | expect { MultiJson.load("{}", options) }.not_to(change { options })
12 | expect { MultiJson.dump([42], options) }.not_to(change { options })
13 | end
14 |
15 | describe ".dump" do
16 | let(:json_pure) { Kernel.const_get("MultiJson::Adapters::JsonPure") rescue nil }
17 | describe "#dump_options" do
18 | before { MultiJson.dump_options = MultiJson.adapter.dump_options = {} }
19 |
20 | after do
21 | MultiJson.dump(1, fizz: "buzz")
22 | MultiJson.dump_options = MultiJson.adapter.dump_options = nil
23 | end
24 |
25 | it "respects global dump options" do
26 | MultiJson.dump_options = {foo: "bar"}
27 | expect(MultiJson.dump_options).to eq({foo: "bar"})
28 | expect(MultiJson.adapter.instance).to receive(:dump).with(1, {foo: "bar", fizz: "buzz"})
29 | end
30 |
31 | it "respects per-adapter dump options" do
32 | MultiJson.adapter.dump_options = {foo: "bar"}
33 | expect(MultiJson.adapter.dump_options).to eq({foo: "bar"})
34 | expect(MultiJson.adapter.instance).to receive(:dump).with(1, {foo: "bar", fizz: "buzz"})
35 | end
36 |
37 | it "adapter-specific are overridden by global options" do
38 | MultiJson.adapter.dump_options = {foo: "foo"}
39 | MultiJson.dump_options = {foo: "bar"}
40 | expect(MultiJson.adapter.dump_options).to eq({foo: "foo"})
41 | expect(MultiJson.dump_options).to eq({foo: "bar"})
42 | expect(MultiJson.adapter.instance).to receive(:dump).with(1, {foo: "bar", fizz: "buzz"})
43 | end
44 | end
45 |
46 | it "writes decodable JSON" do
47 | examples = [
48 | {"abc" => "def"},
49 | [],
50 | 1,
51 | "2",
52 | true,
53 | false,
54 | nil
55 | ]
56 |
57 | examples.each do |example|
58 | expect(MultiJson.load(MultiJson.dump(example))).to eq(example)
59 | end
60 | end
61 |
62 | it "dumps time in correct format" do
63 | time = Time.at(1_355_218_745).utc
64 |
65 | dumped_json = MultiJson.dump(time)
66 | expected = "2012-12-11 09:39:05 UTC"
67 | expect(MultiJson.load(dumped_json)).to eq(expected)
68 | end
69 |
70 | it "dumps symbol and fixnum keys as strings" do
71 | [
72 | [
73 | {foo: {bar: "baz"}},
74 | {"foo" => {"bar" => "baz"}}
75 | ],
76 | [
77 | [{foo: {bar: "baz"}}],
78 | [{"foo" => {"bar" => "baz"}}]
79 | ],
80 | [
81 | {foo: [{bar: "baz"}]},
82 | {"foo" => [{"bar" => "baz"}]}
83 | ],
84 | [
85 | {1 => {2 => {3 => "bar"}}},
86 | {"1" => {"2" => {"3" => "bar"}}}
87 | ]
88 | ].each do |example, expected|
89 | dumped_json = MultiJson.dump(example)
90 | expect(MultiJson.load(dumped_json)).to eq(expected)
91 | end
92 | end
93 |
94 | it "dumps rootless JSON" do
95 | expect(MultiJson.dump("random rootless string")).to eq('"random rootless string"')
96 | expect(MultiJson.dump(123)).to eq("123")
97 | end
98 |
99 | it "passes options to the adapter" do
100 | expect(MultiJson.adapter).to receive(:dump).with("foo", {bar: :baz})
101 | MultiJson.dump("foo", {bar: :baz})
102 | end
103 |
104 | it "dumps custom objects that implement to_json" do
105 | pending "not supported" if adapter.name == "MultiJson::Adapters::Gson"
106 | klass = Class.new do
107 | def to_json(*)
108 | '"foobar"'
109 | end
110 | end
111 | expect(MultiJson.dump(klass.new)).to eq('"foobar"')
112 | end
113 |
114 | it "allows to dump JSON values" do
115 | expect(MultiJson.dump(42)).to eq("42")
116 | end
117 |
118 | it "allows to dump JSON with UTF-8 characters" do
119 | expect(MultiJson.dump("color" => "żółć")).to eq('{"color":"żółć"}')
120 | end
121 | end
122 |
123 | describe ".load" do
124 | describe "#load_options" do
125 | before { MultiJson.load_options = MultiJson.adapter.load_options = {} }
126 |
127 | after do
128 | MultiJson.load("1", fizz: "buzz")
129 | MultiJson.load_options = MultiJson.adapter.load_options = nil
130 | end
131 |
132 | it "respects global load options" do
133 | MultiJson.load_options = {foo: "bar"}
134 | expect(MultiJson.load_options).to eq({foo: "bar"})
135 | expect(MultiJson.adapter.instance).to receive(:load).with("1", {foo: "bar", fizz: "buzz"})
136 | end
137 |
138 | it "respects per-adapter load options" do
139 | MultiJson.adapter.load_options = {foo: "bar"}
140 | expect(MultiJson.adapter.load_options).to eq({foo: "bar"})
141 | expect(MultiJson.adapter.instance).to receive(:load).with("1", {foo: "bar", fizz: "buzz"})
142 | end
143 |
144 | it "adapter-specific are overridden by global options" do
145 | MultiJson.adapter.load_options = {foo: "foo"}
146 | MultiJson.load_options = {foo: "bar"}
147 | expect(MultiJson.adapter.load_options).to eq({foo: "foo"})
148 | expect(MultiJson.load_options).to eq({foo: "bar"})
149 | expect(MultiJson.adapter.instance).to receive(:load).with("1", {foo: "bar", fizz: "buzz"})
150 | end
151 | end
152 |
153 | it "does not modify input" do
154 | input = %(\n\n {"foo":"bar"} \n\n\t)
155 | expect do
156 | MultiJson.load(input)
157 | end.not_to(change { input })
158 | end
159 |
160 | it "does not modify input encoding" do
161 | input = "[123]"
162 | input.force_encoding("iso-8859-1")
163 |
164 | expect do
165 | MultiJson.load(input)
166 | end.not_to(change { input.encoding })
167 | end
168 |
169 | it "properly loads valid JSON" do
170 | expect(MultiJson.load('{"abc":"def"}')).to eq("abc" => "def")
171 | end
172 |
173 | examples = [nil, '{"abc"}', " ", "\t\t\t", "\n", StringIO.new("")]
174 | #
175 | # GSON bug: https://github.com/avsej/gson.rb/issues/3
176 | examples << "\x82\xAC\xEF" unless adapter.name.include?("Gson")
177 |
178 | examples.each do |input|
179 | it "raises MultiJson::ParseError on invalid input: #{input.inspect}" do
180 | expect { MultiJson.load(input) }.to raise_error(MultiJson::ParseError)
181 | end
182 | end
183 |
184 | it "raises MultiJson::ParseError with data on invalid JSON" do
185 | data = "{invalid}"
186 | exception = get_exception(MultiJson::ParseError) { MultiJson.load data }
187 | expect(exception.data).to eq(data)
188 | expect(exception.cause).to match(adapter::ParseError)
189 | end
190 |
191 | it "catches MultiJson::DecodeError for legacy support" do
192 | data = "{invalid}"
193 | exception = get_exception(MultiJson::DecodeError) { MultiJson.load data }
194 | expect(exception.data).to eq(data)
195 | expect(exception.cause).to match(adapter::ParseError)
196 | end
197 |
198 | it "catches MultiJson::LoadError for legacy support" do
199 | data = "{invalid}"
200 | exception = get_exception(MultiJson::LoadError) { MultiJson.load data }
201 | expect(exception.data).to eq(data)
202 | expect(exception.cause).to match(adapter::ParseError)
203 | end
204 |
205 | it "stringifys symbol keys when encoding" do
206 | dumped_json = MultiJson.dump(a: 1, b: {c: 2})
207 | loaded_json = MultiJson.load(dumped_json)
208 | expect(loaded_json).to eq("a" => 1, "b" => {"c" => 2})
209 | end
210 |
211 | it "properly loads valid JSON in StringIOs" do
212 | json = StringIO.new('{"abc":"def"}')
213 | expect(MultiJson.load(json)).to eq("abc" => "def")
214 | end
215 |
216 | it "allows for symbolization of keys" do
217 | [
218 | [
219 | '{"abc":{"def":"hgi"}}',
220 | {abc: {def: "hgi"}}
221 | ],
222 | [
223 | '[{"abc":{"def":"hgi"}}]',
224 | [{abc: {def: "hgi"}}]
225 | ],
226 | [
227 | '{"abc":[{"def":"hgi"}]}',
228 | {abc: [{def: "hgi"}]}
229 | ]
230 | ].each do |example, expected|
231 | expect(MultiJson.load(example, symbolize_keys: true)).to eq(expected)
232 | end
233 | end
234 |
235 | it "allows to load JSON values" do
236 | expect(MultiJson.load("42")).to eq(42)
237 | end
238 |
239 | it "allows to load JSON with UTF-8 characters" do
240 | expect(MultiJson.load('{"color":"żółć"}')).to eq("color" => "żółć")
241 | end
242 | end
243 | end
244 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | 1.15.0
2 | ------
3 |
4 | * [Improve detection of json_gem adapter](https://github.com/intridea/multi_json/commit/62d54019b17ebf83b28c8deb871a02a122e7d9cf)
5 |
6 | 1.14.1
7 | ------
8 |
9 | * [Fix a warning in Ruby 2.7](https://github.com/intridea/multi_json/commit/26a94ab8c78a394cc237e2ea292c1de4f6ed30d7)
10 |
11 | 1.14.0
12 | ------
13 |
14 | * [Support Oj 3.x gem](https://github.com/intridea/multi_json/commit/5d8febdbebc428882811b90d514f3628617a61d5)
15 |
16 | 1.13.1
17 | ------
18 |
19 | * [Fix missing stdlib set dependency in oj adapter](https://github.com/intridea/multi_json/commit/c4ff66e7bee6fb4f45e54429813d7fada1c152b8)
20 |
21 | 1.13.0
22 | -----
23 |
24 | * [Make Oj adapter handle JSON::ParseError correctly](https://github.com/intridea/multi_json/commit/275e3ffd8169797c510d23d9ef5b8b07e64c3b42)
25 |
26 | 1.12.2
27 | ------
28 |
29 | * [Renew gem certificate](https://github.com/intridea/multi_json/commit/57922d898c6eb587cc9a28ba5724c11e81724700)
30 |
31 | 1.12.1
32 | ------
33 |
34 | * [Prevent memory leak in OptionsCache](https://github.com/intridea/multi_json/commit/aa7498199ad272f3d4a13750d7c568a66047e2ee)
35 |
36 | 1.12.0
37 | ------
38 |
39 | * [Introduce global options cache to improve peroformance](https://github.com/intridea/multi_json/commit/7aaef2a1bc2b83c95e4208b12dad5d1d87ff20a6)
40 |
41 | 1.11.2
42 | ------
43 |
44 | * [Only pass one argument to JrJackson when two is not supported](https://github.com/intridea/multi_json/commit/e798fa517c817fc706982d3f3c61129b6651d601)
45 |
46 | 1.11.1
47 | ------
48 |
49 | * [Dump method passes options throught for JrJackson adapter](https://github.com/intridea/multi_json/commit/3c730fd12135c3e7bf212f878958004908f13909)
50 |
51 | 1.11.0
52 | ------
53 |
54 | * [Make all adapters read IO object before load](https://github.com/intridea/multi_json/commit/167f559e18d4efee05e1f160a2661d16dbb215d4)
55 |
56 | 1.10.1
57 | ------
58 | * [Explicitly require stringio for Gson adapter](https://github.com/intridea/multi_json/commit/623ec8142d4a212fa0db763bb71295789a119929)
59 | * [Do not read StringIO object before passing it to JrJackson](https://github.com/intridea/multi_json/commit/a6dc935df08e7b3d5d701fbb9298384c96df0fde)
60 |
61 | 1.10.0
62 | ------
63 | * [Performance tweaks](https://github.com/intridea/multi_json/commit/58724acfed31866d079eaafb1cd824e341ade287)
64 |
65 | 1.9.3
66 | -----
67 | * [Convert indent option to Fixnum before passing to Oj](https://github.com/intridea/multi_json/commit/826fc5535b863b74fc9f981dfdda3e26f1ee4e5b)
68 |
69 | 1.9.2
70 | -----
71 | * [Enable use_to_json option for Oj adapter by default](https://github.com/intridea/multi_json/commit/76a4aaf697b10bbabd5d535d83cf1149efcfe5c7)
72 |
73 | 1.9.1
74 | -----
75 | * [Remove unused LoadError file](https://github.com/intridea/multi_json/commit/65dedd84d59baeefc25c477fedf0bbe85e7ce2cd)
76 |
77 | 1.9.0
78 | ----
79 | * [Rename LoadError to ParseError](https://github.com/intridea/multi_json/commit/4abb98fe3a90b2a7b3d1594515c8a06042b4a27d)
80 | * [Adapter load failure throws AdapterError instead of ArgumentError](https://github.com/intridea/multi_json/commit/4da612b617bd932bb6fa1cc4c43210327f98f271)
81 |
82 | 1.8.4
83 | -----
84 | * [Make Gson adapter explicitly read StringIO object](https://github.com/intridea/multi_json/commit/b58b498747ff6e94f41488c971b2a30a98760ef2)
85 |
86 | 1.8.3
87 | -----
88 | * [Make JrJackson explicitly read StringIO objects](https://github.com/intridea/multi_json/commit/e1f162d5b668e5e4db5afa175361a601a8aa2b05)
89 | * [Prevent calling #downcase on alias symbols](https://github.com/intridea/multi_json/commit/c1cf075453ce0110f7decc4f906444b1233bb67c)
90 |
91 | 1.8.2
92 | -----
93 | * [Downcase adapter string name for OS compatibility](https://github.com/intridea/multi_json/commit/b8e15a032247a63f1410d21a18add05035f3fa66)
94 |
95 | 1.8.1
96 | -----
97 | * [Let the adapter handle strings with invalid encoding](https://github.com/intridea/multi_json/commit/6af2bf87b89f44eabf2ae9ca96779febc65ea94b)
98 |
99 | 1.8.0
100 | -----
101 | * [Raise MultiJson::LoadError on blank input](https://github.com/intridea/multi_json/commit/c44f9c928bb25fe672246ad394b3e5b991be32e6)
102 |
103 | 1.7.9
104 | -----
105 | * [Explicitly require json gem code even when constant is defined](https://github.com/intridea/multi_json/commit/36f7906c66477eb4b55b7afeaa3684b6db69eff2)
106 |
107 | 1.7.8
108 | -----
109 | * [Reorder JrJackson before json_gem](https://github.com/intridea/multi_json/commit/315b6e460b6e4dcdb6c82e04e4be8ee975d395da)
110 | * [Update vendored OkJson to version 43](https://github.com/intridea/multi_json/commit/99a6b662f6ef4036e3ee94d7eb547fa72fb2ab50)
111 |
112 | 1.7.7
113 | -----
114 | * [Fix options caching issues](https://github.com/intridea/multi_json/commit/a3f14c3661688c5927638fa6088c7b46a67e875e)
115 |
116 | 1.7.6
117 | -----
118 | * [Bring back MultiJson::VERSION constant](https://github.com/intridea/multi_json/commit/31b990c2725e6673bf8ce57540fe66b57a751a72)
119 |
120 | 1.7.5
121 | -----
122 | * [Fix warning '*' interpreted as argument prefix](https://github.com/intridea/multi_json/commit/b698962c7f64430222a1f06430669706a47aff89)
123 | * [Remove stdlib warning](https://github.com/intridea/multi_json/commit/d06eec6b7996ac8b4ff0e2229efd835379b0c30f)
124 |
125 | 1.7.4
126 | -----
127 | * [Cache options for better performance](https://github.com/intridea/multi_json/commit/8a26ee93140c4bed36194ed9fb887a1b6919257b)
128 |
129 | 1.7.3
130 | -----
131 | * [Require json/ext to ensure extension version gets loaded for json_gem](https://github.com/intridea/multi_json/commit/942686f7e8597418c6f90ee69e1d45242fac07b1)
132 | * [Rename JrJackson](https://github.com/intridea/multi_json/commit/078de7ba8b6035343c3e96b4767549e9ec43369a)
133 | * [Prefer JrJackson to JSON gem if present](https://github.com/intridea/multi_json/commit/af8bd9799a66855f04b3aff1c488485950cec7bf)
134 | * [Print a warning if outdated gem versions are used](https://github.com/intridea/multi_json/commit/e7438e7ba2be0236cfa24c2bb9ad40ee821286d1)
135 | * [Loosen required_rubygems_version for compatibility with Ubuntu 10.04](https://github.com/intridea/multi_json/commit/59fad014e8fe41dbc6f09485ea0dc21fc42fd7a7)
136 |
137 | 1.7.2
138 | -----
139 | * [Rename Jrjackson adapter to JrJackson](https://github.com/intridea/multi_json/commit/b36dc915fc0e6548cbad06b5db6f520e040c9c8b)
140 | * [Implement jrjackson -> jr_jackson alias for back-compatability](https://github.com/intridea/multi_json/commit/aa50ab8b7bb646b8b75d5d65dfeadae8248a4f10)
141 | * [Update vendored OkJson module](https://github.com/intridea/multi_json/commit/30a3f474e17dd86a697c3fab04f468d1a4fd69d7)
142 |
143 | 1.7.1
144 | -----
145 | * [Fix capitalization of JrJackson class](https://github.com/intridea/multi_json/commit/5373a5e38c647f02427a0477cb8e0e0dafad1b8d)
146 |
147 | 1.7.0
148 | -----
149 | * [Add load_options/dump_options to MultiJson](https://github.com/intridea/multi_json/commit/a153956be6b0df06ea1705ce3c1ff0b5b0e27ea5)
150 | * [MultiJson does not modify arguments](https://github.com/intridea/multi_json/commit/58525b01c4c2f6635ba2ac13d6fd987b79f3962f)
151 | * [Enable quirks_mode by default for json_gem/json_pure adapters](https://github.com/intridea/multi_json/commit/1fd4e6635c436515b7d7d5a0bee4548de8571520)
152 | * [Add JrJackson adapter](https://github.com/intridea/multi_json/commit/4dd86fa96300aaaf6d762578b9b31ea82adb056d)
153 | * [Raise ArgumentError on bad adapter input](https://github.com/intridea/multi_json/commit/911a3756bdff2cb5ac06497da3fa3e72199cb7ad)
154 |
155 | 1.6.1
156 | -----
157 | * [Revert "Use JSON.generate instead of #to_json"](https://github.com/intridea/multi_json/issues/86)
158 |
159 | 1.6.0
160 | -----
161 | * [Add gson.rb support](https://github.com/intridea/multi_json/pull/71)
162 | * [Add MultiJson.default_options](https://github.com/intridea/multi_json/pull/70)
163 | * [Add MultiJson.with_adapter](https://github.com/intridea/multi_json/pull/67)
164 | * [Stringify all possible keys for ok_json](https://github.com/intridea/multi_json/pull/66)
165 | * [Use JSON.generate instead of #to_json](https://github.com/intridea/multi_json/issues/73)
166 | * [Alias `MultiJson::DecodeError` to `MultiJson::LoadError`](https://github.com/intridea/multi_json/pull/79)
167 |
168 | 1.5.1
169 | -----
170 | * [Do not allow Oj or JSON to create symbols by searching for classes](https://github.com/intridea/multi_json/commit/193e28cf4dc61b6e7b7b7d80f06f74c76df65c41)
171 |
172 | 1.5.0
173 | -----
174 | * [Add `MultiJson.with_adapter` method](https://github.com/intridea/multi_json/commit/d14c5d28cae96557a0421298621b9499e1f28104)
175 | * [Stringify all possible keys for `ok_json`](https://github.com/intridea/multi_json/commit/73998074058e1e58c557ffa7b9541d486d6041fa)
176 |
177 | 1.4.0
178 | -----
179 | * [Allow `load`/`dump` of JSON fragments](https://github.com/intridea/multi_json/commit/707aae7d48d39c85b38febbd2c210ba87f6e4a36)
180 |
181 | 1.3.7
182 | -----
183 | * [Fix rescue clause for MagLev](https://github.com/intridea/multi_json/commit/39abdf50199828c50e85b2ce8f8ba31fcbbc9332)
184 | * [Remove unnecessary check for string version of options key](https://github.com/intridea/multi_json/commit/660101b70e962b3c007d0b90d45944fa47d13ec4)
185 | * [Explicitly set default adapter when adapter is set to `nil` or `false`](https://github.com/intridea/multi_json/commit/a9e587d5a63eafb4baee9fb211265e4dd96a26bc)
186 | * [Fix Oj `ParseError` mapping for Oj 1.4.0](https://github.com/intridea/multi_json/commit/7d9045338cc9029401c16f3c409d54ce97f275e2)
187 |
188 | 1.3.6
189 | -----
190 | * [Allow adapter-specific options to be passed through to Oj](https://github.com/intridea/multi_json/commit/d0e5feeebcba0bc69400dd203a295f5c30971223)
191 |
192 | 1.3.5
193 | -----
194 | * [Add pretty support to Oj adapter](https://github.com/intridea/multi_json/commit/0c8f75f03020c53bcf4c6be258faf433d24b2c2b)
195 |
196 | 1.3.4
197 | -----
198 | * [Use `class << self` instead of `module_function` to create aliases](https://github.com/intridea/multi_json/commit/ba1451c4c48baa297e049889be241a424cb05980)
199 |
200 | 1.3.3
201 | -----
202 | * [Remove deprecation warnings](https://github.com/intridea/multi_json/commit/36b524e71544eb0186826a891bcc03b2820a008f)
203 |
204 | 1.3.2
205 | -----
206 | * [Add ability to use adapter per call](https://github.com/intridea/multi_json/commit/106bbec469d5d0a832bfa31fffcb8c0f0cdc9bd3)
207 | * [Add and deprecate `default_engine` method](https://github.com/intridea/multi_json/commit/fc3df0c7a3e2ab9ce0c2c7e7617a4da97dd13f6e)
208 |
209 | 1.3.1
210 | -----
211 | * [Only warn once for each instance a deprecated method is called](https://github.com/intridea/multi_json/commit/e21d6eb7da74b3f283995c1d27d5880e75f0ae84)
212 |
213 | 1.3.0
214 | -----
215 | * [Implement `load`/`dump`; deprecate `decode`/`encode`](https://github.com/intridea/multi_json/commit/e90fd6cb1b0293eb0c73c2f4eb0f7a1764370216)
216 | * [Rename engines to adapters](https://github.com/intridea/multi_json/commit/ae7fd144a7949a9c221dcaa446196ec23db908df)
217 |
218 | 1.2.0
219 | -----
220 | * [Add support for Oj](https://github.com/intridea/multi_json/commit/acd06b233edabe6c44f226873db7b49dab560c60)
221 |
222 | 1.1.0
223 | -----
224 | * [`NSJSONSerialization` support for MacRuby](https://github.com/intridea/multi_json/commit/f862e2fc966cac8867fe7da3997fc76e8a6cf5d4)
225 |
226 | 1.0.4
227 | -----
228 | * [Set data context to `DecodeError` exception](https://github.com/intridea/multi_json/commit/19ddafd44029c6681f66fae2a0f6eabfd0f85176)
229 | * [Allow `ok_json` to fallback to `to_json`](https://github.com/intridea/multi_json/commit/c157240b1193b283d06d1bd4d4b5b06bcf3761f8)
230 | * [Add warning when using `ok_json`](https://github.com/intridea/multi_json/commit/dd4b68810c84f826fb98f9713bfb29ab96888d57)
231 | * [Options can be passed to an engine on encode](https://github.com/intridea/multi_json/commit/e0a7ff5d5ff621ffccc61617ed8aeec5816e81f7)
232 |
233 | 1.0.3
234 | -----
235 | * [`Array` support for `stringify_keys`](https://github.com/intridea/multi_json/commit/644d1c5c7c7f6a27663b11668527b346094e38b9)
236 | * [`Array` support for `symbolize_keys`](https://github.com/intridea/multi_json/commit/c885377d47a2aa39cb0d971fea78db2d2fa479a7)
237 |
238 | 1.0.2
239 | -----
240 | * [Allow encoding of rootless JSON when `ok_json` is used](https://github.com/intridea/multi_json/commit/d1cde7de97cb0f6152aef8daf14037521cdce8c6)
241 |
242 | 1.0.1
243 | -----
244 | * [Correct an issue with `ok_json` not being returned as the default engine](https://github.com/intridea/multi_json/commit/d33c141619c54cccd770199694da8fd1bd8f449d)
245 |
246 | 1.0.0
247 | -----
248 | * [Remove `ActiveSupport::JSON` support](https://github.com/intridea/multi_json/commit/c2f4140141d785a24b3f56e58811b0e561b37f6a)
249 | * [Fix `@engine` ivar warning](https://github.com/intridea/multi_json/commit/3b978a8995721a8dffedc3b75a7f49e5494ec553)
250 | * [Only `rescue` from parsing errors during decoding, not any `StandardError`](https://github.com/intridea/multi_json/commit/391d00b5e85294d42d41347605d8d46b4a7f66cc)
251 | * [Rename `okjson` engine and vendored lib to `ok_json`](https://github.com/intridea/multi_json/commit/5bd1afc977a8208ddb0443e1d57cb79665c019f1)
252 | * [Add `StringIO` support to `json` gem and `ok_json`](https://github.com/intridea/multi_json/commit/1706b11568db7f50af451fce5f4d679aeb3bbe8f)
253 |
254 | 0.0.5
255 | -----
256 | * [Trap all JSON decoding errors; raise `MultiJson::DecodeError`](https://github.com/intridea/multi_json/commit/dea9a1aef6dd1212aa1e5a37ab1669f9b045b732)
257 |
258 | 0.0.4
259 | -----
260 | * [Fix default_engine check for `json` gem](https://github.com/intridea/multi_json/commit/caced0c4e8c795922a109ebc00c3c4fa8635bed8)
261 | * [Make requirement mapper an `Array` to preserve order in Ruby versions < 1.9](https://github.com/intridea/multi_json/commit/526f5f29a42131574a088ad9bbb43d7f48439b2c)
262 |
263 | 0.0.3
264 | -----
265 | * [Improve defaulting and documentation](https://github.com/sferik/twitter/commit/3a0e41b9e4b0909201045fa47704b78c9d949b73)
266 |
267 | 0.0.2
268 | -----
269 |
270 | * [Rename to `multi_json`](https://github.com/sferik/twitter/commit/461ab89ce071c8c9fabfc183581e0ec523788b62)
271 |
272 | 0.0.1
273 | -----
274 |
275 | * [Initial commit](https://github.com/sferik/twitter/commit/518c21ab299c500527491e6c049ab2229e22a805)
276 |
--------------------------------------------------------------------------------
/lib/multi_json/vendor/okjson.rb:
--------------------------------------------------------------------------------
1 | # Copyright 2011, 2012 Keith Rarick
2 | #
3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
4 | # of this software and associated documentation files (the "Software"), to deal
5 | # in the Software without restriction, including without limitation the rights
6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | # copies of the Software, and to permit persons to whom the Software is
8 | # furnished to do so, subject to the following conditions:
9 | #
10 | # The above copyright notice and this permission notice shall be included in
11 | # all copies or substantial portions of the Software.
12 | #
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | # THE SOFTWARE.
20 |
21 | # See https://github.com/kr/okjson for updates.
22 |
23 | require "stringio"
24 |
25 | module MultiJson
26 | # Some parts adapted from
27 | # https://golang.org/src/encoding/json/decode.go and
28 | # https://golang.org/src/unicode/utf8/utf8.go
29 | module OkJson
30 | Upstream = "45"
31 | extend self
32 |
33 | # Decodes a json document in string s and
34 | # returns the corresponding ruby value.
35 | # String s must be valid UTF-8. If you have
36 | # a string in some other encoding, convert
37 | # it first.
38 | #
39 | # String values in the resulting structure
40 | # will be UTF-8.
41 | def decode(s)
42 | ts = lex(s)
43 | v, ts = textparse(ts)
44 | raise Error, "trailing garbage" unless ts.empty?
45 |
46 | v
47 | end
48 |
49 | # Encodes x into a json text. It may contain only
50 | # Array, Hash, String, Numeric, true, false, nil.
51 | # (Note, this list excludes Symbol.)
52 | # X itself must be an Array or a Hash.
53 | # No other value can be encoded, and an error will
54 | # be raised if x contains any other value, such as
55 | # Nan, Infinity, Symbol, and Proc, or if a Hash key
56 | # is not a String.
57 | # Strings contained in x must be valid UTF-8.
58 | def encode(x)
59 | case x
60 | when Hash then objenc(x)
61 | when Array then arrenc(x)
62 | else
63 | raise Error, "root value must be an Array or a Hash"
64 | end
65 | end
66 |
67 | def valenc(x)
68 | case x
69 | when Hash then objenc(x)
70 | when Array then arrenc(x)
71 | when String then strenc(x)
72 | when Numeric then numenc(x)
73 | when true then "true"
74 | when false then "false"
75 | when nil then "null"
76 | else
77 | raise Error, "cannot encode #{x.class}: #{x.inspect}" unless x.respond_to?(:to_json)
78 |
79 | x.to_json
80 | end
81 | end
82 |
83 | private
84 |
85 | # Parses a "json text" in the sense of RFC 4627.
86 | # Returns the parsed value and any trailing tokens.
87 | # Note: this is almost the same as valparse,
88 | # except that it does not accept atomic values.
89 | def textparse(ts)
90 | raise Error, "empty" if ts.length <= 0
91 |
92 | typ, _, val = ts[0]
93 | case typ
94 | when "{" then objparse(ts)
95 | when "[" then arrparse(ts)
96 | else
97 | raise Error, "unexpected #{val.inspect}"
98 | end
99 | end
100 |
101 | # Parses a "value" in the sense of RFC 4627.
102 | # Returns the parsed value and any trailing tokens.
103 | def valparse(ts)
104 | raise Error, "empty" if ts.length <= 0
105 |
106 | typ, _, val = ts[0]
107 | case typ
108 | when "{" then objparse(ts)
109 | when "[" then arrparse(ts)
110 | when :val, :str then [val, ts[1..]]
111 | else
112 | raise Error, "unexpected #{val.inspect}"
113 | end
114 | end
115 |
116 | # Parses an "object" in the sense of RFC 4627.
117 | # Returns the parsed value and any trailing tokens.
118 | def objparse(ts)
119 | ts = eat("{", ts)
120 | obj = {}
121 |
122 | return obj, ts[1..] if ts[0][0] == "}"
123 |
124 | k, v, ts = pairparse(ts)
125 | obj[k] = v
126 |
127 | return obj, ts[1..] if ts[0][0] == "}"
128 |
129 | loop do
130 | ts = eat(",", ts)
131 |
132 | k, v, ts = pairparse(ts)
133 | obj[k] = v
134 |
135 | return obj, ts[1..] if ts[0][0] == "}"
136 | end
137 | end
138 |
139 | # Parses a "member" in the sense of RFC 4627.
140 | # Returns the parsed values and any trailing tokens.
141 | def pairparse(ts)
142 | (typ, _, k) = ts[0]
143 | ts = ts[1..]
144 | raise Error, "unexpected #{k.inspect}" if typ != :str
145 |
146 | ts = eat(":", ts)
147 | v, ts = valparse(ts)
148 | [k, v, ts]
149 | end
150 |
151 | # Parses an "array" in the sense of RFC 4627.
152 | # Returns the parsed value and any trailing tokens.
153 | def arrparse(ts)
154 | ts = eat("[", ts)
155 | arr = []
156 |
157 | return arr, ts[1..] if ts[0][0] == "]"
158 |
159 | v, ts = valparse(ts)
160 | arr << v
161 |
162 | return arr, ts[1..] if ts[0][0] == "]"
163 |
164 | loop do
165 | ts = eat(",", ts)
166 |
167 | v, ts = valparse(ts)
168 | arr << v
169 |
170 | return arr, ts[1..] if ts[0][0] == "]"
171 | end
172 | end
173 |
174 | def eat(typ, ts)
175 | raise Error, "expected #{typ} (got #{ts[0].inspect})" if ts[0][0] != typ
176 |
177 | ts[1..]
178 | end
179 |
180 | # Scans s and returns a list of json tokens,
181 | # excluding white space (as defined in RFC 4627).
182 | def lex(s)
183 | ts = []
184 | until s.empty?
185 | typ, lexeme, val = tok(s)
186 | raise Error, "invalid character at #{s[0, 10].inspect}" if typ.nil?
187 |
188 | ts << [typ, lexeme, val] if typ != :space
189 | s = s[lexeme.length..]
190 | end
191 | ts
192 | end
193 |
194 | # Scans the first token in s and
195 | # returns a 3-element list, or nil
196 | # if s does not begin with a valid token.
197 | #
198 | # The first list element is one of
199 | # '{', '}', ':', ',', '[', ']',
200 | # :val, :str, and :space.
201 | #
202 | # The second element is the lexeme.
203 | #
204 | # The third element is the value of the
205 | # token for :val and :str, otherwise
206 | # it is the lexeme.
207 | def tok(s)
208 | case s[0]
209 | when "{" then ["{", s[0, 1], s[0, 1]]
210 | when "}" then ["}", s[0, 1], s[0, 1]]
211 | when ":" then [":", s[0, 1], s[0, 1]]
212 | when "," then [",", s[0, 1], s[0, 1]]
213 | when "[" then ["[", s[0, 1], s[0, 1]]
214 | when "]" then ["]", s[0, 1], s[0, 1]]
215 | when "n" then nulltok(s)
216 | when "t" then truetok(s)
217 | when "f" then falsetok(s)
218 | when '"' then strtok(s)
219 | when Spc, "\t", "\n", "\r" then [:space, s[0, 1], s[0, 1]]
220 | else
221 | numtok(s)
222 | end
223 | end
224 |
225 | def nulltok(s) = (s[0, 4] == "null") ? [:val, "null", nil] : []
226 |
227 | def truetok(s) = (s[0, 4] == "true") ? [:val, "true", true] : []
228 |
229 | def falsetok(s) = (s[0, 5] == "false") ? [:val, "false", false] : []
230 |
231 | def numtok(s)
232 | m = /(-?(?:[1-9][0-9]+|[0-9]))([.][0-9]+)?([eE][+-]?[0-9]+)?/.match(s)
233 | if m&.begin(0)&.zero?
234 | if !m[2] && !m[3]
235 | [:val, m[0], Integer(m[0])]
236 | elsif m[2]
237 | [:val, m[0], Float(m[0])]
238 | else
239 | [:val, m[0], Integer(m[1]) * (10**m[3][1..].to_i(10))]
240 | end
241 | else
242 | []
243 | end
244 | end
245 |
246 | def strtok(s)
247 | m = %r{"([^"\\]|\\["/\\bfnrt]|\\u[0-9a-fA-F]{4})*"}.match(s)
248 | raise Error, "invalid string literal at #{abbrev(s)}" unless m
249 |
250 | [:str, m[0], unquote(m[0])]
251 | end
252 |
253 | def abbrev(s)
254 | t = s[0, 10]
255 | p = t["`"]
256 | t = t[0, p] if p
257 | t += "..." if t.length < s.length
258 | "`" + t + "`"
259 | end
260 |
261 | # Converts a quoted json string literal q into a UTF-8-encoded string.
262 | # The rules are different than for Ruby, so we cannot use eval.
263 | # Unquote will raise an error if q contains control characters.
264 | def unquote(q)
265 | q = q[1...-1]
266 | a = q.dup # allocate a big enough string
267 | # In ruby >= 1.9, a[w] is a codepoint, not a byte.
268 | a.force_encoding("UTF-8") if rubydoesenc?
269 | r = 0
270 | w = 0
271 | while r < q.length
272 | c = q[r]
273 | if c == "\\"
274 | r += 1
275 | raise Error, "string literal ends with a \"\\\": \"#{q}\"" if r >= q.length
276 |
277 | case q[r]
278 | when '"', "\\", "/", "'"
279 | a[w] = q[r]
280 | r += 1
281 | w += 1
282 | when "b", "f", "n", "r", "t"
283 | a[w] = Unesc[q[r]]
284 | r += 1
285 | w += 1
286 | when "u"
287 | r += 1
288 | uchar = begin
289 | hexdec4(q[r, 4])
290 | rescue RuntimeError => e
291 | raise Error, "invalid escape sequence \\u#{q[r, 4]}: #{e}"
292 | end
293 | r += 4
294 | if surrogate?(uchar) && (q.length >= r + 6)
295 | uchar1 = hexdec4(q[r + 2, 4])
296 | uchar = subst(uchar, uchar1)
297 | if uchar != Ucharerr
298 | # A valid pair; consume.
299 | r += 6
300 | end
301 | end
302 | if rubydoesenc?
303 | a[w] = "" << uchar
304 | w += 1
305 | else
306 | w += ucharenc(a, w, uchar)
307 | end
308 | else
309 | raise Error, "invalid escape char #{q[r]} in \"#{q}\""
310 | end
311 | elsif c == '"' || c < Spc
312 | raise Error, "invalid character in string literal \"#{q}\""
313 | else
314 | # Copy anything else byte-for-byte.
315 | # Valid UTF-8 will remain valid UTF-8.
316 | # Invalid UTF-8 will remain invalid UTF-8.
317 | # In ruby >= 1.9, c is a codepoint, not a byte,
318 | # in which case this is still what we want.
319 | a[w] = c
320 | r += 1
321 | w += 1
322 | end
323 | end
324 | a[0, w]
325 | end
326 |
327 | # Encodes unicode character u as UTF-8
328 | # bytes in string a at position i.
329 | # Returns the number of bytes written.
330 | def ucharenc(a, i, u)
331 | if u <= Uchar1max
332 | a[i] = (u & 0xff).chr
333 | 1
334 | elsif u <= Uchar2max
335 | a[i + 0] = (Utag2 | ((u >> 6) & 0xff)).chr
336 | a[i + 1] = (Utagx | (u & Umaskx)).chr
337 | 2
338 | elsif u <= Uchar3max
339 | a[i + 0] = (Utag3 | ((u >> 12) & 0xff)).chr
340 | a[i + 1] = (Utagx | ((u >> 6) & Umaskx)).chr
341 | a[i + 2] = (Utagx | (u & Umaskx)).chr
342 | 3
343 | else
344 | a[i + 0] = (Utag4 | ((u >> 18) & 0xff)).chr
345 | a[i + 1] = (Utagx | ((u >> 12) & Umaskx)).chr
346 | a[i + 2] = (Utagx | ((u >> 6) & Umaskx)).chr
347 | a[i + 3] = (Utagx | (u & Umaskx)).chr
348 | 4
349 | end
350 | end
351 |
352 | def hexdec4(s)
353 | raise Error, "short" if s.length != 4
354 |
355 | (nibble(s[0]) << 12) | (nibble(s[1]) << 8) | (nibble(s[2]) << 4) | nibble(s[3])
356 | end
357 |
358 | def subst(u1, u2)
359 | return ((u1 - Usurr1) << 10) | ((u2 - Usurr2) + Usurrself) if u1 >= Usurr1 && u1 < Usurr2 && u2 >= Usurr2 && u2 < Usurr3
360 |
361 | Ucharerr
362 | end
363 |
364 | def surrogate?(u)
365 | u >= Usurr1 && u < Usurr3
366 | end
367 |
368 | def nibble(c)
369 | if c >= "0" && c <= "9" then c.ord - "0".ord
370 | elsif c >= "a" && c <= "z" then c.ord - "a".ord + 10
371 | elsif c >= "A" && c <= "Z" then c.ord - "A".ord + 10
372 | else
373 | raise Error, "invalid hex code #{c}"
374 | end
375 | end
376 |
377 | def objenc(x)
378 | "{" + x.map { |k, v| keyenc(k) + ":" + valenc(v) }.join(",") + "}"
379 | end
380 |
381 | def arrenc(a)
382 | "[" + a.map { |x| valenc(x) }.join(",") + "]"
383 | end
384 |
385 | def keyenc(k)
386 | case k
387 | when String then strenc(k)
388 | else
389 | raise Error, "Hash key is not a string: #{k.inspect}"
390 | end
391 | end
392 |
393 | def strenc(s)
394 | t = StringIO.new
395 | t.putc('"')
396 | r = 0
397 |
398 | while r < s.length
399 | case s[r]
400 | when '"' then t.print('\\"')
401 | when "\\" then t.print("\\\\")
402 | when "\b" then t.print('\\b')
403 | when "\f" then t.print('\\f')
404 | when "\n" then t.print('\\n')
405 | when "\r" then t.print('\\r')
406 | when "\t" then t.print('\\t')
407 | else
408 | c = s[r]
409 | # In ruby >= 1.9, s[r] is a codepoint, not a byte.
410 | if rubydoesenc?
411 | begin
412 | # c.ord will raise an error if c is invalid UTF-8
413 | c = "\\u%04x" % [c.ord] if c.ord < Spc.ord
414 | t.write(c)
415 | rescue
416 | t.write(Ustrerr)
417 | end
418 | elsif c < Spc
419 | t.write("\\u%04x" % c)
420 | elsif c >= Spc && c <= "~"
421 | t.putc(c)
422 | else
423 | n = ucharcopy(t, s, r) # ensure valid UTF-8 output
424 | r += n - 1 # r is incremented below
425 | end
426 | end
427 | r += 1
428 | end
429 | t.putc('"')
430 | t.string
431 | end
432 |
433 | def numenc(x)
434 | raise Error, "Numeric cannot be represented: #{x}" if (x.nan? || x.infinite? rescue false)
435 |
436 | x.to_s
437 | end
438 |
439 | # Copies the valid UTF-8 bytes of a single character
440 | # from string s at position i to I/O object t, and
441 | # returns the number of bytes copied.
442 | # If no valid UTF-8 char exists at position i,
443 | # ucharcopy writes Ustrerr and returns 1.
444 | def ucharcopy(t, s, i)
445 | n = s.length - i
446 | raise Utf8Error if n < 1
447 |
448 | c0 = s[i].ord
449 |
450 | # 1-byte, 7-bit sequence?
451 | if c0 < Utagx
452 | t.putc(c0)
453 | return 1
454 | end
455 |
456 | raise Utf8Error if c0 < Utag2 # unexpected continuation byte?
457 |
458 | raise Utf8Error if n < 2 # need continuation byte
459 |
460 | c1 = s[i + 1].ord
461 | raise Utf8Error if c1 < Utagx || c1 >= Utag2
462 |
463 | # 2-byte, 11-bit sequence?
464 | if c0 < Utag3
465 | raise Utf8Error if (((c0 & Umask2) << 6) | (c1 & Umaskx)) <= Uchar1max
466 |
467 | t.putc(c0)
468 | t.putc(c1)
469 | return 2
470 | end
471 |
472 | # need second continuation byte
473 | raise Utf8Error if n < 3
474 |
475 | c2 = s[i + 2].ord
476 | raise Utf8Error if c2 < Utagx || c2 >= Utag2
477 |
478 | # 3-byte, 16-bit sequence?
479 | if c0 < Utag4
480 | u = ((c0 & Umask3) << 12) | ((c1 & Umaskx) << 6) | (c2 & Umaskx)
481 | raise Utf8Error if u <= Uchar2max
482 |
483 | t.putc(c0)
484 | t.putc(c1)
485 | t.putc(c2)
486 | return 3
487 | end
488 |
489 | # need third continuation byte
490 | raise Utf8Error if n < 4
491 |
492 | c3 = s[i + 3].ord
493 | raise Utf8Error if c3 < Utagx || c3 >= Utag2
494 |
495 | # 4-byte, 21-bit sequence?
496 | if c0 < Utag5
497 | u = ((c0 & Umask4) << 18) | ((c1 & Umaskx) << 12) | ((c2 & Umaskx) << 6) | (c3 & Umaskx)
498 | raise Utf8Error if u <= Uchar3max
499 |
500 | t.putc(c0)
501 | t.putc(c1)
502 | t.putc(c2)
503 | t.putc(c3)
504 | return 4
505 | end
506 |
507 | raise Utf8Error
508 | rescue Utf8Error
509 | t.write(Ustrerr)
510 | 1
511 | end
512 |
513 | def rubydoesenc?
514 | ::String.method_defined?(:force_encoding)
515 | end
516 |
517 | class Utf8Error < ::StandardError
518 | end
519 |
520 | class Error < ::StandardError
521 | end
522 |
523 | Utagx = 0b1000_0000
524 | Utag2 = 0b1100_0000
525 | Utag3 = 0b1110_0000
526 | Utag4 = 0b1111_0000
527 | Utag5 = 0b1111_1000
528 | Umaskx = 0b0011_1111
529 | Umask2 = 0b0001_1111
530 | Umask3 = 0b0000_1111
531 | Umask4 = 0b0000_0111
532 | Uchar1max = (1 << 7) - 1
533 | Uchar2max = (1 << 11) - 1
534 | Uchar3max = (1 << 16) - 1
535 | Ucharerr = 0xFFFD # unicode "replacement char"
536 | Ustrerr = "\xef\xbf\xbd" # unicode "replacement char"
537 | Usurrself = 0x10000
538 | Usurr1 = 0xd800
539 | Usurr2 = 0xdc00
540 | Usurr3 = 0xe000
541 |
542 | Spc = " "[0]
543 | Unesc = {"b" => "\b", "f" => "\f", "n" => "\n", "r" => "\r", "t" => "\t"}
544 | end
545 | end
546 |
--------------------------------------------------------------------------------