class2 0.0.1 → 0.0.2

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: a3ba3ccb4eb642b385d3878ba4861cfcfdd66aaa
4
- data.tar.gz: 7f34029b8614857d44725173638ef3263b68cb7c
3
+ metadata.gz: 91ca10f950914ea029d15364b4722c064dc5d669
4
+ data.tar.gz: c1b657103a971487aa414c95c7f789959c1bd5bf
5
5
  SHA512:
6
- metadata.gz: fd9f09822c3eb65d5b3e2584ea5bf3011d5421a232457657b658b3a54b3acf34873d7d3d36e93644c38e338d89915159ba085989c70aad99deb52a92d6235950
7
- data.tar.gz: 05720dd6b75a9ed646b833f0ecdc41163ff68faa25fa238366bc439f0fa627e1339ddd1836a3e42b26f50b86abed1c3c698d86f2dbeec5c1e681121cc1b166a3
6
+ metadata.gz: fe9ece44a7faecc4b52dd0159c4fd0271ab38c65928d4ccdbdd2960ffb0abe8366951152fe2bc83b5fc3001b97758dcd3c9ae25f1fb19d91f665e459b97da5e7
7
+ data.tar.gz: 0294c237a509932691f6e4fd8f976b1d3d49aea607e27053e15681b94eb69a7f58e2ac7601bf7283a195e6405721d466a43834ffea5bf57800024b590655ac90
data/.gitignore CHANGED
@@ -93,7 +93,7 @@ build-iPhoneSimulator/
93
93
 
94
94
  # for a library or gem, you might want to ignore these files since the code is
95
95
  # intended to run in multiple environments; otherwise, check them in:
96
- # Gemfile.lock
96
+ Gemfile.lock
97
97
  # .ruby-version
98
98
  # .ruby-gemset
99
99
 
data/Changes ADDED
@@ -0,0 +1,5 @@
1
+ 2017-08-10 v0.0.2
2
+ --------------------
3
+ * Add support for types
4
+ * Bug fix: don't attempt to create invalid method names
5
+ * Bug fix: don't assume constructor arg is a Hash
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Class2
2
2
 
3
- Easily create hierarchies of classes that support nested attributes, equality, and more.
3
+ Easily create hierarchies of classes that support nested attributes, type conversion, equality, and more.
4
4
 
5
5
  [![Build Status](https://travis-ci.org/sshaw/class2.svg?branch=master)](https://travis-ci.org/sshaw/class2)
6
6
 
@@ -24,9 +24,28 @@ This creates 3 classes: `User`, `Address`, and `Country` with the following attr
24
24
  * `Address`: city, state, zip, country
25
25
  * `Country`: name, code
26
26
 
27
- Each of these classes also contain [several additional methods](#methods).
27
+ Each of these classes are created with [several additional methods](#methods).
28
28
 
29
- Example:
29
+ You can also specify types:
30
+
31
+ ```rb
32
+ Class2(
33
+ :user => {
34
+ :name => String,
35
+ :age => Fixnum,
36
+ :addresses => [
37
+ :city, :state, :zip, # No explicit types for these
38
+ :country => {
39
+ :name => String,
40
+ :code => String
41
+ }
42
+ ]
43
+ }
44
+ )
45
+ ```
46
+ Attributes without types are treated as is.
47
+
48
+ After calling either one of the above you can do following:
30
49
 
31
50
  ```rb
32
51
  user = User.new(
@@ -42,22 +61,68 @@ user = User.new(
42
61
  ]
43
62
  )
44
63
 
45
- p user.name # "sshaw"
46
- p user.addresses.size # 3
47
- p user.addresses.first.city # "LA"
64
+ user.name # "sshaw"
65
+ user.addresses.size # 3
66
+ user.addresses.first.city # "LA"
67
+ user.to_h # {:name => "sshaw", :age => 99, :addresses => [ { ... } ]}
48
68
 
49
- # Keys can be strings too
69
+ # keys can be strings too
50
70
  country = Country.new("name" => "America", "code" => "US")
51
71
  address = Address.new(:city => "Da Bay", :state => "CA", :country => country)
52
72
  user.addresses << address
53
73
 
54
- p User.new(:name => "sshaw") == User.new(:name => "sshaw") # true
74
+ User.new(:name => "sshaw") == User.new(:name => "sshaw") # true
75
+ ```
76
+
77
+ Unknown attributes passed to the constructor are ignored.
78
+
79
+ `Class2` can create classes with typed attributes from example hashes.
80
+ This makes it possible to build classes for things like API responses, using the API response
81
+ itself -or a slightly modified version, see [Issues](#issues)- as the specification:
55
82
 
56
- Class2(:foo, :bar => :baz)
57
- Foo.new
58
- Bar.new(:baz => 123)
83
+ ```rb
84
+ # From JSON.parse
85
+ # of https://api.github.com/repos/sshaw/selfie_formatter/commits
86
+ response = [
87
+ {
88
+ "sha" => "f52f1ed9144e1f73346176ab79a61af78df1b6bd",
89
+ "commit" => {
90
+ "author"=> {
91
+ "name"=>"sshaw",
92
+ "email"=>"skye.shaw@gmail.com",
93
+ "date"=>"2016-06-30T03:51:00Z"
94
+ }
95
+ },
96
+ "comment_count": 0
97
+
98
+ # snip full response
99
+ }
100
+ ]
101
+
102
+ Class2(
103
+ :commit => response.first
104
+ )
105
+
106
+ commit = Commit.new(response.first)
107
+ commit.author.name # "sshaw"
108
+ commit.comment_count # 0
59
109
  ```
60
110
 
111
+ ### Conversions
112
+
113
+ You can use any of these classes or their instances in your class definitions:
114
+
115
+ * `Array`
116
+ * `Date`
117
+ * `DateTime`
118
+ * `Float`
119
+ * `Hash`
120
+ * `Integer`/`Fixnum` - either one will cause a `Fixnum` conversion
121
+ * `TrueClass`/`FalseClass` - either one will cause a boolean conversion
122
+
123
+ Custom conversions are possible, just add the conversion to
124
+ [`Class2::CONVERSIONS`](https://github.com/sshaw/class2/blob/517239afc76a4d80677e169958a1dc7836726659/lib/class2.rb#L14-L29)
125
+
61
126
  ### Namespaces
62
127
 
63
128
  `Class2` can use an exiting namespace or create a new one:
@@ -82,9 +147,13 @@ New::Namespace::User.new(:name => "sshaw")
82
147
 
83
148
  `Class2` uses
84
149
  [`String#classify`](http://api.rubyonrails.org/classes/String.html#method-i-classify)
85
- to turn keys into class names. `:foo` will be `Foo`, `:foo_bars` will
86
- be `FooBar`. It also uses it to turn plural attribute names into
87
- singular classes. An `:addresses` attribute will result in a class named
150
+ to turn keys into class names: `:foo` will be `Foo`, `:foo_bars` will
151
+ be `FooBar`.
152
+
153
+ Plural keys with an array value are always assumed to be accessors for
154
+ a collection and will default to returning an `Array`. `#classify` is
155
+ used to derive the class names from the plural attribute names. An
156
+ `:addresses` key with an `Array` value will result in a class named
88
157
  `Address` being created.
89
158
 
90
159
  Plurality is determined by [`String#pluralize`](http://api.rubyonrails.org/classes/String.html#method-i-pluralize).
@@ -115,6 +184,41 @@ end
115
184
  User.new(:name => "sshaw").first_initial
116
185
  ```
117
186
 
187
+ ## Issues
188
+
189
+ Can't use plural attributes that are not collections.
190
+ Here we want `:users` to be an attribute, not a type:
191
+
192
+ ```rb
193
+ Class2(:foo => [ :users => [ :id, :age, :foo ] ])
194
+ ```
195
+
196
+ Instead you can use a `Set`
197
+
198
+ ```rb
199
+ Class2(:foo => [ :users => Set.new([ :id, :age, :foo ]) ])
200
+ ```
201
+
202
+ ---
203
+
204
+ Building classes from example hashes will fail for arrays of scalars. For example:
205
+
206
+ ```rb
207
+ user = { :name => "sshaw", :hobbies => ["screen-staring", "other thangz"] }
208
+ Class2(:user => user)
209
+ ```
210
+
211
+ Will fail because of `:hobbies`' value. It must be converted to:
212
+
213
+ ```rb
214
+ user = { :name => "sshaw", :hobbies => [] }
215
+ Class2(:user => user)
216
+ ```
217
+
218
+ ---
219
+
220
+ Others, I'm sure.
221
+
118
222
  ## See Also
119
223
 
120
224
  The Perl module that served as the inspiration: [`MooseX::NestedAttributesConstructor`](https://github.com/sshaw/MooseX-NestedAttributesConstructor).
data/class2.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Skye Shaw"]
10
10
  spec.email = ["skye.shaw@gmail.com"]
11
11
 
12
- spec.summary = %q{Easily create hierarchies of classes that support nested attributes, equality, and more.}
12
+ spec.summary = %q{Easily create hierarchies of classes that support nested attributes, type conversion, equality, and more.}
13
13
  spec.homepage = "https://github.com/sshaw/class2"
14
14
  spec.license = "MIT"
15
15
 
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "activesupport", ">= 3.0"
21
+ spec.add_dependency "activesupport", ">= 3.2"
22
22
 
23
23
  spec.add_development_dependency "bundler", "~> 1.10"
24
24
  spec.add_development_dependency "rake", "~> 10.0"
data/lib/class2.rb CHANGED
@@ -1,14 +1,33 @@
1
1
  # coding: utf-8
2
2
 
3
- require "class2/version"
3
+ require "date"
4
4
  require "active_support/core_ext/module"
5
5
  require "active_support/core_ext/string"
6
6
 
7
+ require "class2/version"
8
+
7
9
  def Class2(*args)
8
10
  Class2.new(*args)
9
11
  end
10
12
 
11
13
  class Class2
14
+ CONVERSIONS = {
15
+ Array => lambda { |v| "Array(#{v})" },
16
+ Date => lambda { |v| "Date.parse(#{v})" },
17
+ DateTime => lambda { |v| "DateTime.parse(#{v})" },
18
+ Float => lambda { |v| "Float(#{v})" },
19
+ Hash => lambda { |v| sprintf "%s.respond_to?(:to_h) ? %s.to_h : %s", v, v, v },
20
+ Integer => lambda { |v| "Integer(#{v})" },
21
+ String => lambda { |v| "String(#{v})" },
22
+ TrueClass => lambda do |v|
23
+ sprintf '["1", 1, 1.0, true].freeze.include?(%s.is_a?(String) ? %s.strip : %s)', v, v, v
24
+ end
25
+ }
26
+
27
+ CONVERSIONS[FalseClass] = CONVERSIONS[TrueClass]
28
+ CONVERSIONS[Fixnum] = CONVERSIONS[Integer]
29
+ CONVERSIONS.default = lambda { |v| v }
30
+
12
31
  class << self
13
32
  def new(*argz)
14
33
  specs = argz
@@ -35,17 +54,50 @@ class Class2
35
54
  end
36
55
  end
37
56
 
38
- def make_class(namespace, name, attributes)
57
+ def split_and_normalize_attributes(attributes)
58
+ nested = []
59
+ simple = []
60
+
39
61
  attributes = [attributes] unless attributes.is_a?(Array)
62
+ attributes.compact.each do |attr|
63
+ # Just an attribute name, no type, so use default type String
64
+ if !attr.is_a?(Hash)
65
+ simple << { attr => nil }
66
+ next
67
+ end
68
+
69
+ attr.each do |k, v|
70
+ if v.is_a?(Hash) || v.is_a?(Array)
71
+ if v.empty?
72
+ # If it's empty it's not a nested spec, the attributes type is a Hash or Array
73
+ simple << { k => v.class }
74
+ else
75
+ nested << { k => v }
76
+ end
77
+ else
78
+ # Type can be a class name or an instance
79
+ # If it's an instance, use its type
80
+ v = v.class unless v.is_a?(Class)
81
+ simple << { k => v }
82
+ end
83
+ end
84
+ end
85
+
86
+ [ nested, simple ]
87
+ end
40
88
 
41
- nested, simple = attributes.compact.partition { |e| e.is_a?(Hash) }
89
+ def make_class(namespace, name, attributes)
90
+ nested, simple = split_and_normalize_attributes(attributes)
42
91
  nested.each do |object|
43
92
  object.each { |klass, attrs| make_class(namespace, klass, attrs) }
44
93
  end
45
94
 
95
+ make_method_name = lambda { |x| x.to_s.gsub(/[^\w]+/, "_") } # good enough
96
+
46
97
  klass = Class.new do
47
98
  def initialize(attributes = nil)
48
- assign_attributes(attributes || {})
99
+ return unless attributes.is_a?(Hash)
100
+ assign_attributes(attributes)
49
101
  end
50
102
 
51
103
  class_eval <<-CODE
@@ -62,21 +114,54 @@ class Class2
62
114
 
63
115
  def to_h
64
116
  hash = {}
65
- (#{simple + nested.map { |n| n.keys.first }}).each do |name|
117
+ (#{simple.map { |n| n.keys.first } + nested.map { |n| n.keys.first }}).each do |name|
66
118
  hash[name] = public_send(name)
67
- hash[name] = hash[name].to_h if hash[name].respond_to?(:to_h)
119
+ next unless hash[name].respond_to?(:to_h)
120
+
121
+ errors = [ ArgumentError, TypeError ]
122
+ # Seems needlessly complicated, why doesn't Hash() do some of this?
123
+ begin
124
+ hash[name] = hash[name].to_h
125
+ # to_h is dependent on its contents
126
+ rescue *errors
127
+ next unless hash[name].is_a?(Enumerable)
128
+ hash[name] = hash[name].map do |e|
129
+ begin
130
+ e.respond_to?(:to_h) ? e.to_h : e
131
+ rescue *errors
132
+ # Give up
133
+ end
134
+ end
135
+ end
68
136
  end
69
137
 
70
138
  hash
71
139
  end
140
+
141
+ def __nested_attributes
142
+ #{nested.map { |n| n.keys.first.to_sym }}.freeze
143
+ end
144
+
145
+ private :__nested_attributes
72
146
  CODE
73
147
 
74
- simple.each { |method| attr_accessor method }
148
+ simple.each do |cfg|
149
+ method, type = cfg.first
150
+ method = make_method_name[method]
151
+
152
+ attr_reader method
153
+
154
+ class_eval <<-CODE
155
+ def #{method}=(v)
156
+ @#{method} = #{CONVERSIONS[type]["v"]}
157
+ end
158
+ CODE
159
+ end
75
160
 
76
161
  nested.map { |n| n.keys.first }.each do |method, _|
162
+ method = make_method_name[method]
77
163
  attr_writer method
78
164
 
79
- method = method.to_s
80
165
  retval = method == method.pluralize ? "[]" : "#{method.classify}.new"
81
166
  class_eval <<-CODE
82
167
  def #{method}
@@ -89,17 +174,20 @@ class Class2
89
174
 
90
175
  def assign_attributes(attributes)
91
176
  attributes.each do |key, value|
92
- if value.is_a?(Hash) || value.is_a?(Array)
93
- name = key.to_s.classify
177
+ if __nested_attributes.include?(key.respond_to?(:to_sym) ? key.to_sym : key) &&
178
+ (value.is_a?(Hash) || value.is_a?(Array))
179
+
180
+ name = key.to_s.classify
94
181
 
95
182
  # Only look in our namespace to prevent unwanted lookup
96
183
  next unless self.class.parent.const_defined?(name)
97
- klass = self.class.parent.const_get(name)
98
184
 
185
+ klass = self.class.parent.const_get(name)
99
186
  value = value.is_a?(Hash) ? klass.new(value) : value.map { |v| klass.new(v) }
100
187
  end
101
188
 
102
- public_send("#{key}=", value)
189
+ method = "#{key}="
190
+ public_send(method, value) if respond_to?(method)
103
191
  end
104
192
  end
105
193
  end
@@ -1,3 +1,3 @@
1
1
  class Class2
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  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.0.1
4
+ version: 0.0.2
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-08-06 00:00:00.000000000 Z
11
+ date: 2017-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
19
+ version: '3.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '3.0'
26
+ version: '3.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -75,6 +75,7 @@ extra_rdoc_files: []
75
75
  files:
76
76
  - ".gitignore"
77
77
  - ".travis.yml"
78
+ - Changes
78
79
  - Gemfile
79
80
  - LICENSE.txt
80
81
  - README.md
@@ -105,6 +106,6 @@ rubyforge_project:
105
106
  rubygems_version: 2.6.11
106
107
  signing_key:
107
108
  specification_version: 4
108
- summary: Easily create hierarchies of classes that support nested attributes, equality,
109
- and more.
109
+ summary: Easily create hierarchies of classes that support nested attributes, type
110
+ conversion, equality, and more.
110
111
  test_files: []