diff --git a/CHANGELOG.md b/CHANGELOG.md index a419758..0f30a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [1.2.0] - [unreleased] + +### Added +- Allow to pass adapter specific options + ## [1.1.0] - 2024-02-17 ### Added diff --git a/README.md b/README.md index 94cd8a4..063ccb3 100644 --- a/README.md +++ b/README.md @@ -208,8 +208,8 @@ person.to_yaml ### Converting TOML to object To use TOML with Shale you have to set adapter you want to use. -Out of the box Shale suports [Tomlib](https://github.com/kgiszczak/tomlib). -It also comes with adapter for [toml-rb](https://github.com/emancu/toml-rb) if you prefer that. +It comes with adapters for [Tomlib](https://github.com/kgiszczak/tomlib) and +[toml-rb](https://github.com/emancu/toml-rb). For details see [Adapters](#adapters) section. To set it, first make sure Tomlib gem is installed: @@ -221,8 +221,8 @@ $ gem install tomlib then setup adapter: ```ruby -require 'tomlib' -Shale.toml_adapter = Tomlib +require 'sahle/adapter/tomlib' +Shale.toml_adapter = Shale::Adapter::Tomlib # Alternatively if you'd like to use toml-rb, use: require 'shale/adapter/toml_rb' @@ -1085,6 +1085,29 @@ Person.to_csv(people, headers: true, col_sep: '|') # James|Sixpack ``` +Most adapters accept options specific to them. Eg. if you want to be able to work +with NaN values in JSON: + +```ruby +class Person + attribute :age, Shale::Type::Float +end + +person = Person.from_jsom('{"age": NaN}', allow_nan: true) + +# => +# +# # + +Person.to_json(person, allow_nan: true) + +# => +# +# { +# "age": NaN +# } +``` + ### Using custom models By default Shale combines mapper and model into one class. If you want to use your own classes diff --git a/lib/shale.rb b/lib/shale.rb index 9e7fb45..b8d5308 100644 --- a/lib/shale.rb +++ b/lib/shale.rb @@ -80,16 +80,17 @@ class << self # @api public attr_writer :yaml_adapter - # TOML adapter accessor. + # TOML adapter accessor. Available adapters are Shale::Adapter::Tomlib + # and Shale::Adapter::TomRB # - # @param [@see Shale::Adapter::TomlRB] adapter + # @param [@see Shale::Adapter::Tomlib] adapter # # @example setting adapter - # Shale.toml_adapter = Shale::Adapter::TomlRB + # Shale.toml_adapter = Shale::Adapter::Tomlib # # @example getting adapter # Shale.toml_adapter - # # => Shale::Adapter::TomlRB + # # => Shale::Adapter::Tomlib # # @api public attr_accessor :toml_adapter diff --git a/lib/shale/adapter/json.rb b/lib/shale/adapter/json.rb index d2c6250..89c09ca 100644 --- a/lib/shale/adapter/json.rb +++ b/lib/shale/adapter/json.rb @@ -11,27 +11,30 @@ class JSON # Parse JSON into Hash # # @param [String] json JSON document + # @param [Hash] options # # @return [Hash] # # @api private - def self.load(json) - ::JSON.parse(json) + def self.load(json, **options) + ::JSON.parse(json, **options) end # Serialize Hash into JSON # # @param [Hash] obj Hash object - # @param [true, false] pretty + # @param [Hash] options # # @return [String] # # @api private - def self.dump(obj, pretty: false) - if pretty - ::JSON.pretty_generate(obj) + def self.dump(obj, **options) + json_options = options.except(:pretty) + + if options[:pretty] + ::JSON.pretty_generate(obj, **json_options) else - ::JSON.generate(obj) + ::JSON.generate(obj, **json_options) end end end diff --git a/lib/shale/adapter/toml_rb.rb b/lib/shale/adapter/toml_rb.rb index 23cb099..d93fde2 100644 --- a/lib/shale/adapter/toml_rb.rb +++ b/lib/shale/adapter/toml_rb.rb @@ -4,29 +4,31 @@ module Shale module Adapter - # TOML adapter + # TomlRB adapter # # @api public class TomlRB # Parse TOML into Hash # # @param [String] toml TOML document + # @param [Hash] options # # @return [Hash] # # @api private - def self.load(toml) + def self.load(toml, **_options) ::TomlRB.parse(toml) end # Serialize Hash into TOML # # @param [Hash] obj Hash object + # @param [Hash] options # # @return [String] # # @api private - def self.dump(obj) + def self.dump(obj, **_options) ::TomlRB.dump(obj) end end diff --git a/lib/shale/adapter/tomlib.rb b/lib/shale/adapter/tomlib.rb new file mode 100644 index 0000000..a193291 --- /dev/null +++ b/lib/shale/adapter/tomlib.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'tomlib' + +module Shale + module Adapter + # Tomlib adapter + # + # @api public + class Tomlib + # Parse TOML into Hash + # + # @param [String] toml TOML document + # @param [Hash] options + # + # @return [Hash] + # + # @api private + def self.load(toml, **_options) + ::Tomlib.load(toml) + end + + # Serialize Hash into TOML + # + # @param [Hash] obj Hash object + # @param [Hash] options + # + # @return [String] + # + # @api private + def self.dump(obj, **options) + ::Tomlib.dump(obj, **options) + end + end + end +end diff --git a/lib/shale/error.rb b/lib/shale/error.rb index 329adc6..f11397e 100644 --- a/lib/shale/error.rb +++ b/lib/shale/error.rb @@ -9,7 +9,8 @@ module Shale # To use Tomlib: # Make sure tomlib is installed eg. execute: gem install tomlib - Shale.toml_adapter = Tomlib + require 'shale/adapter/tomlib' + Shale.toml_adapter = Shale::Adapter::Tomlib # To use toml-rb: # Make sure toml-rb is installed eg. execute: gem install toml-rb diff --git a/lib/shale/type/complex.rb b/lib/shale/type/complex.rb index e290750..6dcc539 100644 --- a/lib/shale/type/complex.rb +++ b/lib/shale/type/complex.rb @@ -257,13 +257,14 @@ def as_#{format}(instance, only: nil, except: nil, context: nil) # @param [Array] only # @param [Array] except # @param [any] context + # @param [Hash] json_options # # @return [model instance] # # @api public - def from_json(json, only: nil, except: nil, context: nil) + def from_json(json, only: nil, except: nil, context: nil, **json_options) of_json( - Shale.json_adapter.load(json), + Shale.json_adapter.load(json, **json_options), only: only, except: except, context: context @@ -277,14 +278,15 @@ def from_json(json, only: nil, except: nil, context: nil) # @param [Array] except # @param [any] context # @param [true, false] pretty + # @param [Hash] json_options # # @return [String] # # @api public - def to_json(instance, only: nil, except: nil, context: nil, pretty: false) + def to_json(instance, only: nil, except: nil, context: nil, pretty: false, **json_options) Shale.json_adapter.dump( as_json(instance, only: only, except: except, context: context), - pretty: pretty + **json_options.merge(pretty: pretty) ) end @@ -294,13 +296,14 @@ def to_json(instance, only: nil, except: nil, context: nil, pretty: false) # @param [Array] only # @param [Array] except # @param [any] context + # @param [Hash] yaml_options # # @return [model instance] # # @api public - def from_yaml(yaml, only: nil, except: nil, context: nil) + def from_yaml(yaml, only: nil, except: nil, context: nil, **yaml_options) of_yaml( - Shale.yaml_adapter.load(yaml), + Shale.yaml_adapter.load(yaml, **yaml_options), only: only, except: except, context: context @@ -313,13 +316,15 @@ def from_yaml(yaml, only: nil, except: nil, context: nil) # @param [Array] only # @param [Array] except # @param [any] context + # @param [Hash] yaml_options # # @return [String] # # @api public - def to_yaml(instance, only: nil, except: nil, context: nil) + def to_yaml(instance, only: nil, except: nil, context: nil, **yaml_options) Shale.yaml_adapter.dump( - as_yaml(instance, only: only, except: except, context: context) + as_yaml(instance, only: only, except: except, context: context), + **yaml_options ) end @@ -329,14 +334,15 @@ def to_yaml(instance, only: nil, except: nil, context: nil) # @param [Array] only # @param [Array] except # @param [any] context + # @param [Hash] toml_options # # @return [model instance] # # @api public - def from_toml(toml, only: nil, except: nil, context: nil) + def from_toml(toml, only: nil, except: nil, context: nil, **toml_options) validate_toml_adapter of_toml( - Shale.toml_adapter.load(toml), + Shale.toml_adapter.load(toml, **toml_options), only: only, except: except, context: context @@ -349,14 +355,16 @@ def from_toml(toml, only: nil, except: nil, context: nil) # @param [Array] only # @param [Array] except # @param [any] context + # @param [Hash] toml_options # # @return [String] # # @api public - def to_toml(instance, only: nil, except: nil, context: nil) + def to_toml(instance, only: nil, except: nil, context: nil, **toml_options) validate_toml_adapter Shale.toml_adapter.dump( - as_toml(instance, only: only, except: except, context: context) + as_toml(instance, only: only, except: except, context: context), + **toml_options ) end @@ -1000,17 +1008,19 @@ def to_hash(only: nil, except: nil, context: nil) # @param [Array] except # @param [any] context # @param [true, false] pretty + # @param [Hash] json_options # # @return [String] # # @api public - def to_json(only: nil, except: nil, context: nil, pretty: false) + def to_json(only: nil, except: nil, context: nil, pretty: false, **json_options) self.class.to_json( self, only: only, except: except, context: context, - pretty: pretty + pretty: pretty, + **json_options ) end @@ -1019,12 +1029,13 @@ def to_json(only: nil, except: nil, context: nil, pretty: false) # @param [Array] only # @param [Array] except # @param [any] context + # @param [Hash] yaml_options # # @return [String] # # @api public - def to_yaml(only: nil, except: nil, context: nil) - self.class.to_yaml(self, only: only, except: except, context: context) + def to_yaml(only: nil, except: nil, context: nil, **yaml_options) + self.class.to_yaml(self, only: only, except: except, context: context, **yaml_options) end # Convert Object to TOML @@ -1032,12 +1043,13 @@ def to_yaml(only: nil, except: nil, context: nil) # @param [Array] only # @param [Array] except # @param [any] context + # @param [Hash] toml_options # # @return [String] # # @api public - def to_toml(only: nil, except: nil, context: nil) - self.class.to_toml(self, only: only, except: except, context: context) + def to_toml(only: nil, except: nil, context: nil, **toml_options) + self.class.to_toml(self, only: only, except: except, context: context, **toml_options) end # Convert Object to CSV @@ -1045,6 +1057,8 @@ def to_toml(only: nil, except: nil, context: nil) # @param [Array] only # @param [Array] except # @param [any] context + # @param [true, false] headers + # @param [Hash] csv_options # # @return [String] # diff --git a/spec/shale/adapter/json_spec.rb b/spec/shale/adapter/json_spec.rb index 4afb463..c5bd203 100644 --- a/spec/shale/adapter/json_spec.rb +++ b/spec/shale/adapter/json_spec.rb @@ -8,6 +8,11 @@ doc = described_class.load('{"foo": "bar"}') expect(doc).to eq({ 'foo' => 'bar' }) end + + it 'accepts extra options' do + doc = described_class.load('{"foo": NaN}', allow_nan: true) + expect(doc['foo'].nan?).to eq(true) + end end describe '.dump' do @@ -30,5 +35,10 @@ expect(json).to eq(expected) end end + + it 'accepts extra options' do + json = described_class.dump({ 'foo' => Float::NAN }, allow_nan: true) + expect(json).to eq('{"foo":NaN}') + end end end diff --git a/spec/shale/adapter/toml_rb_spec.rb b/spec/shale/adapter/toml_rb_spec.rb index d959255..f16e8f2 100644 --- a/spec/shale/adapter/toml_rb_spec.rb +++ b/spec/shale/adapter/toml_rb_spec.rb @@ -8,11 +8,21 @@ doc = described_class.load('foo = "bar"') expect(doc).to eq({ 'foo' => 'bar' }) end + + it 'accepts extra options' do + doc = described_class.load('foo = "bar"', foo: 'bar') + expect(doc).to eq({ 'foo' => 'bar' }) + end end describe '.dump' do it 'generates TOML document' do - toml = described_class.dump('foo' => 'bar') + toml = described_class.dump({ 'foo' => 'bar' }) + expect(toml).to eq("foo = \"bar\"\n") + end + + it 'accepts extra options' do + toml = described_class.dump({ 'foo' => 'bar' }, foo: 'bar') expect(toml).to eq("foo = \"bar\"\n") end end diff --git a/spec/shale/adapter/tomlib_spec.rb b/spec/shale/adapter/tomlib_spec.rb new file mode 100644 index 0000000..3edb521 --- /dev/null +++ b/spec/shale/adapter/tomlib_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'shale/adapter/tomlib' + +RSpec.describe Shale::Adapter::Tomlib do + describe '.load' do + it 'parses TOML document' do + doc = described_class.load('foo = "bar"') + expect(doc).to eq({ 'foo' => 'bar' }) + end + + it 'accepts extra options' do + doc = described_class.load('foo = "bar"', foo: 'bar') + expect(doc).to eq({ 'foo' => 'bar' }) + end + end + + describe '.dump' do + it 'generates TOML document' do + toml = described_class.dump({ 'foo' => 'bar' }) + expect(toml).to eq("foo = \"bar\"\n") + end + + it 'accepts extra options' do + toml = described_class.dump({ 'foo' => 'bar' }, indent: false) + expect(toml).to eq("foo = \"bar\"\n") + end + end +end diff --git a/spec/shale/type/complex_spec/with_default_mapping_spec.rb b/spec/shale/type/complex_spec/with_default_mapping_spec.rb index 328b913..9b6f85d 100644 --- a/spec/shale/type/complex_spec/with_default_mapping_spec.rb +++ b/spec/shale/type/complex_spec/with_default_mapping_spec.rb @@ -3,7 +3,7 @@ require 'shale' require 'shale/adapter/rexml' require 'shale/adapter/csv' -require 'tomlib' +require 'shale/adapter/tomlib' module ComplexSpec__DefaultMapping # rubocop:disable Naming/ClassAndModuleCamelCase class Child < Shale::Mapper @@ -27,7 +27,7 @@ class ParentCsv < Shale::Mapper before(:each) do Shale.json_adapter = Shale::Adapter::JSON Shale.yaml_adapter = YAML - Shale.toml_adapter = Tomlib + Shale.toml_adapter = Shale::Adapter::Tomlib Shale.csv_adapter = Shale::Adapter::CSV Shale.xml_adapter = Shale::Adapter::REXML end @@ -178,6 +178,17 @@ class ParentCsv < Shale::Mapper expect(instance[i].child.two).to eq(%w[foo bar baz]) end end + + context 'with params' do + let(:json) do + '{ "one": NaN }' + end + + it 'maps json to object' do + instance = mapper.from_json(json, allow_nan: true) + expect(instance.one).to eq('NaN') + end + end end describe '.to_json' do @@ -224,6 +235,13 @@ class ParentCsv < Shale::Mapper expect(mapper.to_json([instance, instance], pretty: true)).to eq(json_collection) end end + + context 'with other params' do + it 'converts objects to json' do + instance = mapper.new(one: 'foo', two: nil) + expect(instance.to_json(allow_nan: true)).to eq('{"one":"foo"}') + end + end end end @@ -293,6 +311,11 @@ class ParentCsv < Shale::Mapper expect(instance[i].child.two).to eq(%w[foo bar baz]) end end + + it 'accepts extra options' do + instance = mapper.from_yaml(yaml, aliases: true) + expect(instance.one).to eq('foo') + end end describe '.to_yaml' do @@ -315,6 +338,11 @@ class ParentCsv < Shale::Mapper expect(mapper.to_yaml([instance, instance])).to eq(yaml_collection) end + + it 'accepts extra options' do + instance = mapper.new(one: 'foo', two: nil) + expect(instance.to_yaml(aliases: true)).to eq("---\none: foo\n") + end end end @@ -350,6 +378,11 @@ class ParentCsv < Shale::Mapper expect(instance.child.one).to eq('foo') expect(instance.child.two).to eq(%w[foo bar baz]) end + + it 'accepts extra options' do + instance = mapper.from_toml(toml, foo: :bar) + expect(instance.one).to eq('foo') + end end end @@ -374,6 +407,16 @@ class ParentCsv < Shale::Mapper expect(instance.to_toml).to eq(toml) end + + it 'accepts extra options' do + instance = mapper.new( + one: 'foo', + two: %w[foo bar baz], + child: child_class.new(one: 'foo', two: %w[foo bar baz]) + ) + + expect(instance.to_toml(indent: false)).to eq(toml) + end end end end @@ -382,6 +425,22 @@ class ParentCsv < Shale::Mapper let(:mapper) { ComplexSpec__DefaultMapping::ParentCsv } describe '.from_csv' do + context 'when adapter is not set' do + let(:csv) do + <<~DOC + foo,bar + DOC + end + + it 'raises an error' do + Shale.csv_adapter = nil + + expect do + mapper.from_csv(csv) + end.to raise_error(Shale::AdapterError, /CSV Adapter is not set/) + end + end + context 'without params' do let(:csv) do <<~DOC @@ -481,6 +540,22 @@ class ParentCsv < Shale::Mapper end describe '.to_csv' do + context 'when adapter is not set' do + let(:csv) do + <<~DOC + foo,bar + DOC + end + + it 'raises an error' do + Shale.csv_adapter = nil + + expect do + mapper.to_csv(csv) + end.to raise_error(Shale::AdapterError, /CSV Adapter is not set/) + end + end + context 'without params' do let(:csv) do <<~DOC