fancy_hash 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7eb58877c6cd0969a6cb72a53e046edf48db5ec9cbd7216e490d1ea27878432f
4
+ data.tar.gz: 83bc4c6786f530f58f5914f8cb9f1134972db99b94d310d7b7610946253446ef
5
+ SHA512:
6
+ metadata.gz: ddc06cba5b08804e28a616c79e64aba221722dcbca8b23babc1e71edb162e4d2960698e56b1e519d502a3f72136a47fadbce73321db2d80ff001e922800996be
7
+ data.tar.gz: 3562bea884e17f2e0238965afc218350fb392c3483a8f4040997d3d0765493c2a1a99f77b9c742c9c83fa9c72d9689c50ac69ac179c8927bc886bf746ef76628
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FancyHash < SimpleDelegator
4
+ VERSION = '0.1.0'
5
+ end
data/lib/fancy_hash.rb ADDED
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fancy_hash/version'
4
+ require 'active_model'
5
+
6
+ # FancyHash was born to simplify handling third party JSON payloads. Let's say for example we have the following JSON:
7
+ #
8
+ # payload = {
9
+ # 'identificador' => 1,
10
+ # 'nomeCompleto' => "John Doe",
11
+ # 'dtNascimento' => "1990-01-01",
12
+ # 'genero' => 1, # Let's say we know this field is going to be 1 for Male, 2 for Female
13
+ # }
14
+ #
15
+ # It would be very tedius having to remember the field names and handle type conversion everywhere, like this:
16
+ #
17
+ # payload['dtNascimento'].to_date
18
+ # payload['dtNascimento'] = Date.new(1990, 1, 2).iso8601
19
+ #
20
+ # if payload['genero'] == 1
21
+ # # do something
22
+ # end
23
+ #
24
+ # Instead, we can do this:
25
+ #
26
+ # class Person < FancyHash
27
+ # attribute :id, field: 'identificador', type: :integer
28
+ # attribute :name, field: 'nomeCompleto', type: :string
29
+ # attribute :birth_date, field: 'dtNascimento', type: :date
30
+ # attribute :gender, field: 'genero', type: :enum, of: { male: 1, female: 2 }
31
+ # end
32
+ #
33
+ # person = Person.new(payload) # `payload` here is a Hash that we retrieved from an hypothetical API
34
+ # person.id # => 1
35
+ # person.name # => "John Doe"
36
+ # person.name = 'Mary Smith'
37
+ # person.birth_date # => Mon, 01 Jan 1990
38
+ # person.birth_date = Date.new(1990, 1, 2)
39
+ # person.gender # => :male
40
+ # person.male? # => true
41
+ # person.female? # => false
42
+ # person.gender = :female # we can use the symbols here, the FancyHash will convert it to the right value
43
+ #
44
+ # person.__getobj__ # => { 'identificador' => 1, 'nomeCompleto' => 'Mary Smith', 'dtNascimento' => '1990-01-02', 'genero' => 2 }
45
+ #
46
+ # This can be used for inbound payloads that we need to parse and for outbound requests we need to send so we
47
+ # don't need to worry about type casting and enum mapping either way.
48
+ class FancyHash < SimpleDelegator
49
+ extend ActiveModel::Naming
50
+ extend ActiveModel::Translation
51
+ include ActiveModel::Validations
52
+ include ActiveModel::Conversion
53
+
54
+ module Types
55
+ class << self
56
+ def find(type, **config)
57
+ return type if type.is_a?(Class)
58
+
59
+ {
60
+ nil => -> { ActiveModel::Type::Value.new },
61
+ array: -> { Types::Array.new(**config) },
62
+ boolean: -> { ActiveModel::Type::Boolean.new },
63
+ binboolean: -> { BinBoolean.new },
64
+ string: -> { ActiveModel::Type::String.new },
65
+ date: -> { Date.new },
66
+ datetime: -> { DateTime.new },
67
+ integer: -> { ActiveModel::Type::Integer.new },
68
+ decimal: -> { ActiveModel::Type::Decimal.new },
69
+ money: -> { Money.new },
70
+ enum: -> { Enum.new(**config) },
71
+ }.fetch(type).call
72
+ end
73
+ end
74
+
75
+ class Array < ActiveModel::Type::Value
76
+ attr_reader :of
77
+
78
+ def initialize(of: nil)
79
+ super()
80
+
81
+ @of = of
82
+ end
83
+
84
+ def serialize(value)
85
+ value&.map { |v| Types.find(of).serialize(v) }
86
+ end
87
+
88
+ private
89
+
90
+ def cast_value(value)
91
+ # Freezing to prevent adding items to it, as that would be misleading and not affect the original array
92
+ value&.map { |v| Types.find(of).cast(v) }&.freeze
93
+ end
94
+ end
95
+
96
+ class Enum < ActiveModel::Type::Value
97
+ attr_reader :config
98
+
99
+ def initialize(of:)
100
+ super()
101
+
102
+ @config = of.stringify_keys
103
+ end
104
+
105
+ def serialize(value)
106
+ return value if config.value?(value)
107
+
108
+ config[value.to_s]
109
+ end
110
+
111
+ private
112
+
113
+ def cast_value(value)
114
+ return value.to_sym if config.keys.map(&:to_s).include?(value.to_s)
115
+
116
+ config.key(value)
117
+ end
118
+ end
119
+
120
+ class BinBoolean < ActiveModel::Type::Boolean
121
+ def serialize(value)
122
+ value ? '1' : '0'
123
+ end
124
+ end
125
+
126
+ class Date < ActiveModel::Type::Date
127
+ def serialize(value)
128
+ value&.iso8601
129
+ end
130
+ end
131
+
132
+ class DateTime < ActiveModel::Type::DateTime
133
+ def serialize(value)
134
+ value&.iso8601
135
+ end
136
+
137
+ def cast_value(value)
138
+ # Facil returns no timezone information, then it defaults to the OS timezone, which may be UTC in production
139
+ value = "#{value}#{Time.zone.formatted_offset}" if value.is_a?(String) && value.size == 19
140
+
141
+ super
142
+ end
143
+ end
144
+
145
+ class Money < ActiveModel::Type::Decimal
146
+ def serialize(value)
147
+ value&.to_f
148
+ end
149
+
150
+ private
151
+
152
+ def cast_value(_)
153
+ ::Money.from_amount(super)
154
+ end
155
+ end
156
+ end
157
+
158
+ class << self
159
+ def serialize(fancy_hash)
160
+ fancy_hash.is_a?(FancyHash) ? fancy_hash.__getobj__ : fancy_hash
161
+ end
162
+
163
+ def wrap_many(array)
164
+ Array.wrap(array).map { |hash| new(hash) }
165
+ end
166
+
167
+ # Allows defining attributes coming from a Hash with a different attribute name. For example:
168
+ #
169
+ # attribute :name, type: :string
170
+ # attribute :born_on, field: 'birthDate', type: :date
171
+ # attribute :favorite_color, field: 'favoriteColor', type: :enum, of: { red: 0, green: 1, blue: 2 }
172
+ def attribute(name, field: name.to_s, type: nil, default: nil, **, &block)
173
+ attribute_names << name
174
+
175
+ attribute_definitions[name] = { field:, type: }
176
+
177
+ field = Array(field)
178
+
179
+ defaults[name] = default unless default.nil?
180
+
181
+ type_serializer = Types.find(type, **)
182
+
183
+ raw_method = :"raw_#{name}"
184
+ define_method(raw_method) { dig(*field) }
185
+
186
+ define_method(name) do
187
+ type_serializer.cast(send(raw_method)).tap do |value|
188
+ Array(value).each do |v|
189
+ instance_exec(v, &block) if block
190
+ end
191
+ end
192
+ end
193
+
194
+ if type_serializer.is_a?(ActiveModel::Type::Boolean)
195
+ define_method(:"#{name}?") { send(name) }
196
+ elsif type.is_a?(Class)
197
+ define_method(:"#{name}_attributes=") { |attributes| send(:"#{raw_method}=", type.new(**attributes)) }
198
+ end
199
+
200
+ define_method(:"#{name}=") do |new_value|
201
+ send(:"#{raw_method}=", type_serializer.serialize(type_serializer.cast(new_value)))
202
+ end
203
+
204
+ define_method(:"#{raw_method}=") do |new_raw_value|
205
+ hsh = self
206
+
207
+ field[0..-2].each do |key|
208
+ hsh = hsh[key] ||= {}
209
+ end
210
+
211
+ hsh[field.last] = new_raw_value
212
+ end
213
+
214
+ return unless type == :enum
215
+
216
+ define_singleton_method(name.to_s.pluralize) do
217
+ type_serializer.config.symbolize_keys
218
+ end
219
+
220
+ type_serializer.config.each_key do |key|
221
+ define_method(:"#{key}?") do
222
+ send(name) == key
223
+ end
224
+ end
225
+ end
226
+
227
+ def attribute_definitions
228
+ @attribute_definitions ||= {}
229
+ end
230
+
231
+ def cast(raw)
232
+ return raw if raw.nil? || raw.is_a?(self)
233
+
234
+ new(raw)
235
+ end
236
+
237
+ # This is to support FancyHash instances as ActiveModel attributes
238
+ def assert_valid_value(_value)
239
+ # NOOP
240
+ end
241
+
242
+ def attribute_names
243
+ @attribute_names ||= Set.new
244
+ end
245
+
246
+ def defaults
247
+ @defaults ||= {}
248
+ end
249
+ end
250
+
251
+ def initialize(hash = {}, **attributes)
252
+ raise ArgumentError, "Unexpected object class. Should be a Hash or #{self.class}, got #{hash.class} (#{hash})" unless hash.is_a?(Hash) || hash.is_a?(self.class)
253
+
254
+ super(hash)
255
+
256
+ defaults = self.class.defaults.transform_values { |v| v.is_a?(Proc) ? instance_exec(&v) : v }
257
+ assign_attributes(defaults.merge(attributes))
258
+ end
259
+
260
+ def classes
261
+ (self['_klass'] || []) + [self.class.to_s]
262
+ end
263
+
264
+ def merge(other)
265
+ merged = __getobj__.merge(other.__getobj__)
266
+ merged['_klass'] ||= []
267
+ merged['_klass'].push(self.class.to_s)
268
+
269
+ other.class.new(self.class.new(merged))
270
+ end
271
+
272
+ def attributes
273
+ self.class.attribute_names.index_with { send(_1) }
274
+ end
275
+
276
+ def assign_attributes(attributes)
277
+ attributes.each { |k, v| public_send(:"#{k}=", v) }
278
+
279
+ self
280
+ end
281
+
282
+ # Override this method so it does not get delegated to the underlying Hash,
283
+ # which allows us to override `blank?` in entities
284
+ def present?
285
+ !blank?
286
+ end
287
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fancy_hash
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Diego Selzlein
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-02-15 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activemodel
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 6.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 6.0.0
26
+ description: FancyHash provides additional functionality and convenience methods for
27
+ working with Ruby hashes
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - lib/fancy_hash.rb
33
+ - lib/fancy_hash/version.rb
34
+ homepage: https://github.com/diego-aslz/fancy_hash
35
+ licenses:
36
+ - MIT
37
+ metadata:
38
+ source_code_uri: https://github.com/diego-aslz/fancy_hash
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 2.7.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.6.2
54
+ specification_version: 4
55
+ summary: A gem for working with enhanced Ruby hashes
56
+ test_files: []