structure 0.22.1 → 0.23.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.
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