class2 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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: []