Skip to content

Commit

Permalink
Adds BigDecimal mapper (#49)
Browse files Browse the repository at this point in the history
* Adds Decimal mapper type

Shale Mappers generated from XSD's will use the Decimal mapper type to preserve the precision
when reading decimal values from XML documents instead of converting them to floats.

When the mapper is used for mapping other formats than XML the internal BigDecimal values are converted to floats
as they would otherwise be written as strings in output documents.

It should be possible to modify this in the future if the underlying serializers can handle BigDecimals.

* Updates tests to handle the Decimal mapper

The "maps xxx to object" test are extracted to a shared example to reduce repetition.
But of course there were differences in the tests so the check for the value_collection was
extracted as well.
  • Loading branch information
kjeldahl authored Jan 14, 2025
1 parent 7b998cc commit bd7c878
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 291 deletions.
2 changes: 2 additions & 0 deletions lib/shale.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# frozen_string_literal: true

require 'yaml'
require 'bigdecimal'

require_relative 'shale/mapper'
require_relative 'shale/adapter/json'
require_relative 'shale/type'
require_relative 'shale/type/boolean'
require_relative 'shale/type/date'
require_relative 'shale/type/decimal'
require_relative 'shale/type/float'
require_relative 'shale/type/integer'
require_relative 'shale/type/string'
Expand Down
21 changes: 21 additions & 0 deletions lib/shale/schema/compiler/decimal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module Shale
module Schema
module Compiler
# Class that maps Schema type to Shale Decimal type
#
# @api private
class Decimal
# Return name of the Shale type
#
# @return [String]
#
# @api private
def name
'Shale::Type::Decimal'
end
end
end
end
end
10 changes: 9 additions & 1 deletion lib/shale/schema/xml_compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require_relative '../error'
require_relative 'compiler/boolean'
require_relative 'compiler/date'
require_relative 'compiler/decimal'
require_relative 'compiler/float'
require_relative 'compiler/integer'
require_relative 'compiler/string'
Expand Down Expand Up @@ -103,11 +104,16 @@ class XMLCompiler
# XML Schema "float" types
# @api private
XS_TYPE_FLOAT = [
"#{XS_NAMESPACE_URI}:decimal",
"#{XS_NAMESPACE_URI}:float",
"#{XS_NAMESPACE_URI}:double",
].freeze

# XML Schema "decimal" types
# @api private
XS_TYPE_DECIMAL = [
"#{XS_NAMESPACE_URI}:decimal"
].freeze

# XML Schema "integer" types
# @api private
XS_TYPE_INTEGER = [
Expand Down Expand Up @@ -612,6 +618,8 @@ def infer_type_from_xs_type(type, namespaces)
Compiler::Time.new
elsif XS_TYPE_STRING.include?(type)
Compiler::String.new
elsif XS_TYPE_DECIMAL.include?(type)
Compiler::Decimal.new
elsif XS_TYPE_FLOAT.include?(type)
Compiler::Float.new
elsif XS_TYPE_INTEGER.include?(type)
Expand Down
1 change: 1 addition & 0 deletions lib/shale/schema/xml_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def self.get_xml_type(shale_type)
register_xml_type(Shale::Type::Boolean, 'boolean')
register_xml_type(Shale::Type::Date, 'date')
register_xml_type(Shale::Type::Float, 'decimal')
register_xml_type(Shale::Type::Decimal, 'decimal')
register_xml_type(Shale::Type::Integer, 'integer')
register_xml_type(Shale::Type::Time, 'dateTime')
register_xml_type(Shale::Type::Value, 'anyType')
Expand Down
51 changes: 51 additions & 0 deletions lib/shale/type/decimal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require_relative 'value'

module Shale
module Type
# Cast value to BigDecimal
#
# @api public
class Decimal < Value
class << self
# @param [String, Float, Integer, nil] value Value to cast
#
# @return [BigDecimal, nil]
#
# @api private
def cast(value)
return if value.nil?

case value
when ::BigDecimal then value
when ::Float then BigDecimal(value, value.to_s.length)
else BigDecimal(value)
end
end

def as_json(value, **)
value.to_f
end

def as_yaml(value, **)
value.to_f
end

def as_csv(value, **)
value.to_f
end

def as_toml(value, **)
value.to_f
end

def as_xml_value(value, **)
value.to_s('F')
end
end
end

register(:decimal, Decimal)
end
end
11 changes: 11 additions & 0 deletions spec/shale/schema/compiler/decimal_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

require 'shale/schema/compiler/decimal'

RSpec.describe Shale::Schema::Compiler::Decimal do
describe '#name' do
it 'returns Shale type name' do
expect(described_class.new.name).to eq('Shale::Type::Decimal')
end
end
end
63 changes: 46 additions & 17 deletions spec/shale/schema/xml_compiler_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'shale/adapter/rexml'
require 'shale/schema/compiler/boolean'
require 'shale/schema/compiler/date'
require 'shale/schema/compiler/decimal'
require 'shale/schema/compiler/float'
require 'shale/schema/compiler/integer'
require 'shale/schema/compiler/string'
Expand Down Expand Up @@ -812,6 +813,7 @@
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="complex1" type="complex1" />
<xs:element name="complex2" type="complex2" />
<xs:element name="complex3" type="complex3" />
<xs:simpleType name="customInt">
<xs:restriction base="xs:integer">
Expand Down Expand Up @@ -840,14 +842,30 @@
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:simpleType name="decimal125">
<xs:restriction base="xs:decimal">
<xs:fractionDigits value="5"/>
<xs:maxExclusive value="10000000"/>
<xs:minInclusive value="0"/>
<xs:totalDigits value="12"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="complex3">
<xs:simpleContent>
<xs:extension base="decimal125">
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:schema>
SCHEMA
end

it 'generates models' do
models = described_class.new.as_models([schema])

expect(models.length).to eq(3)
expect(models.length).to eq(4)

expect(models[0].id).to eq('shoesize')
expect(models[0].name).to eq('Shoesize')
Expand All @@ -866,33 +884,44 @@
expect(models[0].properties[1].prefix).to eq(nil)
expect(models[0].properties[1].namespace).to eq(nil)

expect(models[1].id).to eq('complex2')
expect(models[1].name).to eq('Complex2')
expect(models[1].root).to eq('complex2')
expect(models[1].properties.length).to eq(2)
expect(models[1].id).to eq('complex3')
expect(models[1].name).to eq('Complex3')
expect(models[1].root).to eq('complex3')
expect(models[1].properties.length).to eq(1)
expect(models[1].properties[0].mapping_name).to eq('content')
expect(models[1].properties[0].type).to be_a(Shale::Schema::Compiler::String)
expect(models[1].properties[0].type).to be_a(Shale::Schema::Compiler::Decimal)
expect(models[1].properties[0].collection?).to eq(false)
expect(models[1].properties[0].default).to eq(nil)
expect(models[1].properties[0].prefix).to eq(nil)
expect(models[1].properties[0].namespace).to eq(nil)
expect(models[1].properties[1].mapping_name).to eq('el1')
expect(models[1].properties[1].type).to be_a(Shale::Schema::Compiler::Integer)
expect(models[1].properties[1].collection?).to eq(false)
expect(models[1].properties[1].default).to eq(nil)
expect(models[1].properties[1].prefix).to eq(nil)
expect(models[1].properties[1].namespace).to eq(nil)

expect(models[2].id).to eq('complex1')
expect(models[2].name).to eq('Complex1')
expect(models[2].root).to eq('complex1')
expect(models[2].properties.length).to eq(1)
expect(models[2].id).to eq('complex2')
expect(models[2].name).to eq('Complex2')
expect(models[2].root).to eq('complex2')
expect(models[2].properties.length).to eq(2)
expect(models[2].properties[0].mapping_name).to eq('content')
expect(models[2].properties[0].type).to be_a(Shale::Schema::Compiler::Integer)
expect(models[2].properties[0].type).to be_a(Shale::Schema::Compiler::String)
expect(models[2].properties[0].collection?).to eq(false)
expect(models[2].properties[0].default).to eq(nil)
expect(models[2].properties[0].prefix).to eq(nil)
expect(models[2].properties[0].namespace).to eq(nil)
expect(models[2].properties[1].mapping_name).to eq('el1')
expect(models[2].properties[1].type).to be_a(Shale::Schema::Compiler::Integer)
expect(models[2].properties[1].collection?).to eq(false)
expect(models[2].properties[1].default).to eq(nil)
expect(models[2].properties[1].prefix).to eq(nil)
expect(models[2].properties[1].namespace).to eq(nil)

expect(models[3].id).to eq('complex1')
expect(models[3].name).to eq('Complex1')
expect(models[3].root).to eq('complex1')
expect(models[3].properties.length).to eq(1)
expect(models[3].properties[0].mapping_name).to eq('content')
expect(models[3].properties[0].type).to be_a(Shale::Schema::Compiler::Integer)
expect(models[3].properties[0].collection?).to eq(false)
expect(models[3].properties[0].default).to eq(nil)
expect(models[3].properties[0].prefix).to eq(nil)
expect(models[3].properties[0].namespace).to eq(nil)
end
end

Expand Down
9 changes: 9 additions & 0 deletions spec/shale/schema/xml_generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class BranchTwo < Shale::Mapper
class Root < Shale::Mapper
attribute :boolean, :boolean
attribute :date, :date
attribute :decimal, :decimal
attribute :float, :float
attribute :integer, :integer
attribute :string, :string
Expand All @@ -45,6 +46,7 @@ class Root < Shale::Mapper

attribute :boolean_default, :boolean, default: -> { true }
attribute :date_default, :date, default: -> { Date.new(2021, 1, 1) }
attribute :decimal_default, :decimal, default: -> { BigDecimal('1.0') }
attribute :float_default, :float, default: -> { 1.0 }
attribute :integer_default, :integer, default: -> { 1 }
attribute :string_default, :string, default: -> { 'string' }
Expand All @@ -55,6 +57,7 @@ class Root < Shale::Mapper

attribute :boolean_collection, :boolean, collection: true
attribute :date_collection, :date, collection: true
attribute :decimal_collection, :decimal, collection: true
attribute :float_collection, :float, collection: true
attribute :integer_collection, :integer, collection: true
attribute :string_collection, :string, collection: true
Expand All @@ -77,6 +80,7 @@ class Root < Shale::Mapper

map_element 'boolean', to: :boolean
map_element 'date', to: :date
map_element 'decimal', to: :decimal
map_element 'float', to: :float
map_element 'integer', to: :integer
map_element 'string', to: :string
Expand All @@ -85,6 +89,7 @@ class Root < Shale::Mapper

map_element 'boolean_default', to: :boolean_default
map_element 'date_default', to: :date_default
map_element 'decimal_default', to: :decimal_default
map_element 'float_default', to: :float_default
map_element 'integer_default', to: :integer_default
map_element 'string_default', to: :string_default
Expand All @@ -93,6 +98,7 @@ class Root < Shale::Mapper

map_element 'boolean_collection', to: :boolean_collection
map_element 'date_collection', to: :date_collection
map_element 'decimal_collection', to: :decimal_collection
map_element 'float_collection', to: :float_collection
map_element 'integer_collection', to: :integer_collection
map_element 'string_collection', to: :string_collection
Expand Down Expand Up @@ -172,20 +178,23 @@ class PersonMapper < Shale::Mapper
<xs:sequence>
<xs:element name="boolean" type="xs:boolean" minOccurs="0"/>
<xs:element name="date" type="xs:date" minOccurs="0"/>
<xs:element name="decimal" type="xs:decimal" minOccurs="0"/>
<xs:element name="float" type="xs:decimal" minOccurs="0"/>
<xs:element name="integer" type="xs:integer" minOccurs="0"/>
<xs:element name="string" type="xs:string" minOccurs="0"/>
<xs:element name="time" type="xs:dateTime" minOccurs="0"/>
<xs:element name="value" type="xs:anyType" minOccurs="0"/>
<xs:element name="boolean_default" type="xs:boolean" minOccurs="0" default="true"/>
<xs:element name="date_default" type="xs:date" minOccurs="0" default="2021-01-01"/>
<xs:element name="decimal_default" type="xs:decimal" minOccurs="0" default="1.0"/>
<xs:element name="float_default" type="xs:decimal" minOccurs="0" default="1.0"/>
<xs:element name="integer_default" type="xs:integer" minOccurs="0" default="1"/>
<xs:element name="string_default" type="xs:string" minOccurs="0" default="string"/>
<xs:element name="time_default" type="xs:dateTime" minOccurs="0" default="2021-01-01T10:10:10+01:00"/>
<xs:element name="value_default" type="xs:anyType" minOccurs="0" default="value"/>
<xs:element name="boolean_collection" type="xs:boolean" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="date_collection" type="xs:date" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="decimal_collection" type="xs:decimal" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="float_collection" type="xs:decimal" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="integer_collection" type="xs:integer" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="string_collection" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
Expand Down
Loading

0 comments on commit bd7c878

Please sign in to comment.