class2 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9de74e43eb48f696e8284882f70046f2e824f1bf
4
- data.tar.gz: 4de24fa14eb525838acb447c0d4bb79d3f7eff04
3
+ metadata.gz: a06a22bea52408ffd64b85a1adaca09f92d9c01b
4
+ data.tar.gz: ef14964e9c023946f7fefb3c6b68eabe2b074007
5
5
  SHA512:
6
- metadata.gz: f6ebc7cf450ae0324f32f96bf8e6fe4349e10c8d65a32bc37f341cf872d38b508475a41bf198a8af93cdec6bf5628489206c6dc7ed44ac5dac7e01b06be4ef5e
7
- data.tar.gz: f7b3b93aa9b066835095184cb2265f5faddd4c857bb60494e6dd0775bb4822868039bfcec6673498bd5c9a064f0dcb3270f86083a340378e98eb61546f5c4117
6
+ metadata.gz: ce1b018403485200bf1348d45eb77c4d4e3c49158233c351b573eb1d4fead11843c036a7c8c3061b2f87700d79a46818273281bf635956030f978a879cb03645
7
+ data.tar.gz: c43d375dfa1f161163da622010932aab022736e155f850669fc37258e513db5bcfc251f3dea6160c277e290e7bb0b5373bb1721a302542796838fc47200b1e02
data/.travis.yml CHANGED
@@ -1,8 +1,9 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3.0
5
- - 2.4.0
4
+ - 2.3
5
+ - 2.4
6
+ - 2.5
6
7
 
7
8
  before_install: gem install bundler
8
9
  notifications:
data/Changes CHANGED
@@ -1,3 +1,10 @@
1
+ 2018-05-27 v0.4.0
2
+ --------------------
3
+ * Fix conversion bug when a module is used as a type specifier
4
+ * Fix Fixnum deprecation warnings
5
+ * Add support for JSON serialization and accessor formats that differ from definition
6
+ * Add method that modules can use for initialization
7
+
1
8
  2017-12-04 v0.3.0
2
9
  --------------------
3
10
  * For attributes with a type of String don't convert nil to an empty string
data/README.md CHANGED
@@ -30,7 +30,7 @@ Each of these classes are created with
30
30
  ```rb
31
31
  class2 :user => {
32
32
  :name => String,
33
- :age => Fixnum,
33
+ :age => Integer,
34
34
  :addresses => [
35
35
  :city, :state, :zip, # No explicit types for these
36
36
  :country => {
@@ -95,13 +95,37 @@ response = [
95
95
  }
96
96
  ]
97
97
 
98
- class2 :commit => response.first
98
+ class2 :commit => response.first do
99
+ include Class2::SnakeCase::JSON
100
+ end
99
101
 
100
102
  commit = Commit.new(response.first)
101
103
  commit.author.name # "sshaw"
102
104
  commit.comment_count # 0
105
+ JSON.dump(commit)
103
106
  ```
104
107
 
108
+ If the JSON uses `camelCase` but you want your class to use `snake_case` you can do the following:
109
+
110
+ ```rb
111
+ class2 "commit" => { "camelCase" => { "someKey" => 123, "anotherKey" => 456 } } do
112
+ include Class2::SnakeCase::Attributes # snake_case accessors
113
+ include Class2::LowerCamelCase::JSON # but serialize using camelCase
114
+ end
115
+
116
+ commit = Commit.new(:camel_case => { :some_key => 55 })
117
+ commit.camel_case.some_key # 55
118
+
119
+ commit = Commit.new(:camelCase => { :someKey => 55 })
120
+ commit.camel_case.some_key # 55
121
+ ```
122
+
123
+ For more info on accessor formats and JSON see:
124
+
125
+ * [`Class2::SnakeCase`](https://www.rubydoc.info/gems/class2/Class2/SnakeCase)
126
+ * [`Class2::UpperCamelCase`](https://www.rubydoc.info/gems/class2/Class2/UpperCamelCase)
127
+ * [`Class2::LowerCamelCase`](https://www.rubydoc.info/gems/class2/Class2/LowerCamelCase)
128
+
105
129
  ### class2 API
106
130
 
107
131
  The are 3 ways to use class2. Pick the one that suites your style and/or requirements:
@@ -142,7 +166,7 @@ You can use any of these classes or their instances in your class definitions:
142
166
  * `DateTime`
143
167
  * `Float`
144
168
  * `Hash`
145
- * `Integer`/`Fixnum` - either one will cause a `Fixnum` conversion
169
+ * `Integer`
146
170
  * `TrueClass`/`FalseClass` - either one will cause a boolean conversion
147
171
 
148
172
  Custom conversions are possible, just add the conversion to
@@ -221,6 +245,7 @@ Now an `ArgumentError` will be raised if anything but `id`, `name`, or
221
245
 
222
246
  Also see [Customizations](#customizations).
223
247
 
248
+
224
249
  ## See Also
225
250
 
226
251
  The Perl modules that served as inspiration:
data/lib/class2.rb CHANGED
@@ -1,8 +1,9 @@
1
- # coding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  require "date"
4
+ require "json"
4
5
  require "active_support/core_ext/module"
5
- require "active_support/core_ext/string"
6
+ require "active_support/inflector"
6
7
 
7
8
  require "class2/version"
8
9
 
@@ -37,7 +38,7 @@ class Class2
37
38
  }
38
39
 
39
40
  CONVERSIONS[FalseClass] = CONVERSIONS[TrueClass]
40
- CONVERSIONS[Fixnum] = CONVERSIONS[Integer]
41
+ CONVERSIONS[Fixnum] = CONVERSIONS[Integer] if defined?(Fixnum)
41
42
  CONVERSIONS.default = lambda { |v| v }
42
43
 
43
44
  class << self
@@ -91,7 +92,7 @@ class Class2
91
92
  else
92
93
  # Type can be a class name or an instance
93
94
  # If it's an instance, use its type
94
- v = v.class unless v.is_a?(Class)
95
+ v = v.class unless v.is_a?(Class) || v.is_a?(Module)
95
96
  simple << { k => v }
96
97
  end
97
98
  end
@@ -113,8 +114,7 @@ class Class2
113
114
 
114
115
  klass = Class.new do
115
116
  def initialize(attributes = nil)
116
- return unless attributes.is_a?(Hash)
117
- assign_attributes(attributes)
117
+ __initialize(attributes)
118
118
  end
119
119
 
120
120
  class_eval <<-CODE, __FILE__, __LINE__
@@ -127,11 +127,11 @@ class Class2
127
127
  to_h == other.to_h
128
128
  end
129
129
 
130
- alias :eql? :==
130
+ alias eql? ==
131
131
 
132
132
  def to_h
133
133
  hash = {}
134
- (#{simple.map { |n| n.keys.first } + nested.map { |n| n.keys.first }}).each do |name|
134
+ self.class.__attributes.each do |name|
135
135
  hash[name] = v = public_send(name)
136
136
  # Don't turn nil into a Hash
137
137
  next if v.nil? || !v.respond_to?(:to_h)
@@ -158,11 +158,13 @@ class Class2
158
158
  hash
159
159
  end
160
160
 
161
- def __nested_attributes
161
+ def self.__nested_attributes
162
162
  #{nested.map { |n| n.keys.first.to_sym }}.freeze
163
163
  end
164
164
 
165
- private :__nested_attributes
165
+ def self.__attributes
166
+ (#{simple.map { |n| n.keys.first.to_sym }} + __nested_attributes).freeze
167
+ end
166
168
  CODE
167
169
 
168
170
  simple.each do |cfg|
@@ -205,11 +207,18 @@ class Class2
205
207
  # Do this last to allow for overriding the methods we define
206
208
  class_eval(&block) unless block.nil?
207
209
 
210
+ protected
211
+
212
+ def __initialize(attributes)
213
+ return unless attributes.is_a?(Hash)
214
+ assign_attributes(attributes)
215
+ end
216
+
208
217
  private
209
218
 
210
219
  def assign_attributes(attributes)
211
220
  attributes.each do |key, value|
212
- if __nested_attributes.include?(key.respond_to?(:to_sym) ? key.to_sym : key) &&
221
+ if self.class.__nested_attributes.include?(key.respond_to?(:to_sym) ? key.to_sym : key) &&
213
222
  (value.is_a?(Hash) || value.is_a?(Array))
214
223
 
215
224
  name = key.to_s.classify
@@ -233,22 +242,143 @@ class Class2
233
242
 
234
243
  #
235
244
  # By default unknown arguments are ignored. <code>include<code>ing this will
236
- # cause an ArgumentError to be raised if an attribute is unknown:
245
+ # cause an ArgumentError to be raised if an attribute is unknown.
237
246
  #
238
247
  module StrictConstructor
239
248
  def self.included(klass)
240
249
  klass.class_eval do
241
250
  def initialize(attributes = nil)
242
- return unless attributes.is_a?(Hash)
243
- assign_attributes(attributes)
244
-
245
- accepted = to_h.keys
251
+ return unless __initialize(attributes)
246
252
  attributes.each do |name, _|
247
- next if accepted.include?(name.respond_to?(:to_sym) ? name.to_sym : name)
253
+ next if self.class.__attributes.include?(name.respond_to?(:to_sym) ? name.to_sym : name)
248
254
  raise ArgumentError, "unknown attribute: #{name}"
249
255
  end
250
256
  end
251
257
  end
252
258
  end
253
259
  end
260
+
261
+ #
262
+ # Support +CamelCase+ attributes. See Class2::SnakeCase.
263
+ #
264
+ module UpperCamelCase
265
+ module Attributes
266
+ def self.included(klass)
267
+ Util.convert_attributes(klass) { |v| v.camelize }
268
+ end
269
+ end
270
+
271
+ module JSON
272
+ def as_json(*)
273
+ Util.as_json(self, :camelize)
274
+ end
275
+
276
+ def to_json(*argz)
277
+ as_json.to_json(*argz)
278
+ end
279
+ end
280
+ end
281
+
282
+ #
283
+ # Support +camelCase+ attributes. See Class2::SnakeCase .
284
+ #
285
+ module LowerCamelCase
286
+ module Attributes
287
+ def self.included(klass)
288
+ Util.convert_attributes(klass) { |v| v.camelize(:lower) }
289
+ end
290
+ end
291
+
292
+ module JSON
293
+ def as_json(*)
294
+ Util.as_json(self, :camelize, :lower)
295
+ end
296
+
297
+ def to_json(*argz)
298
+ as_json.to_json(*argz)
299
+ end
300
+ end
301
+ end
302
+
303
+ #
304
+ # Use this when the class was not defined using a Hash with +snake_case+ keys
305
+ # but +snake_case+ is a desired access or serialization mechanism.
306
+ #
307
+ module SnakeCase
308
+ #
309
+ # Support +snake_case+ attributes.
310
+ # This will accept them in the constructor and return them via #to_h.
311
+ #
312
+ # The key format used to define the class will still be accepted and its accessors will
313
+ # remain.
314
+ #
315
+ module Attributes
316
+ def self.included(klass)
317
+ Util.convert_attributes(klass) { |v| v.underscore }
318
+ end
319
+ end
320
+
321
+ #
322
+ # Create JSON documents that have +snake_case+ properties.
323
+ # This will add #as_json and #to_json methods.
324
+ #
325
+ module JSON
326
+ def as_json(*)
327
+ Util.as_json(self, :underscore)
328
+ end
329
+
330
+ def to_json(*argz)
331
+ as_json.to_json(*argz)
332
+ end
333
+ end
334
+ end
335
+
336
+ module Util
337
+ def self.as_json(klass, *argz)
338
+ hash = {}
339
+ klass.to_h.each do |k, v|
340
+ if v.is_a?(Hash)
341
+ v = as_json(v, *argz)
342
+ elsif v.is_a?(Array)
343
+ v = v.map { |e| as_json(e, *argz) }
344
+ elsif v.respond_to?(:as_json)
345
+ v = v.as_json
346
+ end
347
+
348
+ hash[k.to_s.public_send(*argz)] = v
349
+ end
350
+
351
+ hash
352
+ end
353
+
354
+ def self.convert_attributes(klass)
355
+ klass.class_eval do
356
+ new_nested = []
357
+ new_attributes = []
358
+
359
+ __attributes.map do |old_name|
360
+ new_name = yield(old_name.to_s)
361
+ alias_method new_name, old_name
362
+ alias_method "#{new_name}=", "#{old_name}="
363
+
364
+ new_attributes << new_name.to_sym
365
+ new_nested << new_attributes.last if __nested_attributes.include?(old_name)
366
+ end
367
+
368
+ class_eval <<-CODE
369
+ def self.__attributes
370
+ #{new_attributes}.freeze
371
+ end
372
+
373
+ # We need both styles nere to support proper assignment of nested attributes... :(
374
+ def self.__nested_attributes
375
+ #{new_nested + __nested_attributes}.freeze
376
+ end
377
+ CODE
378
+ end
379
+ end
380
+ end
381
+
382
+ private_constant :Util
383
+
254
384
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Class2
2
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
3
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: class2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Skye Shaw
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-12-05 00:00:00.000000000 Z
11
+ date: 2018-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport