structure 0.21.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,6 +1,11 @@
1
1
  source :rubygems
2
2
  gemspec
3
+
3
4
  gem 'activesupport', '~> 3.0'
4
- gem 'json', :platform => [:mri_18, :jruby, :rbx]
5
+ if RUBY_VERSION.include? '1.8'
6
+ gem 'json'
7
+ gem 'minitest'
8
+ end
9
+
5
10
  gem 'pry' unless ENV['CI']
6
11
  gem 'rake'
data/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  [![travis](https://secure.travis-ci.org/hakanensari/structure.png?branch=master)](http://travis-ci.org/hakanensari/structure)
4
4
 
5
- Structure is a typed, nestable key/value container.
5
+ Structure is a typed key/value container.
6
6
 
7
7
  class Person < Structure
8
- key :name
9
- many :friends
8
+ key :name
9
+ key :friends, Array, []
10
10
  end
11
11
 
12
12
  Please see the [wiki] [1] for more detail.
data/Rakefile CHANGED
@@ -4,6 +4,6 @@ require 'rake/testtask'
4
4
  task :default => :test
5
5
 
6
6
  Rake::TestTask.new do |test|
7
- test.libs << 'test'
7
+ test.libs += %w{lib test}
8
8
  test.test_files = FileList['test/**/*_test.rb']
9
9
  end
@@ -0,0 +1,58 @@
1
+ begin
2
+ JSON::JSON_LOADED
3
+ rescue NameError
4
+ require 'json'
5
+ end
6
+
7
+ class Structure
8
+ module JSON
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ # Converts structure to its JSON representation
14
+ #
15
+ # @param [Hash] args
16
+ # @return [JSON] a JSON representation of the structure
17
+ def to_json(*args)
18
+ { ::JSON.create_id => self.class.name }.
19
+ merge(@attributes).
20
+ to_json(*args)
21
+ end
22
+
23
+ if defined? ActiveSupport
24
+ # Converts structure to its JSON representation
25
+ #
26
+ # @param [Hash] options
27
+ # @return [JSON] a JSON representation of the structure
28
+ def as_json(options = nil)
29
+ subset = if options
30
+ if only = options[:only]
31
+ @attributes.slice(*Array.wrap(only))
32
+ elsif except = options[:except]
33
+ @attributes.except(*Array.wrap(except))
34
+ else
35
+ @attributes.dup
36
+ end
37
+ else
38
+ @attributes.dup
39
+ end
40
+
41
+ { ::JSON.create_id => self.class.name }.
42
+ merge(subset)
43
+ end
44
+ end
45
+
46
+ module ClassMethods
47
+ # Builds a structure out of its JSON representation
48
+ #
49
+ # @param [Hash] hsh a hashified JSON
50
+ # @return [Structure] a structure
51
+ def json_create(hsh)
52
+ hsh.delete('json_class')
53
+
54
+ new(hsh)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,3 @@
1
1
  class Structure
2
- VERSION = '0.21.0'
2
+ VERSION = '0.22.0'
3
3
  end
data/lib/structure.rb CHANGED
@@ -1,211 +1,190 @@
1
- begin
2
- JSON::JSON_LOADED
3
- rescue NameError
4
- require 'json'
5
- end
6
-
7
- # Structure is a nestable, typed key/value container.
1
+ # Structure is a typed key/value container
8
2
  #
3
+ # @example
9
4
  # class Person < Structure
10
- # key :name
11
- # many :friends
5
+ # key :name
6
+ # key :friends, Array, []
12
7
  # end
13
8
  #
14
9
  class Structure
15
- include Enumerable
16
-
17
- # A namespaced basic object.
18
- #
19
- # If running a legacy Ruby version, we either quote Builder's or
20
- # fabricate one ourselves.
21
- if defined?(BasicObject)
22
- BasicObject = ::BasicObject
23
- elsif defined?(BlankSlate)
24
- BasicObject = ::BlankSlate
25
- else
26
- class BasicObject
27
- instance_methods.each do |mth|
28
- undef_method(mth) unless mth =~ /\A(__|instance_eval)/
10
+ # Summon a Basic Object.
11
+ unless defined? BasicObject
12
+ if defined? BlankSlate
13
+ BasicObject = BlankSlate
14
+ else
15
+ class BasicObject
16
+ instance_methods.each do |mth|
17
+ undef_method(mth) unless mth =~ /__/
18
+ end
29
19
  end
30
20
  end
31
21
  end
32
22
 
33
- # A double that stands in for a yet-to-be-defined class. Otherwise
34
- # known as "lazy evaluation."
23
+ # A wrapper for lazy-evaluating undeclared classes
35
24
  #
36
- # Idea lifted from:
37
- # http://github.com/soveran/ohm/
38
- class Double < BasicObject
25
+ # @note Borrowed from the same-named class in Ohm
26
+ class Wrapper < BasicObject
27
+ # Wraps specified class in a wrapper if it is not already wrapped
28
+ #
29
+ # @param [Class] klass
30
+ # @return [Wrapper]
31
+ def self.wrap(klass)
32
+ klass.class == self ? klass : new(klass.to_s)
33
+ end
34
+
35
+ # Creates a new wrapper for specified class name
36
+ #
37
+ # @param [#to_s] name
39
38
  def initialize(name)
40
- @name = name
39
+ @name = name.to_s
40
+ end
41
+
42
+ # @return [Class] the class of the object
43
+ def class
44
+ Wrapper
41
45
  end
42
46
 
43
- def to_s
44
- @name.to_s
47
+ # Unwraps wrapped class
48
+ #
49
+ # @return [Class] the unwrapped class
50
+ def unwrap
51
+ @name.split('::').inject(::Kernel) do |parent, child|
52
+ parent.const_get(child)
53
+ end
45
54
  end
46
55
 
56
+ private
57
+
47
58
  def method_missing(mth, *args, &block)
48
59
  @unwrapped ? super : @unwrapped = true
49
60
  ::Kernel.const_get(@name).send(mth, *args, &block)
50
61
  ensure
51
62
  @unwrapped = false
52
- end; private :method_missing
63
+ end
53
64
  end
54
65
 
55
- class << self
56
- # Returns attribute keys and their default values.
57
- def defaults
58
- @defaults ||= {}
66
+ # A key definition
67
+ class Definition
68
+ # Creates a key definition
69
+ #
70
+ # @param [Class] type the key type
71
+ # @param [Object] default an optional default value
72
+ def initialize(type, default = nil)
73
+ @wrapper = Wrapper.wrap(type)
74
+ @default = default
75
+ end
76
+
77
+ # @return the default value for the key
78
+ attr :default
79
+
80
+ # @return [Class] the key type
81
+ def type
82
+ @type ||= @wrapper.unwrap
59
83
  end
60
84
 
61
- # Recreates a structure out of its JSON representation.
62
- def json_create(hsh)
63
- hsh.delete('json_class')
64
- new(hsh)
85
+ # Typecasts specified value
86
+ #
87
+ # @param [Object] val a value
88
+ # @raise [TypeError] value isn't a type
89
+ # @return [Object] a typecast value
90
+ def typecast(val)
91
+ if val.nil? || val.is_a?(type)
92
+ val.dup rescue val
93
+ elsif Kernel.respond_to?(type.to_s)
94
+ Kernel.send(type.to_s, val)
95
+ else
96
+ raise TypeError, "#{val} isn't a #{type}"
97
+ end
98
+ end
99
+ end
100
+
101
+ class << self
102
+ # @return [Hash] a collection of keys and their definitions
103
+ def blueprint
104
+ @blueprint ||= {}
65
105
  end
66
106
 
67
- # Defines an attribute.
107
+ # Defines a key
108
+ #
109
+ # @note Key type defaults to +String+ if not specified.
68
110
  #
69
- # Takes a name and, optionally, a type and default value.
70
- def key(name, type = nil, default = nil)
111
+ # @param [#to_sym] name the key name
112
+ # @param [Class] type an optional key type
113
+ # @param [Object] default an optional default value
114
+ # @raise [NameError] name is already taken
115
+ def key(name, type = String, default = nil)
71
116
  name = name.to_sym
72
117
 
73
118
  if method_defined?(name)
74
119
  raise NameError, "#{name} is taken"
75
120
  end
76
121
 
77
- if !default || !type || default.is_a?(type)
78
- defaults[name] = default
79
- else
122
+ if default && !default.is_a?(type)
80
123
  raise TypeError, "#{default} isn't a #{type}"
81
124
  end
82
125
 
83
- define_method(name) { attributes[name] }
126
+ # Add key to blueprint.
127
+ blueprint[name] = Definition.new(type, default)
84
128
 
85
- define_method("#{name}=") do |val|
86
- attributes[name] =
87
- if type.nil? || val.nil? || val.is_a?(type)
88
- val.dup rescue val
89
- elsif Kernel.respond_to?(type.to_s)
90
- Kernel.send(type.to_s, val)
91
- else
92
- raise TypeError, "#{val} isn't a #{type}"
93
- end
129
+ # Define getter.
130
+ define_method(name) do
131
+ @attributes[name]
94
132
  end
95
- end
96
-
97
- # Defines an attribute that is an array and defaults to an empty
98
- # one.
99
- def many(name)
100
- key name, Array, []
101
- end
102
133
 
103
- alias_method :new_original, :new
104
-
105
- def new(hsh)
106
- hsh = hsh.inject({}) do |a, (k, v)|
107
- a[Inflector.underscore(k)] =
108
- case v
109
- when Hash
110
- Structure.new(v)
111
- when Array
112
- v.map { |e| e.is_a?(Hash) ? Structure.new(e) : e }
113
- else
114
- v
115
- end
116
- a
117
- end
118
-
119
- klass = Class.new(Structure) do
120
- hsh.keys.each { |k| key k }
134
+ # Define setter.
135
+ define_method("#{name}=") do |val|
136
+ @attributes[name] = self.class.blueprint[name].typecast(val)
121
137
  end
122
-
123
- klass.new(hsh)
124
138
  end
125
139
 
126
- # Lazy-eval undefined constants, typically other structures,
127
- # assuming they will be defined later in the text.
128
- def const_missing(name)
129
- Double.new(name)
130
- end; private :const_missing
140
+ private
131
141
 
132
- def inherited(child)
133
- if self.eql? Structure
134
- class << child; alias_method :new, :new_original; end
135
- end
136
- end; private :inherited
142
+ def const_missing(name)
143
+ Wrapper.new(name)
144
+ end
137
145
  end
138
146
 
139
- # Creates a new structure.
147
+ # Builds a new structure
140
148
  #
141
- # A hash, if provided, seeds the attributes.
149
+ # @param [Hash] hsh a hash of key-value pairs
142
150
  def initialize(hsh = {})
143
- @attributes = defaults.inject({}) do |a, (k, v)|
144
- a[k] = v.dup rescue v
151
+ @attributes = blueprint.inject({}) do |a, (k, v)|
152
+ a[k] = v.default.dup rescue v.default
145
153
  a
146
154
  end
147
155
 
148
156
  hsh.each { |k, v| self.send("#{k}=", v) }
149
157
  end
150
158
 
151
- # The attributes that make up the structure.
152
- attr :attributes
153
-
154
- # Calls block once for each attribute in the structure, passing that
155
- # attribute as a parameter.
156
- def each(&block)
157
- attributes.each { |v| block.call(v) }
158
- end
159
-
160
- # Converts structure to a hash.
159
+ # @return [Hash] a hash representation of the structure
161
160
  def to_hash
162
- attributes.inject({}) do |a, (k, v)|
163
- a[k] =
164
- if v.respond_to?(:to_hash)
165
- v.to_hash
166
- elsif v.is_a?(Array)
167
- v.map { |e| e.respond_to?(:to_hash) ? e.to_hash : e }
168
- else
169
- v
170
- end
161
+ @attributes.inject({}) do |a, (k, v)|
162
+ a[k] = if v.respond_to?(:to_hash)
163
+ v.to_hash
164
+ elsif v.is_a?(Array)
165
+ v.map { |e| e.respond_to?(:to_hash) ? e.to_hash : e }
166
+ else
167
+ v
168
+ end
171
169
 
172
170
  a
173
171
  end
174
172
  end
175
173
 
176
- # Converts structure to its JSON representation.
177
- def to_json(*args)
178
- { JSON.create_id => self.class.name }.
179
- merge(attributes).
180
- to_json(*args)
181
- end
182
-
183
- # Compares this object with another object for equality. A Structure
184
- # is equal to the other object when both are of the same class and
185
- # the their attributes are the same.
174
+ # Compares this object with another object for equality
175
+ #
176
+ # A structure is equal to another object when both are of the same
177
+ # class and their attributes are the same.
178
+ #
179
+ # @param [Object] other another object
180
+ # @return [true, false]
186
181
  def ==(other)
187
182
  other.is_a?(self.class) && attributes == other.attributes
188
183
  end
189
184
 
190
- def defaults
191
- self.class.defaults
192
- end; private :defaults
185
+ private
193
186
 
194
- module Inflector
195
- # Makes an underscored, lowercase form from the expression in the
196
- # string.
197
- #
198
- # You know where this is lifted from.
199
- def self.underscore(camel_cased_word)
200
- word = camel_cased_word.to_s.dup
201
- word.gsub!(/::/, '/')
202
- word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
203
- word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
204
- word.tr!("-", "_")
205
- word.downcase!
206
- word
207
- end
187
+ def blueprint
188
+ self.class.blueprint
208
189
  end
209
190
  end
210
-
211
- require 'structure/ext/active_support' if defined?(Rails)
data/structure.gemspec CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |s|
9
9
  s.authors = ['Hakan Ensari']
10
10
  s.email = ['code@papercavalier.com']
11
11
  s.homepage = 'http://github.com/hakanensari/structure'
12
- s.summary = 'A typed, nestable key/value container'
13
- s.description = 'Structure is a typed, nestable key/value container.'
12
+ s.summary = 'A typed key/value container'
13
+ s.description = 'Structure is a typed key/value container.'
14
14
 
15
15
  s.rubyforge_project = 'structure'
16
16
 
@@ -1,51 +1,33 @@
1
- require 'bundler/setup'
2
- require 'test/unit'
1
+ require 'minitest/autorun'
2
+
3
3
  begin
4
4
  require 'pry'
5
5
  rescue LoadError
6
6
  end
7
- require File.expand_path('../../lib/structure', __FILE__)
7
+
8
+ require 'structure'
9
+ require 'structure/json'
8
10
 
9
11
  class Person < Structure
10
- key :name
11
- key :location, Location
12
- many :friends
12
+ key :name
13
+ key :friends, Array, []
14
+ key :city, City
13
15
  end
14
16
 
15
- class Location < Structure
16
- key :lon, Float
17
- key :lat, Float
17
+ class City < Structure
18
+ key :name
18
19
  end
19
20
 
20
- class TestStructure < Test::Unit::TestCase
21
- def test_double
22
- double = Structure::Double.new(:Foo)
23
- assert_equal 'Foo', "#{double}"
24
- assert_raise(NameError) { double.class }
25
- ::Kernel.const_set(:Foo, 1)
26
- assert_kind_of Fixnum, double
27
- end
28
-
29
- def test_anonymous
30
- hsh = { 'FirstName' => 'John', 'LastName' => 'Doe' }
31
- str = Structure.new(hsh)
32
- assert_equal hsh['FirstName'], str.first_name
33
- assert_equal hsh['LastName'], str.last_name
34
- assert_raise(NoMethodError) { str.FirstName }
35
- end
21
+ class TestStructure < MiniTest::Unit::TestCase
22
+ def test_lazy_evaluation
23
+ wrapper = Structure::Wrapper.new(:Foo)
24
+ assert_raises(NameError) { wrapper.bar }
25
+ assert_raises(NameError) { wrapper.unwrap.bar }
36
26
 
37
- def test_anonymous_recursion
38
- hsh = { 'Name' => 'John',
39
- 'Address' => { 'City' => 'Oslo' },
40
- 'Friends' => [{ 'Name' => 'Jane' }]
41
- }
42
- str = Structure.new(hsh)
43
- assert_equal hsh['Address']['City'], str.address.city
44
- assert_equal hsh['Friends'].first['Name'], str.friends.first.name
45
- end
46
-
47
- def test_enumeration
48
- assert_respond_to Person.new, :map
27
+ klass = Class.new { def self.bar; end }
28
+ ::Kernel.const_set(:Foo, klass)
29
+ assert_respond_to wrapper, :bar
30
+ assert_equal Foo, wrapper.unwrap
49
31
  end
50
32
 
51
33
  def test_accessors
@@ -54,24 +36,24 @@ class TestStructure < Test::Unit::TestCase
54
36
  end
55
37
 
56
38
  def test_key_errors
57
- assert_raise(NameError) { Person.key :class }
58
- assert_raise(TypeError) { Person.key :foo, String, 1 }
39
+ assert_raises(NameError) { Person.key :class }
40
+ assert_raises(TypeError) { Person.key :foo, Hash, 1 }
59
41
  end
60
42
 
61
43
  def test_key_defaults
62
44
  assert_equal [], Person.new.friends
63
45
  end
64
46
 
65
- def test_typechecking
66
- loc = Location.new
67
- loc.lon = "1"
68
- assert_kind_of Float, loc.lon
47
+ def test_typecasting
48
+ person = Person.new
49
+ person.name = 123
50
+ assert_kind_of String, person.name
69
51
 
70
- loc.lon = nil
71
- assert_nil loc.lon
52
+ person.name = nil
53
+ assert_nil person.name
72
54
  end
73
55
 
74
- def test_array_type
56
+ def test_many_relationship
75
57
  person = Person.new
76
58
  assert_equal [], person.friends
77
59
 
@@ -80,26 +62,36 @@ class TestStructure < Test::Unit::TestCase
80
62
  assert_equal 0, person.friends.first.friends.size
81
63
  end
82
64
 
83
- # def test_to_hash
84
- # person = Person.new(:name => 'John')
85
- # person.friends << Person.new(:name => 'Jane')
86
- # assert_equal 'John', person.to_hash[:name]
87
- # assert_equal 'Jane', person.to_hash[:friends].first[:name]
88
- # end
65
+ def test_new
66
+ person = Person.new(:name => 'John')
67
+ assert_equal 'John', person.name
68
+
69
+ other = Person.new(:name => 'Jane', :friends => [person])
70
+ assert_equal 'John', other.friends.first.name
71
+ end
72
+
73
+ def test_to_hash
74
+ person = Person.new(:name => 'John')
75
+ person.friends << Person.new(:name => 'Jane')
76
+ hash = person.to_hash
77
+
78
+ assert_equal 'John', hash[:name]
79
+ assert_equal 'Jane', hash[:friends].first[:name]
80
+ end
89
81
 
90
82
  def test_json
91
- person = Person.new
92
- person.friends << Person.new
83
+ Person.send :include, Structure::JSON
84
+
85
+ person = Person.new(:name => 'John')
86
+ person.friends << Person.new(:name => 'Jane')
93
87
  json = person.to_json
94
88
  assert_kind_of Person, JSON.parse(json)
95
89
  assert_kind_of Person, JSON.parse(json).friends.first
96
-
97
90
  assert_equal false, person.respond_to?(:as_json)
98
91
 
99
92
  require 'active_support/ordered_hash'
100
93
  require 'active_support/json'
101
- require 'structure/ext/active_support'
102
-
94
+ load 'structure/json.rb'
103
95
  assert_equal true, person.as_json(:only => :name).has_key?(:name)
104
96
  assert_equal false, person.as_json(:except => :name).has_key?(:name)
105
97
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: structure
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.0
4
+ version: 0.22.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,9 +9,9 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-09-10 00:00:00.000000000Z
12
+ date: 2011-10-26 00:00:00.000000000 Z
13
13
  dependencies: []
14
- description: Structure is a typed, nestable key/value container.
14
+ description: Structure is a typed key/value container.
15
15
  email:
16
16
  - code@papercavalier.com
17
17
  executables: []
@@ -25,7 +25,7 @@ files:
25
25
  - README.md
26
26
  - Rakefile
27
27
  - lib/structure.rb
28
- - lib/structure/ext/active_support.rb
28
+ - lib/structure/json.rb
29
29
  - lib/structure/version.rb
30
30
  - structure.gemspec
31
31
  - test/structure_test.rb
@@ -43,7 +43,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
43
43
  version: '0'
44
44
  segments:
45
45
  - 0
46
- hash: -3352714224337607479
46
+ hash: 2031698207987898845
47
47
  required_rubygems_version: !ruby/object:Gem::Requirement
48
48
  none: false
49
49
  requirements:
@@ -52,12 +52,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
52
  version: '0'
53
53
  segments:
54
54
  - 0
55
- hash: -3352714224337607479
55
+ hash: 2031698207987898845
56
56
  requirements: []
57
57
  rubyforge_project: structure
58
- rubygems_version: 1.8.6
58
+ rubygems_version: 1.8.10
59
59
  signing_key:
60
60
  specification_version: 3
61
- summary: A typed, nestable key/value container
61
+ summary: A typed key/value container
62
62
  test_files:
63
63
  - test/structure_test.rb
@@ -1,19 +0,0 @@
1
- class Structure
2
- # Converts structure to a JSON representation.
3
- def as_json(options = nil)
4
- subset = if options
5
- if only = options[:only]
6
- attributes.slice(*Array.wrap(only))
7
- elsif except = options[:except]
8
- attributes.except(*Array.wrap(except))
9
- else
10
- attributes.dup
11
- end
12
- else
13
- attributes.dup
14
- end
15
-
16
- { JSON.create_id => self.class.name }.
17
- merge(subset)
18
- end
19
- end