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 +4 -4
- data/LICENSE +18 -9
- data/README.md +5 -11
- data/lib/structure.rb +224 -146
- data/lib/structure/ext/active_support.rb +22 -0
- data/lib/structure/version.rb +1 -1
- data/structure.gemspec +2 -2
- data/test/defined_structure_test.rb +60 -0
- data/test/helper.rb +8 -0
- data/test/structure_test.rb +130 -78
- metadata +10 -6
- data/lib/structure/json.rb +0 -79
data/Gemfile
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
source :rubygems
|
2
2
|
gemspec
|
3
3
|
|
4
|
-
gem 'activesupport'
|
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
|
-
|
2
|
-
Version 2, December 2004
|
1
|
+
(The MIT License)
|
3
2
|
|
4
|
-
|
3
|
+
Copyright (c) 2011 Hakan Ensari
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
1
|
+
![structure](http://f.cl.ly/items/2u2v0e3k2I3w1A0y2e25/ruby.png)
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
|
11
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
35
|
-
#
|
36
|
-
#
|
37
|
-
|
38
|
-
|
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
|
-
#
|
42
|
-
|
43
|
-
|
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
|
-
|
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
|
56
|
-
|
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
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
127
|
-
|
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
|
-
#
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
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
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
191
|
+
super
|
155
192
|
end
|
156
193
|
end
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
171
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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
|
-
|
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
|
data/lib/structure/version.rb
CHANGED
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
|
13
|
-
s.description = 'Structure is a
|
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
data/test/structure_test.rb
CHANGED
@@ -1,109 +1,161 @@
|
|
1
|
-
require '
|
1
|
+
require File.expand_path('../helper.rb', __FILE__)
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
19
|
-
end
|
16
|
+
def test_element_reference
|
17
|
+
assert_raises(NoMethodError) { @person[1] }
|
18
|
+
end
|
20
19
|
|
21
|
-
|
22
|
-
|
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
|
34
|
-
|
35
|
-
|
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
|
39
|
-
|
40
|
-
|
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
|
44
|
-
|
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
|
48
|
-
|
49
|
-
person.
|
50
|
-
|
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
|
-
|
53
|
-
|
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
|
57
|
-
person =
|
58
|
-
|
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.
|
61
|
-
assert_equal
|
62
|
-
|
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 =
|
112
|
+
person = Structure.new(:name => 'John', :age => 70)
|
67
113
|
assert_equal 'John', person.name
|
68
|
-
|
69
|
-
|
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
|
74
|
-
person
|
75
|
-
person.
|
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
|
-
|
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
|
-
|
86
|
-
assert_equal
|
124
|
+
@person.age = 30
|
125
|
+
assert_equal 30, @person.age
|
87
126
|
|
88
|
-
person.
|
89
|
-
|
90
|
-
assert_equal
|
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
|
94
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
104
|
-
|
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.
|
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-
|
12
|
+
date: 2011-12-07 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
|
-
description: Structure is a
|
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/
|
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.
|
54
|
+
rubygems_version: 1.8.11
|
53
55
|
signing_key:
|
54
56
|
specification_version: 3
|
55
|
-
summary: A
|
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
|
data/lib/structure/json.rb
DELETED
@@ -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
|