easy_attrs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/easy_attrs.rb +219 -0
  3. metadata +72 -0
@@ -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
@@ -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: []