easy_attrs 0.0.1
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/easy_attrs.rb +219 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 58184a091c40cb53d3095fe0fb5dcafb569ba8f6
|
4
|
+
data.tar.gz: c148a6e5a1a6c4ca143286bcde5a299de9c463df
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a8861b44fbd803d3d651f4fc27df92a664482956bce99e68a44db14daad59c7a75ef96e6b60d3a12b0e22650c753bc2bb09b60289ddbd522a30e79ffd9fc96d6
|
7
|
+
data.tar.gz: d2331453c8e4c130a2aefb085f8d1cbb446a900c6ab23abd5f33d1a605380d0301b85233ccff4a0a5fca19ff30d66103a197a0dc2137489ac67f53d720fcf403
|
data/lib/easy_attrs.rb
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext/hash/keys'
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
6
|
+
#
|
7
|
+
# This module is meant to be used for objects being initialized with either raw
|
8
|
+
# JSON or a regular Hash as opposed to a database row (like sub classes of
|
9
|
+
# ActiveRecord). The raw_input (JSON or Hash) can come from any source but it
|
10
|
+
# would typically be a response from an external API. You would have to build
|
11
|
+
# your own client for the objects you're interested in (see the readme for an
|
12
|
+
# example of a design).
|
13
|
+
#
|
14
|
+
# EasyAttrs only keeps attributes specified in the `instance_variables_only`,
|
15
|
+
# `accessors`, `writers` and `readers` class macros and discards all other keys
|
16
|
+
# in the raw_input.
|
17
|
+
#
|
18
|
+
# `accessors`, `writers` and `readers` are self explanatory and any
|
19
|
+
# symbol/string passed to it will be made an attr_accessor, attr_writer or
|
20
|
+
# attr_reader.
|
21
|
+
#
|
22
|
+
# `instance_variables_only` is here in case the including class needs access to
|
23
|
+
# a specific key in the raw_input but does not want to make this a public
|
24
|
+
# method (accessor/reader/writer). It creates an instance variable for the
|
25
|
+
# symbol/string passed in whose value is the value under that key in the
|
26
|
+
# raw_input. See the `Item` class for an example of how it is used.
|
27
|
+
#
|
28
|
+
# Usage:
|
29
|
+
#
|
30
|
+
# Specify the `instance_variables_only`, `accessors`, `writers` and `readers`
|
31
|
+
# the including class will use and EasyAttrs will create the methods or
|
32
|
+
# variables for it. Any of those class macros can be left out.
|
33
|
+
#
|
34
|
+
# class Competitor
|
35
|
+
# include EasyAttrs
|
36
|
+
|
37
|
+
# readers :id, :name
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# This will create the `id` and `name` readers on the class based on the
|
41
|
+
# raw_input and every other key in the input will be unused.
|
42
|
+
#
|
43
|
+
# $ json = {id: 1, name: 'amazon', other_key: 'other_value'}.to_json
|
44
|
+
# $ comp = Competitor.new(json)
|
45
|
+
# => #<Competitor:0x007faef0f2f930 @id=1, @name="amazon">
|
46
|
+
#
|
47
|
+
# Here is a slightly more complex example with `@instance_variables_only`:
|
48
|
+
#
|
49
|
+
# class Item
|
50
|
+
# include EasyAttrs
|
51
|
+
|
52
|
+
# instance_variables_only :nested_data
|
53
|
+
# accessors :name
|
54
|
+
# readers :id, :category
|
55
|
+
|
56
|
+
# def elements
|
57
|
+
# @nested_data['elements'].map(&:upcase)
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# Note how `instance_variables_only` is used for `:nested_data`. We want to use
|
62
|
+
# the data contained under the nested_data key in the raw_input but we want
|
63
|
+
# to transform the data before exposing it to the outside world. The
|
64
|
+
# transformation is done in the `elements` public method.
|
65
|
+
#
|
66
|
+
# Another thing to note is that if a class defines some attributes (readers for
|
67
|
+
# example) then all subclasses of that class will automatically have the those
|
68
|
+
# readers created and the values set on initialize (see below for an example).
|
69
|
+
#
|
70
|
+
# class Item
|
71
|
+
# include EasyAttrs
|
72
|
+
|
73
|
+
# readers :id, :name
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# class BetterItem < Item
|
77
|
+
# accessors :price
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# $ BetterItem.new(id: 1, name: 'Better Item', price: 25)
|
81
|
+
# => #<BetterItem:0x007faef0f2f930 @id=1, @name="Better Item", @price=25>
|
82
|
+
#
|
83
|
+
# class EvenBetterItem < BetterItem
|
84
|
+
# readers :category
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# $ EvenBetterItem.new(id: 1, name: 'Better Item', price: 25, category: 'BOOOH')
|
88
|
+
# => #<EvenBetterItem:0x007fbb10371978
|
89
|
+
# @category="BOOOH",
|
90
|
+
# @id=1,
|
91
|
+
# @name="Better Item",
|
92
|
+
# @price=25
|
93
|
+
# >
|
94
|
+
#
|
95
|
+
|
96
|
+
module EasyAttrs
|
97
|
+
module ClassMethods
|
98
|
+
|
99
|
+
# `all_attributes` needs to be public for instances to call it in
|
100
|
+
# `intialize`.
|
101
|
+
#
|
102
|
+
def all_attributes
|
103
|
+
@_all_attributes ||= begin
|
104
|
+
attributes = []
|
105
|
+
|
106
|
+
# Yes, this is a nested loop.
|
107
|
+
# The result is memoized so it only happens when the first instance of
|
108
|
+
# the including class is initialized and the length of the ancestor
|
109
|
+
# chain is rarely going to be very long (it will of course vary
|
110
|
+
# depending on the class hierarchy of the applicaton using EasyAttrs).
|
111
|
+
#
|
112
|
+
easy_attrs_ancestors.each do |a|
|
113
|
+
[:readers, :writers, :accessors, :instance_variables_only].each do |i_var|
|
114
|
+
i_var_from_ancestor = a.instance_variable_get("@#{i_var}")
|
115
|
+
|
116
|
+
if i_var_from_ancestor
|
117
|
+
attributes.concat(i_var_from_ancestor)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
attributes
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def readers *attrs
|
129
|
+
unless attrs.empty?
|
130
|
+
@readers = attrs
|
131
|
+
|
132
|
+
class_eval do
|
133
|
+
attr_reader *attrs
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def writers *attrs
|
139
|
+
unless attrs.empty?
|
140
|
+
@writers = attrs
|
141
|
+
|
142
|
+
class_eval do
|
143
|
+
attr_writer *attrs
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def accessors *attrs
|
149
|
+
unless attrs.empty?
|
150
|
+
@accessors = attrs
|
151
|
+
|
152
|
+
class_eval do
|
153
|
+
attr_accessor *attrs
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def instance_variables_only *attrs
|
159
|
+
@instance_variables_only = attrs unless attrs.empty?
|
160
|
+
end
|
161
|
+
|
162
|
+
# The ancestor chain includes `self`, which is exactly what we want in this
|
163
|
+
# case because we want to grab all the class instance variables of all the
|
164
|
+
# ancestors AND tose of the current class so we can find all attributes to
|
165
|
+
# use when an instance calls `new`.
|
166
|
+
#
|
167
|
+
def easy_attrs_ancestors
|
168
|
+
ancestors.select { |a| a.include? EasyAttrs }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.included klass
|
173
|
+
klass.extend ClassMethods
|
174
|
+
end
|
175
|
+
|
176
|
+
# Transform all top level keys to snake case symbols to handle camel case
|
177
|
+
# input.
|
178
|
+
# Then, if a given key is part of `all_attributes` AND its content is a Hash,
|
179
|
+
# recursively transform all keys to snake case symbols.
|
180
|
+
# We want to avoid running `deep_transform_keys` on the raw_input because we
|
181
|
+
# may end up transforming a lot of deeply nested keys that will be discarded
|
182
|
+
# if they are not part of `all_attributes`.
|
183
|
+
#
|
184
|
+
# It's fastest to pass a Hash as input. A Json string is slower. A Hash with
|
185
|
+
# camel case keys is even slower. And a Json string with camel case keys is
|
186
|
+
# the slowest.
|
187
|
+
#
|
188
|
+
def initialize raw_input={}
|
189
|
+
input = parse_input(raw_input)
|
190
|
+
set_instance_variables(input) unless input.empty?
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def parse_input raw_input
|
196
|
+
if raw_input.is_a?(Hash)
|
197
|
+
raw_input
|
198
|
+
elsif raw_input.is_a?(String)
|
199
|
+
ActiveSupport::JSON.decode(raw_input)
|
200
|
+
else
|
201
|
+
{}
|
202
|
+
end.map { |k, v| [k.to_s.underscore.to_sym, v] }.to_h
|
203
|
+
end
|
204
|
+
|
205
|
+
def set_instance_variables attrs_as_hash
|
206
|
+
self.class.all_attributes.each do |attribute|
|
207
|
+
raw_value = attrs_as_hash[attribute.to_sym]
|
208
|
+
next if raw_value.nil?
|
209
|
+
|
210
|
+
value = if raw_value.is_a?(Hash)
|
211
|
+
raw_value.deep_transform_keys { |k| k.to_s.underscore.to_sym }
|
212
|
+
else
|
213
|
+
raw_value
|
214
|
+
end
|
215
|
+
|
216
|
+
instance_variable_set("@#{attribute}", value)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: easy_attrs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Noe Stauffert
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-11-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.7'
|
41
|
+
description:
|
42
|
+
email: noe.stauffert@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- lib/easy_attrs.rb
|
48
|
+
homepage: http://www.noestauffert.com
|
49
|
+
licenses:
|
50
|
+
- MIT
|
51
|
+
metadata: {}
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 2.6.12
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: easy_attrs allows you to build objects easily
|
72
|
+
test_files: []
|