structure 0.22.1 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,11 +1,11 @@
1
1
  source :rubygems
2
2
  gemspec
3
3
 
4
- gem 'activesupport', '~> 3.0'
4
+ gem 'activesupport' # Required for testing Rails compatibility
5
+ gem 'pry' unless ENV['CI']
6
+ gem 'rake'
7
+
5
8
  if RUBY_VERSION.include? '1.8'
6
9
  gem 'json'
7
10
  gem 'minitest'
8
11
  end
9
-
10
- gem 'pry' unless ENV['CI']
11
- gem 'rake'
data/LICENSE CHANGED
@@ -1,13 +1,22 @@
1
- DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
- Version 2, December 2004
1
+ (The MIT License)
3
2
 
4
- Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
3
+ Copyright (c) 2011 Hakan Ensari
5
4
 
6
- Everyone is permitted to copy and distribute verbatim or modified
7
- copies of this license document, and changing it is allowed as long
8
- as the name is changed.
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
9
12
 
10
- DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
12
15
 
13
- 0. You just DO WHAT THE FUCK YOU WANT TO.
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,14 +1,8 @@
1
- # Structure
1
+ ![structure](http://f.cl.ly/items/2u2v0e3k2I3w1A0y2e25/ruby.png)
2
2
 
3
- [![travis](https://secure.travis-ci.org/hakanensari/structure.png?branch=master)](http://travis-ci.org/hakanensari/structure)
4
-
5
- Structure is a typed key/value container.
3
+ > Machines only work when they break down and by continually breaking
4
+ > down.
6
5
 
7
- class Person < Structure
8
- key :name
9
- key :friends, Array, []
10
- end
6
+ [Delve into the API.](http://rubydoc.info/github/hakanensari/structure/master/frames)
11
7
 
12
- Please see the [wiki] [1] for more detail.
13
-
14
- [1]: https://github.com/hakanensari/structure/wiki/
8
+ [![travis](https://secure.travis-ci.org/hakanensari/structure.png?branch=master)](http://travis-ci.org/hakanensari/structure)
data/lib/structure.rb CHANGED
@@ -1,189 +1,267 @@
1
- # Structure is a typed key/value container
1
+ begin
2
+ JSON::JSON_LOADED
3
+ rescue NameError
4
+ require 'json'
5
+ end
6
+
7
+ # Structure is a key/value container. On the most basic level, it
8
+ # mirrors the functionality of OpenStruct:
9
+ #
10
+ # require 'structure'
11
+ #
12
+ # record = Structure.new
13
+ # record.name = "John Smith"
14
+ # record.age = 70
15
+ # record.pension = 300
16
+ #
17
+ # puts record.name # -> "John Smith"
18
+ # puts record.address # -> nil
19
+ #
20
+ # Build structures recursively:
21
+ #
22
+ # hash = {
23
+ # "name" => "Australia",
24
+ # "population" => "20000000",
25
+ # "cities" => [
26
+ # {
27
+ # "name" => "Sydney",
28
+ # "population" => "4100000"
29
+ # },
30
+ # {
31
+ # "name" => "Melbourne",
32
+ # "population" => "4000000"
33
+ # } ]
34
+ # }
35
+ #
36
+ # country = Structure.new(hash)
37
+ # puts country.name # -> "Australia"
38
+ # puts country.cities.count # -> 2
39
+ # puts country.cities.first.name # -> "Sydney"
2
40
  #
3
- # @example
4
- # class Person < Structure
5
- # key :name
6
- # key :friends, Array, []
7
- # end
41
+ # Define optionally-typed fields in a structure:
8
42
  #
43
+ # class Price < Structure
44
+ # field :cents, Integer
45
+ # field :currency, :default => "USD"
46
+ # end
47
+ #
48
+ # hash = { "cents" => "100" }
49
+ #
50
+ # price = Price.new(hash)
51
+ # puts price.cents # -> 100
52
+ # puts price.currency # -> "USD"
53
+ #
54
+ # Alternatively, define a proc to cast or otherwise manipulate assigned
55
+ # values:
56
+ #
57
+ # class Product < Structure
58
+ # field :sku, lambda(&:upcase)
59
+ # end
60
+ #
61
+ # product = Product.new(:sku => 'foo-bar')
62
+ # puts product.sku # -> "FOO-BAR"
63
+ #
64
+ # Structures are fully conversant in JSON, which is quite handy in the
65
+ # ephemeral landscape of APIs.
9
66
  class Structure
10
- unless defined? BasicObject
11
- if defined? BlankSlate
12
- BasicObject = BlankSlate
13
- else
14
- class BasicObject
15
- instance_methods.each do |mth|
16
- undef_method(mth) unless mth =~ /__/
17
- end
18
- end
19
- end
20
- end
67
+ class << self
68
+ attr_accessor :blueprint
21
69
 
22
- # A wrapper for lazy-evaluating undeclared classes
23
- #
24
- # @note Somewhat borrowed from the same-named class in Ohm
25
- class Wrapper < BasicObject
26
- # Wraps specified class in a wrapper if it is not already wrapped
27
- #
28
- # @param [Class] klass a class, which may already be wrapped
29
- # @return [Wrapper] the wrapped class
30
- def self.wrap(klass)
31
- klass.class == self ? klass : new(klass.to_s)
70
+ # Builds a structure out of a JSON representation.
71
+ # @param [Hash] hsh a JSON representation translated to a hash
72
+ # @return [Structure] a structure
73
+ def json_create(hsh)
74
+ hsh.delete('json_class')
75
+ new(hsh)
32
76
  end
33
77
 
34
- # Creates a new wrapper for specified class name
35
- #
36
- # @param [#to_s] name
37
- def initialize(name)
38
- @name = name.to_s
78
+ # Creates a field.
79
+ # @overload field(key, opts = {})
80
+ # Creates a field.
81
+ # @param [#to_sym] key the name of the field
82
+ # @param [Hash] opts the options to create the field with
83
+ # @option opts [Object] :default the default value
84
+ # @overload field(key, type, opts = {})
85
+ # Creates a typed field.
86
+ # @param [#to_sym] key the name of the field
87
+ # @param [Class, Proc] type the type to cast assigned values
88
+ # @param [Hash] opts the options to create the field with
89
+ # @option opts [Object] :default the default value
90
+ def field(key, *args)
91
+ opts = args.last.is_a?(Hash) ? args.pop : {}
92
+ default = opts[:default]
93
+ type = args.shift
94
+ @blueprint[key] = { :type => type,
95
+ :default => default }
39
96
  end
40
97
 
41
- # @return [Class] the class of the object
42
- def class
43
- Wrapper
98
+ # Syntactic sugar to create a typed field that defaults to an empty
99
+ # array.
100
+ # @param key the name of the field
101
+ def many(key)
102
+ field(key, Array, :default => [])
44
103
  end
45
104
 
46
- # Unwraps wrapped class
47
- #
48
- # @return [Class] the unwrapped class
49
- def unwrap
50
- @name.split('::').inject(::Kernel) do |parent, child|
51
- parent.const_get(child)
52
- end
53
- end
105
+ private
54
106
 
55
- def method_missing(mth, *args, &block)
56
- @unwrapped ? super : @unwrapped = true
57
- ::Kernel.const_get(@name).send(mth, *args, &block)
58
- ensure
59
- @unwrapped = false
107
+ def inherited(child)
108
+ child.blueprint = blueprint.dup
60
109
  end
61
110
  end
62
111
 
63
- # A key definition
64
- class Definition
65
- # Creates a key definition
66
- #
67
- # @param [Class] type the key type
68
- # @param [Object] default an optional default value
69
- def initialize(type, default = nil)
70
- @wrapper = Wrapper.wrap(type)
71
- @default = typecast(default)
72
- end
73
-
74
- # @return the default value for the key
75
- attr :default
112
+ @blueprint = {}
76
113
 
77
- # @return [Class] the key type
78
- def type
79
- @type ||= @wrapper.unwrap
114
+ # Creates a new structure.
115
+ # @param [Hash] hsh an optional hash to populate fields
116
+ def initialize(hsh = {})
117
+ # It may have improved performance if I had defined these methods
118
+ # on the class level, but I decided to privilege consistency here.
119
+ # Who wouldn't?
120
+ @table = blueprint.inject({}) do |a, (k, v)|
121
+ a.merge new_field(k, v[:type]) => v[:default]
80
122
  end
81
123
 
82
- # Typecasts specified value
83
- #
84
- # @param [Object] val a value
85
- # @raise [TypeError] value isn't a type
86
- # @return [Object] a typecast value
87
- def typecast(val)
88
- if val.nil?
89
- nil
90
- elsif val.is_a?(type)
91
- val.dup rescue val
92
- elsif Kernel.respond_to?(type.to_s)
93
- Kernel.send(type.to_s, val)
94
- else
95
- raise TypeError, "#{val} isn't a #{type}"
96
- end
97
- end
124
+ marshal_load(hsh)
98
125
  end
99
126
 
100
- class << self
101
- # @return [Hash] a collection of keys and their definitions
102
- def blueprint
103
- @blueprint ||= {}
127
+ # Deletes a field.
128
+ # @param [#to_sym] key
129
+ # @return [Object] the value of the deleted field
130
+ def delete_field(key)
131
+ key = key.to_sym
132
+ class << self; self; end.class_eval do
133
+ [key, "#{key}="].each { |m| remove_method m }
104
134
  end
105
135
 
106
- # Defines a key
107
- #
108
- # @note Key type defaults to +String+ if not specified.
109
- #
110
- # @param [#to_sym] name the key name
111
- # @param [Class] type an optional key type
112
- # @param [Object] default an optional default value
113
- # @raise [NameError] name is already taken
114
- def key(name, type = String, default = nil)
115
- if method_defined?(name = name.to_sym)
116
- raise NameError, "#{name} is taken"
117
- end
136
+ @table.delete key
137
+ end
118
138
 
119
- blueprint[name] = Definition.new(type, default)
120
- define_method(name) { @attributes[name] }
121
- define_method("#{name}=") do |val|
122
- modifiable[name] = blueprint[name].typecast(val)
123
- end
139
+ # Provides marshalling support for use by the Marshal library.
140
+ # @return [Hash] a hash of the keys and values of the structure
141
+ def marshal_dump
142
+ @table.inject({}) do |a, (k, v)|
143
+ a.merge k => recursively_dump(v)
124
144
  end
145
+ end
125
146
 
126
- def const_missing(name)
127
- Wrapper.new(name)
147
+ # Provides marshalling support for use by the Marshal library.
148
+ # @param [Hash] hsh a hash of keys and values to populate the
149
+ # structure
150
+ def marshal_load(hsh)
151
+ hsh.each do |k, v|
152
+ self.send("#{new_field(k)}=", v)
128
153
  end
129
154
  end
130
155
 
131
- # Builds a new structure
132
- #
133
- # @param [Hash] hsh a hash of key-value pairs
134
- def initialize(hsh = {})
135
- @attributes = blueprint.inject({}) do |a, (k, v)|
136
- a[k] = v.default.dup rescue v.default
137
- a
138
- end
156
+ # @return [String] a JSON representation of the structure
157
+ def to_json(*args)
158
+ { JSON.create_id => self.class.name }.
159
+ merge(marshal_dump).
160
+ to_json(*args)
161
+ end
139
162
 
140
- hsh.each { |k, v| self.send("#{k}=", v) }
163
+ # @return [Boolean] whether the object and +other+ are equal
164
+ def ==(other)
165
+ other.is_a?(Structure) && @table == other.table
141
166
  end
142
167
 
143
- # @return [Hash] a hash representation of the structure
144
- def to_hash
145
- @attributes.inject({}) { |a, (k, v)| a.merge k => hashify(v) }
168
+ protected
169
+
170
+ attr :table
171
+
172
+ private
173
+
174
+ def blueprint
175
+ self.class.blueprint
176
+ end
177
+
178
+ def initialize_copy(orig)
179
+ super
180
+ @table = @table.dup
146
181
  end
147
182
 
148
- def hashify(obj)
149
- if obj.respond_to? :to_hash
150
- obj.to_hash
151
- elsif obj.is_a? Array
152
- obj.map { |e| hashify(e) }
183
+ def method_missing(mth, *args)
184
+ name = mth.to_s
185
+ len = args.length
186
+ if name.chomp!('=') && mth != :[]=
187
+ modifiable[new_field(name)] = recursively_load(args.first)
188
+ elsif len == 0
189
+ @table[new_field(mth)]
153
190
  else
154
- obj
191
+ super
155
192
  end
156
193
  end
157
- private :hashify
158
-
159
- # Compares this object with another object for equality
160
- #
161
- # A structure is equal to another object when both are of the same
162
- # class and their attributes are the same.
163
- #
164
- # @param [Object] other another object
165
- # @return [true, false]
166
- def ==(other)
167
- other.is_a?(self.class) && attributes == other.attributes
194
+
195
+ def modifiable
196
+ if frozen?
197
+ raise RuntimeError, "can't modify frozen #{self.class}", caller(3)
198
+ end
199
+
200
+ @table
168
201
  end
169
202
 
170
- def blueprint
171
- self.class.blueprint
203
+ def new_field(key, type = nil)
204
+ key = key.to_sym
205
+ unless self.respond_to?(key)
206
+ class << self; self; end.class_eval do
207
+ define_method(key) { @table[key] }
208
+
209
+ assignment =
210
+ case type
211
+ when nil
212
+ lambda { |v| modifiable[key] = recursively_load(v) }
213
+ when Proc
214
+ lambda { |v| modifiable[key] = type.call(v) }
215
+ when Class
216
+ mth = type.to_s.to_sym
217
+ if Kernel.respond_to?(mth)
218
+ lambda { |v|
219
+ modifiable[key] = v.nil? ? nil : Kernel.send(mth, v)
220
+ }
221
+ else
222
+ lambda { |v|
223
+ modifiable[key] =
224
+ if v.nil? || v.is_a?(type)
225
+ v
226
+ else
227
+ raise TypeError, "#{v} isn't a #{type}"
228
+ end
229
+ }
230
+ end
231
+ else
232
+ raise TypeError, "#{type} isn't a valid type"
233
+ end
234
+
235
+ define_method("#{key}=", assignment)
236
+ end
237
+ end
238
+
239
+ key
172
240
  end
173
- private :blueprint
174
241
 
175
- # Used internally to check if the structure is frozen or not before
176
- # updating a value
177
- #
178
- # @note Borrowed from OpenStruct
179
- def modifiable
180
- begin
181
- @modifiable = true
182
- rescue
183
- raise TypeError, "can't modify frozen #{self.class}", caller(3)
242
+ def recursively_dump(val)
243
+ if val.respond_to? :marshal_dump
244
+ val.marshal_dump
245
+ elsif val.is_a? Array
246
+ val.map { |v| recursively_dump(v) }
247
+ else
248
+ val
249
+ end
250
+ end
251
+
252
+ def recursively_load(val)
253
+ case val
254
+ when Hash
255
+ self.class.new(val)
256
+ when Array
257
+ val.map { |v| recursively_load(v) }
258
+ else
259
+ val
184
260
  end
261
+ end
185
262
 
186
- @attributes
263
+ if defined? ActiveSupport
264
+ require 'structure/ext/active_support'
265
+ include Ext::ActiveSupport
187
266
  end
188
- private :modifiable
189
267
  end
@@ -0,0 +1,22 @@
1
+ class Structure
2
+ module Ext
3
+ module ActiveSupport
4
+ def as_json(options = nil)
5
+ subset = if options
6
+ if only = options[:only]
7
+ marshal_dump.slice(*Array.wrap(only))
8
+ elsif except = options[:except]
9
+ marshal_dump.except(*Array.wrap(except))
10
+ else
11
+ marshal_dump
12
+ end
13
+ else
14
+ marshal_dump
15
+ end
16
+
17
+ { JSON.create_id => self.class.name }.
18
+ merge(subset)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  class Structure
2
- VERSION = '0.22.1'
2
+ VERSION = '0.23.0'
3
3
  end
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 key/value container'
13
- s.description = 'Structure is a typed key/value container.'
12
+ s.summary = 'A key/value container'
13
+ s.description = 'Structure is a key/value container.'
14
14
 
15
15
  s.rubyforge_project = 'structure'
16
16
 
@@ -0,0 +1,60 @@
1
+ require File.expand_path('../helper.rb', __FILE__)
2
+
3
+ class Product < Structure
4
+ field :title
5
+ field :sku, lambda(&:upcase)
6
+ field :cents, Integer
7
+ field :currency, String, :default => 'USD'
8
+ field :in_stock, :default => true
9
+ many :related
10
+ end
11
+
12
+ class Foo < Structure
13
+ field :bar, Hash
14
+ end
15
+
16
+ class TestDefinedStructure < MiniTest::Unit::TestCase
17
+ def setup
18
+ @product = Product.new(:title => 'Widget')
19
+ end
20
+
21
+ def test_inheritance
22
+ assert_equal 'USD', Class.new(Product).new.currency
23
+ end
24
+
25
+ def test_equal_value
26
+ assert @product == Class.new(Product).new(:title => 'Widget')
27
+ refute @product == Product.new(:title => 'Widget', :sku => '123')
28
+ end
29
+
30
+ def test_casting
31
+ @product.title = 1
32
+ assert_kind_of Integer, @product.title
33
+
34
+ @product.sku = 'sku-123'
35
+ assert_equal 'SKU-123', @product.sku
36
+
37
+ @product.cents = '1'
38
+ assert_kind_of Integer, @product.cents
39
+
40
+ @product.related = '1'
41
+ assert_kind_of Array, @product.related
42
+ end
43
+
44
+ def test_default_values
45
+ assert_equal nil, @product.cents
46
+ assert_equal 'USD', @product.currency
47
+ assert_equal true, @product.in_stock
48
+ assert_equal [], @product.related
49
+ end
50
+
51
+ def test_hashes_with_recursion
52
+ foo = Foo.new('bar' => { 'baz' => 1 })
53
+ hsh = foo.marshal_dump
54
+ foo.marshal_load(hsh)
55
+ assert_equal({ 'baz' => 1 }, foo.bar)
56
+
57
+ json = foo.to_json
58
+ assert foo, JSON.parse(json)
59
+ end
60
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'minitest/autorun'
2
+
3
+ begin
4
+ require 'pry'
5
+ rescue LoadError
6
+ end
7
+
8
+ require 'structure'
@@ -1,109 +1,161 @@
1
- require 'minitest/autorun'
1
+ require File.expand_path('../helper.rb', __FILE__)
2
2
 
3
- begin
4
- require 'pry'
5
- rescue LoadError
6
- end
7
-
8
- require 'structure'
9
- require 'structure/json'
3
+ # Most tests below are borrowed from RubySpec.
4
+ class TestStructure < MiniTest::Unit::TestCase
5
+ def setup
6
+ @person = Structure.new(:name => 'John')
7
+ end
10
8
 
11
- class Person < Structure
12
- key :name
13
- key :friends, Array, []
14
- key :city, City
15
- end
9
+ def test_delete_field
10
+ @person.delete_field(:name)
11
+ assert_nil @person.send(:table)[:name]
12
+ refute_respond_to(@person, :name)
13
+ refute_respond_to(@person, :name=)
14
+ end
16
15
 
17
- class City < Structure
18
- key :name
19
- end
16
+ def test_element_reference
17
+ assert_raises(NoMethodError) { @person[1] }
18
+ end
20
19
 
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 }
26
-
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
20
+ def test_element_set
21
+ assert_raises(NoMethodError) { @person[1] = 2 }
31
22
  end
32
23
 
33
- def test_accessors
34
- assert_respond_to Person.new, :name
35
- assert_respond_to Person.new, :name=
24
+ def test_equal_value
25
+ refute @person == 'foo'
26
+ assert @person == @person
27
+ assert @person == Structure.new(:name => 'John')
28
+ assert @person == Class.new(Structure).new(:name => 'John')
29
+ refute @person == Structure.new(:name => 'Johnny')
30
+ refute @person == Structure.new(:name => 'John', :age => 20)
36
31
  end
37
32
 
38
- def test_key_errors
39
- assert_raises(NameError) { Person.key :class }
40
- assert_raises(TypeError) { Person.key :foo, Hash, 1 }
33
+ def test_frozen
34
+ @person.freeze
35
+
36
+ assert_equal 'John', @person.name
37
+ assert_raises(RuntimeError) { @person.age = 42 }
38
+ assert_raises(RuntimeError) { @person.state = :new }
39
+
40
+ c = @person.clone
41
+ assert_equal 'John', c.name
42
+ assert_raises(RuntimeError) { c.age = 42 }
43
+ assert_raises(RuntimeError) { c.state = :new }
44
+
45
+ d = @person.dup
46
+ assert_equal 'John', d.name
47
+ d.age = 42
48
+ assert_equal 42, d.age
41
49
  end
42
50
 
43
- def test_key_defaults
44
- assert_equal [], Person.new.friends
51
+ def test_initialize_copy
52
+ d = @person.dup
53
+ d.name = 'Jane'
54
+ assert_equal 'Jane', d.name
55
+ assert_equal 'John', @person.name
56
+
57
+ @person.friends = ['Joe']
58
+ d = @person.dup
59
+ d.friends = ['Jim']
60
+ assert_equal ['Jim'], d.friends
61
+ assert_equal ['Joe'], @person.friends
45
62
  end
46
63
 
47
- def test_typecasting
48
- person = Person.new
49
- person.name = 123
50
- assert_kind_of String, person.name
64
+ def test_json
65
+ friend = Structure.new(:name => 'Jane')
66
+ @person.friend = friend
67
+ @person.cities = ['Zurich']
68
+ json = '{"json_class":"Structure",
69
+ "name":"John",
70
+ "friend":{"name":"Jane"},
71
+ "cities":["Zurich"]}'.gsub(/\s+/, '')
72
+ assert_equal @person, JSON.parse(json)
73
+ assert_equal friend, JSON.parse(json).friend
74
+ assert_equal 'Zurich', JSON.parse(json).cities.first
75
+
76
+ refute_respond_to @person, :as_json
77
+ require 'active_support/ordered_hash'
78
+ require 'active_support/json'
79
+ load 'structure.rb'
80
+ assert @person.as_json(:only => :name).has_key?(:name)
81
+ refute @person.as_json(:except => :name).has_key?(:name)
82
+ end
51
83
 
52
- person.name = nil
53
- assert_nil person.name
84
+ def test_marshaling
85
+ assert_equal({ :name => 'John' }, @person.marshal_dump)
86
+ @person.marshal_load(:age => 20, :name => 'Jane')
87
+ assert_equal 20, @person.age
88
+ assert_equal 'Jane', @person.name
54
89
  end
55
90
 
56
- def test_many_relationship
57
- person = Person.new
58
- assert_equal [], person.friends
91
+ def test_method_missing
92
+ @person.test = 'test'
93
+ assert_respond_to @person, :test
94
+ assert_respond_to @person, :test=
95
+ assert_equal 'test', @person.test
96
+ assert_equal 'test', @person.send(:table)[:test]
97
+ @person.test = 'changed'
98
+ assert_equal 'changed', @person.test
99
+
59
100
 
60
- person.friends << Person.new
61
- assert_equal 1, person.friends.size
62
- assert_equal 0, person.friends.first.friends.size
101
+ @person.send(:table)[:age] = 20
102
+ assert_equal 20, @person.age
103
+
104
+ assert_raises(NoMethodError) { @person.gender(1) }
105
+ assert_nil @person.gender
106
+
107
+ @person.freeze
108
+ assert_raises(RuntimeError) { @person.gender = 'male' }
63
109
  end
64
110
 
65
111
  def test_new
66
- person = Person.new(:name => 'John')
112
+ person = Structure.new(:name => 'John', :age => 70)
67
113
  assert_equal 'John', person.name
68
-
69
- other = Person.new(:name => 'Jane', :friends => [person])
70
- assert_equal 'John', other.friends.first.name
114
+ assert_equal 70, person.age
115
+ assert_equal({}, Structure.new.send(:table))
71
116
  end
72
117
 
73
- def test_cant_change_sex_when_frozen
74
- person = Person.new(:name => 'John')
75
- person.freeze
76
- assert_raises(TypeError) { person.name = 'Jane' }
77
- end
118
+ def test_new_field
119
+ @person.send(:table)[:age] = 20
120
+ @person.send(:new_field, :age)
78
121
 
79
- def test_to_hash
80
- person = Person.new(:name => 'John')
81
- friend = Person.new(:name => 'Jane')
82
- person.friends << friend
83
- hsh = person.to_hash
122
+ assert_equal 20, @person.age
84
123
 
85
- assert_equal person.name, hsh[:name]
86
- assert_equal friend.name, hsh[:friends].first[:name]
124
+ @person.age = 30
125
+ assert_equal 30, @person.age
87
126
 
88
- person.friends = [[friend]]
89
- hsh = person.to_hash
90
- assert_equal friend.name, hsh[:friends].first.first[:name]
127
+ @person.instance_eval { def gender; 'male'; end }
128
+ @person.send(:new_field, :gender)
129
+ assert_equal 'male', @person.gender
130
+ refute_respond_to @person, :gender=
91
131
  end
92
132
 
93
- def test_json
94
- Person.send :include, Structure::JSON
133
+ def test_recursive_assignment
134
+ friend = { :name => 'Jane' }
135
+ @person.friend = friend
136
+ @person.friends = [friend]
137
+ assert_equal 'Jane', @person.friend.name
138
+ assert_equal 'Jane', @person.friends.first.name
139
+ end
95
140
 
96
- person = Person.new(:name => 'John')
97
- person.friends << Person.new(:name => 'Jane')
98
- json = person.to_json
99
- assert_kind_of Person, JSON.parse(json)
100
- assert_kind_of Person, JSON.parse(json).friends.first
101
- assert_equal false, person.respond_to?(:as_json)
141
+ def test_recursive_marshaling
142
+ hsh = {
143
+ :name => 'John',
144
+ :friend => { :name => 'Jane' },
145
+ :friends => [{ :name => 'Jane' }]
146
+ }
147
+ friend = Structure.new(:name => 'Jane')
148
+ @person.friend = friend
149
+ @person.friends = [friend]
150
+ assert_equal hsh, @person.marshal_dump
151
+
152
+ person = Structure.new
153
+ person.marshal_load(hsh)
154
+ assert_equal friend, person.friend
155
+ assert_equal friend, person.friends.first
156
+ end
102
157
 
103
- require 'active_support/ordered_hash'
104
- require 'active_support/json'
105
- load 'structure/json.rb'
106
- assert_equal true, person.as_json(:only => :name).has_key?(:name)
107
- assert_equal false, person.as_json(:except => :name).has_key?(:name)
158
+ def test_table
159
+ assert_equal({ :name => 'John' }, @person.send(:table))
108
160
  end
109
161
  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.22.1
4
+ version: 0.23.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-10-27 00:00:00.000000000 Z
12
+ date: 2011-12-07 00:00:00.000000000 Z
13
13
  dependencies: []
14
- description: Structure is a typed key/value container.
14
+ description: Structure is a key/value container.
15
15
  email:
16
16
  - code@papercavalier.com
17
17
  executables: []
@@ -25,9 +25,11 @@ files:
25
25
  - README.md
26
26
  - Rakefile
27
27
  - lib/structure.rb
28
- - lib/structure/json.rb
28
+ - lib/structure/ext/active_support.rb
29
29
  - lib/structure/version.rb
30
30
  - structure.gemspec
31
+ - test/defined_structure_test.rb
32
+ - test/helper.rb
31
33
  - test/structure_test.rb
32
34
  homepage: http://github.com/hakanensari/structure
33
35
  licenses: []
@@ -49,9 +51,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
49
51
  version: '0'
50
52
  requirements: []
51
53
  rubyforge_project: structure
52
- rubygems_version: 1.8.10
54
+ rubygems_version: 1.8.11
53
55
  signing_key:
54
56
  specification_version: 3
55
- summary: A typed key/value container
57
+ summary: A key/value container
56
58
  test_files:
59
+ - test/defined_structure_test.rb
60
+ - test/helper.rb
57
61
  - test/structure_test.rb
@@ -1,79 +0,0 @@
1
- begin
2
- JSON::JSON_LOADED
3
- rescue NameError
4
- require 'json'
5
- end
6
-
7
- class Structure
8
- # JSON methods for a structure
9
- #
10
- # Include this in your structure if you need to cast it to JSON and
11
- # vice versa.
12
- #
13
- # @example
14
- # class Point < Structure
15
- # include JSON
16
- #
17
- # key :x, Integer
18
- # key :y, Integer
19
- # end
20
- #
21
- # Alternatively, include it in the parent class if you have more than
22
- # one structure.
23
- #
24
- # @example
25
- # class Structure
26
- # include JSON
27
- # end
28
- #
29
- module JSON
30
- def self.included(base)
31
- base.extend ClassMethods
32
- end
33
-
34
- # Converts structure to its JSON representation
35
- #
36
- # @param [Hash] args
37
- # @return [JSON] a JSON representation of the structure
38
- def to_json(*args)
39
- { ::JSON.create_id => self.class.name }.
40
- merge(@attributes).
41
- to_json(*args)
42
- end
43
-
44
- if defined? ActiveSupport
45
- # Converts structure to its JSON representation
46
- #
47
- # @param [Hash] options
48
- # @return [JSON] a JSON representation of the structure
49
- def as_json(options = nil)
50
- subset = if options
51
- if only = options[:only]
52
- @attributes.slice(*Array.wrap(only))
53
- elsif except = options[:except]
54
- @attributes.except(*Array.wrap(except))
55
- else
56
- @attributes.dup
57
- end
58
- else
59
- @attributes.dup
60
- end
61
-
62
- { ::JSON.create_id => self.class.name }.
63
- merge(subset)
64
- end
65
- end
66
-
67
- module ClassMethods
68
- # Builds a structure out of its JSON representation
69
- #
70
- # @param [Hash] hsh a hash representation of a JSON
71
- # @return [Structure] a structure
72
- def json_create(hsh)
73
- hsh.delete('json_class')
74
-
75
- new(hsh)
76
- end
77
- end
78
- end
79
- end