├── .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 | --------------------------------------------------------------------------------