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 +7 -0
- data/lib/fancy_hash/version.rb +5 -0
- data/lib/fancy_hash.rb +287 -0
- metadata +56 -0
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
|
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: []
|