ializer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'ializer'
6
+
7
+ require 'irb'
8
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/ializer.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'ializer/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'ializer'
9
+ spec.version = Ializer::VERSION
10
+ spec.authors = ['Jeremy Steinberg']
11
+
12
+ spec.summary = 'Simple (de)serializers for Ruby objects'
13
+ spec.homepage = 'https://github.com/jsteinberg/ializer'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'activesupport'
24
+ spec.add_dependency 'multi_json', '~> 1.0'
25
+
26
+ spec.add_development_dependency 'bundler'
27
+ spec.add_development_dependency 'pry'
28
+ spec.add_development_dependency 'rake'
29
+ spec.add_development_dependency 'rspec'
30
+ spec.add_development_dependency 'rubocop'
31
+ spec.add_development_dependency 'rubocop-rspec'
32
+ spec.add_development_dependency 'simplecov'
33
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module De
4
+ module Ser
5
+ class Ializer < ::Ser::Ializer
6
+ class << self
7
+ def parse(data, model_class)
8
+ return parse_many(data, model_class) if data.is_a? Array
9
+
10
+ parse_one(data, model_class)
11
+ end
12
+
13
+ def parse_many(data, model_class)
14
+ data.map { |obj_data| parse_one(obj_data, model_class) }
15
+ end
16
+
17
+ def parse_one(data, model_class)
18
+ # OpenStruct lazily defines methods so respond_to? fails initially
19
+ return parse_ostruct(data) if ostruct?(model_class)
20
+
21
+ parse_object(data, model_class.new)
22
+ end
23
+
24
+ def parse_object(data, object)
25
+ data.each do |key, value|
26
+ parse_attribute(object, key, value)
27
+ end
28
+
29
+ object
30
+ end
31
+
32
+ def parse_json(json, model_class)
33
+ data = MultiJson.load(json)
34
+ parse(data, model_class)
35
+ end
36
+
37
+ def parse_attribute(object, key, value)
38
+ field = _attributes[key]
39
+
40
+ return unless field
41
+
42
+ return unless object.respond_to?(field.setter)
43
+
44
+ parse_field(object, field, value)
45
+ end
46
+
47
+ protected
48
+
49
+ def create_anon_class
50
+ Class.new(De::Ser::Ializer)
51
+ end
52
+
53
+ private
54
+
55
+ def parse_field(object, field, value)
56
+ parsed_value = field.parse(value)
57
+
58
+ return if parsed_value.nil?
59
+
60
+ object.public_send(field.setter, parsed_value)
61
+ end
62
+
63
+ def ostruct?(model_class)
64
+ defined?(OpenStruct) && model_class <= OpenStruct
65
+ end
66
+
67
+ def parse_ostruct(data)
68
+ object = OpenStruct.new
69
+
70
+ data.each do |key, value|
71
+ parse_ostruct_field(object, key, value)
72
+ end
73
+
74
+ object
75
+ end
76
+
77
+ def parse_ostruct_field(object, key, value)
78
+ field = _attributes[key]
79
+
80
+ return unless field
81
+
82
+ parse_field(object, field, value)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+
5
+ module Ializer
6
+ class BigDecimalDeSer
7
+ def self.serialize(value, _context = nil)
8
+ value.to_s('F')
9
+ end
10
+
11
+ def self.parse(value)
12
+ BigDecimal(value)
13
+ end
14
+ end
15
+ end
16
+
17
+ Ser::Ializer.register('decimal', Ializer::BigDecimalDeSer, :BigDecimal, :decimal)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ializer
4
+ class BooleanDeSer
5
+ def self.serialize(value, _context = nil)
6
+ value
7
+ end
8
+
9
+ def self.parse(value)
10
+ return value if value.is_a? TrueClass
11
+ return value if value.is_a? FalseClass
12
+
13
+ value.to_s == 'true'
14
+ end
15
+ end
16
+ end
17
+
18
+ Ser::Ializer.register('boolean', Ializer::BooleanDeSer, :Boolean, :boolean)
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ializer
4
+ class Config
5
+ def initialize
6
+ @warn_on_default = true
7
+ end
8
+
9
+ ##
10
+ # :key_transform=: key_transform
11
+ #
12
+ # symbol of string transform to call on field keys
13
+ # default is +:dasherize+.
14
+ def key_transform=(key_transform)
15
+ self.key_transformer = key_transform&.to_proc
16
+ end
17
+
18
+ ##
19
+ # :attr_accessor: key_transformer
20
+ #
21
+ # object that responds_to :call with arity 1
22
+ # the Field name is passed into the key_transformer
23
+ # A key_transformer has higher precedence than key_transform
24
+ # default is +nil+.
25
+ attr_accessor :key_transformer
26
+
27
+ ##
28
+ # :attr_accessor: warn_on_default
29
+ #
30
+ # The DefaultDeSer when converting to JSON will only work properly for standard
31
+ # JSON value types(:string, :number, :boolean)
32
+ # A warning message will be logged if the DefaultDeSer has been used
33
+ # default is +true+.
34
+ attr_accessor :warn_on_default
35
+ alias warn_on_default? warn_on_default
36
+
37
+ ##
38
+ # :attr_accessor: raise_on_default
39
+ #
40
+ # The DefaultDeSer when converting to JSON will only work properly for standard
41
+ # JSON value types(:string, :number, :boolean)
42
+ # An error will be raised if the DefaultDeSer has been used
43
+ # default is +nil+.
44
+ attr_accessor :raise_on_default
45
+ alias raise_on_default? raise_on_default
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Ializer
6
+ class DateDeSer
7
+ def self.serialize(value, _context = nil)
8
+ value.to_s
9
+ end
10
+
11
+ def self.parse(value)
12
+ Date.parse(value)
13
+ rescue ArgumentError
14
+ nil
15
+ end
16
+ end
17
+ end
18
+
19
+ Ser::Ializer.register('date', Ializer::DateDeSer, Date, :date)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ializer
4
+ class DefaultDeSer
5
+ def self.serialize(value, _context = nil)
6
+ value
7
+ end
8
+
9
+ def self.parse(value)
10
+ value
11
+ end
12
+ end
13
+ end
14
+
15
+ Ser::Ializer.register_default(Ializer::DefaultDeSer)
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ializer
4
+ class FixNumDeSer
5
+ def self.serialize(value, _context = nil)
6
+ value
7
+ end
8
+
9
+ def self.parse(value)
10
+ return value if value.is_a? Numeric
11
+
12
+ value.to_i
13
+ end
14
+ end
15
+ end
16
+
17
+ Ser::Ializer.register('integer', Ializer::FixNumDeSer, Integer, :integer)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ializer
4
+ class FloatDeSer
5
+ NAN_STRING = Float::NAN.to_s
6
+ INFINITY_STRING = Float::INFINITY.to_s
7
+ NEGATIVE_INFINITY_STRING = (-Float::INFINITY).to_s
8
+
9
+ def self.serialize(value, _context = nil)
10
+ value = value.to_f unless value.is_a? Float
11
+
12
+ return NAN_STRING if value.nan?
13
+
14
+ return value.to_s if value.infinite?
15
+
16
+ value
17
+ end
18
+
19
+ def self.parse(value)
20
+ return Float::NAN if value == NAN_STRING
21
+
22
+ return -Float::INFINITY if value == NEGATIVE_INFINITY_STRING
23
+
24
+ return Float::INFINITY if value == INFINITY_STRING
25
+
26
+ value.to_f
27
+ end
28
+ end
29
+ end
30
+
31
+ Ser::Ializer.register('float', Ializer::FloatDeSer, Float, :float)
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Ializer
6
+ class MillisDeSer
7
+ def self.serialize(value, _context = nil)
8
+ (value.to_f * 1000).to_i
9
+ end
10
+
11
+ def self.parse(value)
12
+ Time.at(value / 1000.0)
13
+ end
14
+ end
15
+ end
16
+
17
+ Ser::Ializer.register('millis', Ializer::MillisDeSer, :Millis, :millis)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ializer
4
+ class StringDeSer
5
+ def self.serialize(value, _context = nil)
6
+ value.to_s
7
+ end
8
+
9
+ def self.parse(value)
10
+ value.to_s
11
+ end
12
+ end
13
+ end
14
+
15
+ Ser::Ializer.register('string', Ializer::StringDeSer, String, :string)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ializer
4
+ class SymbolDeSer
5
+ def self.serialize(value, _context = nil)
6
+ value.to_s
7
+ end
8
+
9
+ def self.parse(value)
10
+ value.to_sym
11
+ end
12
+ end
13
+ end
14
+
15
+ Ser::Ializer.register('symbol', Ializer::SymbolDeSer, Symbol, :symbol, :sym)
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/time'
4
+
5
+ module Ializer
6
+ class TimeDeSer
7
+ def self.serialize(value, _context = nil)
8
+ value.to_time.iso8601(3) # to_time to force consistent serialization
9
+ end
10
+
11
+ def self.parse(value)
12
+ DateTime.iso8601 value
13
+ end
14
+ end
15
+ end
16
+
17
+ Ser::Ializer.register('timestamp', Ializer::TimeDeSer, Time, DateTime, :timestamp)
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ializer
4
+ VERSION = '0.1.0'
5
+ end
data/lib/ializer.rb ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ializer/version'
4
+
5
+ require 'ser/ializer/field'
6
+ require 'ser/ializer'
7
+ require 'de/ser/ializer'
8
+
9
+ require 'ializer/big_decimal_de_ser'
10
+ require 'ializer/boolean_de_ser'
11
+ require 'ializer/date_de_ser'
12
+ require 'ializer/default_de_ser'
13
+ require 'ializer/fix_num_de_ser'
14
+ require 'ializer/float_de_ser'
15
+ require 'ializer/millis_de_ser'
16
+ require 'ializer/string_de_ser'
17
+ require 'ializer/symbol_de_ser'
18
+ require 'ializer/time_de_ser'
19
+
20
+ require 'ializer/config'
21
+
22
+ require 'multi_json'
23
+
24
+ module Ializer
25
+ # Returns the global configuration instance
26
+ def self.config
27
+ @config ||= Ializer::Config.new
28
+ end
29
+
30
+ # Initialization block is passed a global Config instance that can be
31
+ # used to configure Ializer behavior. E.g., if you want to
32
+ # disable automation DefaultDeSer warnings put the following in
33
+ # an initializer: config/initializers/ializer.rb
34
+ #
35
+ # Ializer.setup do |config|
36
+ # config.warn_on_default = false
37
+ # end
38
+ #
39
+ def self.setup
40
+ yield config
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ser
4
+ class Ializer
5
+ class Field
6
+ class << self
7
+ def transform(key)
8
+ return key unless ::Ializer.config.key_transformer
9
+
10
+ ::Ializer.config.key_transformer.call(key)
11
+ end
12
+ end
13
+
14
+ attr_reader :name, :setter, :key, :deser, :model_class, :if_condition, :block
15
+
16
+ def initialize(name, options, &block)
17
+ @name = name
18
+ @setter = options[:setter] || "#{name}="
19
+ @key = options[:key] || Field.transform(name.to_s)
20
+ @deser = options[:deser]
21
+ @if_condition = options[:if]
22
+ @model_class = options[:model_class]
23
+ @block = block
24
+ end
25
+
26
+ def serialize(value, context)
27
+ deser.serialize(value, context)
28
+ end
29
+
30
+ def parse(value)
31
+ if model_class
32
+ deser.parse(value, model_class)
33
+ else
34
+ deser.parse(value)
35
+ end
36
+ end
37
+
38
+ def valid_for_context?(object, context)
39
+ return true if if_condition.nil?
40
+
41
+ if_condition.call(object, context)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ser
4
+ class Ializer # rubocop:disable Metrics/ClassLength
5
+ @@method_registry = {} # rubocop:disable Style/ClassVars
6
+
7
+ class << self
8
+ # Public DSL
9
+ def property(name, options = {}, &block)
10
+ return add_attribute(Field.new(name, options, &block)) if options[:deser]
11
+
12
+ return default(name, options, &block) unless options[:type]
13
+
14
+ meth = lookup_method(options[:type])
15
+
16
+ return meth.call(name, options, &block) if meth
17
+
18
+ default(name, options, &block)
19
+ end
20
+
21
+ def nested(name, options = {}, &block)
22
+ if block
23
+ deser = create_anon_class
24
+ deser.class_eval(&block)
25
+ options[:deser] = deser
26
+ end
27
+
28
+ add_attribute(Field.new(name, options))
29
+ end
30
+
31
+ def with(deser)
32
+ deser._attributes.values.map(&:dup).each(&method(:add_attribute))
33
+ end
34
+ # End Public DSL
35
+
36
+ def serialize(object, context = nil)
37
+ return serialize_one(object, context) unless valid_enumerable?(object)
38
+
39
+ object.map { |o| serialize_one(o, context) }
40
+ end
41
+
42
+ def serialize_json(object, context = nil)
43
+ MultiJson.dump(serialize(object, context))
44
+ end
45
+
46
+ def register(method_name, deser, *matchers)
47
+ raise ArgumentError, 'register should only be called on the Ser::Ializer class' unless self == Ser::Ializer
48
+
49
+ define_singleton_method(method_name) do |name, options = {}, &block|
50
+ options[:deser] = deser
51
+ add_attribute Field.new(name, options, &block)
52
+ end
53
+
54
+ matchers.each do |matcher|
55
+ method_registry[matcher.to_s.to_sym] = method_name
56
+ end
57
+ end
58
+
59
+ def register_default(deser)
60
+ define_singleton_method('default') do |name, options = {}, &block|
61
+ raise ArgumentError, warning_message(name) if ::Ializer.config.raise_on_default?
62
+
63
+ puts warning_message(name) if ::Ializer.config.warn_on_default?
64
+
65
+ options[:deser] = deser
66
+ add_attribute Field.new(name, options, &block)
67
+ end
68
+ end
69
+
70
+ def attribute_names
71
+ _attributes.values.map(&:name)
72
+ end
73
+
74
+ protected
75
+
76
+ def create_anon_class
77
+ Class.new(Ser::Ializer)
78
+ end
79
+
80
+ def method_registry
81
+ @@method_registry
82
+ end
83
+
84
+ def lookup_method(type)
85
+ method_name = method_registry[type.to_s.to_sym]
86
+
87
+ return nil unless method_name
88
+
89
+ method(method_name)
90
+ end
91
+
92
+ def _attributes
93
+ @attributes ||=
94
+ if equal? Ser::Ializer
95
+ {}
96
+ else
97
+ superclass._attributes.dup
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def warning_message(name)
104
+ "Warning: #{self} using default DeSer for property #{name}"
105
+ end
106
+
107
+ def add_attribute(field)
108
+ _attributes[field.key] = field
109
+
110
+ if field.block
111
+ add_attribute_with_block(field)
112
+ else
113
+ add_attribute_with_method(field)
114
+ end
115
+ end
116
+
117
+ def add_attribute_with_block(field)
118
+ define_singleton_method field.name do |object, context|
119
+ value = field.block.call(object, context)
120
+
121
+ serialize_field(field, value, context)
122
+ end
123
+ end
124
+
125
+ def add_attribute_with_method(field)
126
+ define_singleton_method field.name do |object, context|
127
+ value = object.public_send(field.name)
128
+
129
+ serialize_field(field, value, context)
130
+ end
131
+ end
132
+
133
+ def serialize_field(field, value, context)
134
+ return nil if value.nil?
135
+
136
+ return field.serialize(value, context) unless valid_enumerable?(value)
137
+
138
+ value.map { |v| field.serialize(v, context) }
139
+ end
140
+
141
+ def serialize_one(object, context)
142
+ _attributes.values.each_with_object({}) do |field, data|
143
+ next unless field.valid_for_context?(object, context)
144
+
145
+ value = public_send(field.name, object, context)
146
+
147
+ next if value.nil?
148
+
149
+ data[field.key] = value
150
+ end
151
+ end
152
+
153
+ def valid_enumerable?(object)
154
+ return true if object.is_a? Array
155
+
156
+ return true if defined?(ActiveRecord) && object.is_a?(ActiveRecord::Relation)
157
+
158
+ false
159
+ end
160
+ end
161
+ end
162
+ end