structure 0.15.1 → 0.16.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/.travis.yml +3 -0
- data/Gemfile +10 -1
- data/README.md +14 -4
- data/lib/structure/collection.rb +67 -0
- data/lib/structure/document/static.rb +66 -0
- data/lib/structure/document.rb +212 -0
- data/lib/structure/version.rb +2 -2
- data/lib/structure.rb +1 -189
- data/structure.gemspec +12 -8
- data/test/collection_test.rb +33 -0
- data/test/document_test.rb +113 -0
- data/test/helper.rb +2 -1
- data/test/static_test.rb +4 -4
- metadata +50 -11
- data/CHANGELOG.md +0 -39
- data/lib/structure/static.rb +0 -61
- data/test/structure_test.rb +0 -124
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
source :rubygems
|
2
2
|
|
3
|
-
|
3
|
+
gemspec
|
4
|
+
|
4
5
|
gem 'json', :platform => [:mri_18, :jruby, :rbx]
|
5
6
|
gem 'rake'
|
7
|
+
|
8
|
+
platforms :mri_18 do
|
9
|
+
gem 'ruby-debug', :require => 'ruby-debug' unless ENV['CI']
|
10
|
+
end
|
11
|
+
|
12
|
+
platforms :mri_19 do
|
13
|
+
gem 'ruby-debug19', :require => 'ruby-debug' unless ENV['CI']
|
14
|
+
end
|
data/README.md
CHANGED
@@ -2,17 +2,27 @@
|
|
2
2
|
|
3
3
|
[](http://travis-ci.org/hakanensari/structure)
|
4
4
|
|
5
|
-
Structure is a
|
5
|
+
Structure is a typed, nestable key/value container.
|
6
6
|
|
7
|
-
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Install and require the gem.
|
8
10
|
|
9
11
|
require 'structure'
|
10
12
|
|
11
|
-
|
13
|
+
Define a model.
|
14
|
+
|
15
|
+
Document = Structure::Document
|
16
|
+
|
17
|
+
class Person < Document
|
12
18
|
key :name
|
13
|
-
many :friends
|
19
|
+
many :friends, :class_name => 'Person'
|
14
20
|
end
|
15
21
|
|
22
|
+
person = Person.create(:name => 'John')
|
23
|
+
person.friends << Person.create(:name => 'Jane')
|
24
|
+
person.friends.size # 1
|
25
|
+
|
16
26
|
Please see [the project page] [1] for more detailed info.
|
17
27
|
|
18
28
|
[1]: http://code.papercavalier.com/structure/
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Structure
|
4
|
+
class Collection < Array
|
5
|
+
class << self
|
6
|
+
attr :type
|
7
|
+
|
8
|
+
alias overridden_new new
|
9
|
+
|
10
|
+
def new(type)
|
11
|
+
unless type < Document
|
12
|
+
raise TypeError, "#{type} isn't a Document"
|
13
|
+
end
|
14
|
+
class_name = "#{type}Collection"
|
15
|
+
|
16
|
+
begin
|
17
|
+
class_name.constantize
|
18
|
+
rescue NameError
|
19
|
+
Object.class_eval <<-ruby
|
20
|
+
class #{class_name} < Structure::Collection
|
21
|
+
@type = #{type}
|
22
|
+
end
|
23
|
+
ruby
|
24
|
+
retry
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def inherited(child)
|
31
|
+
Kernel.send(:define_method, child.name) do |arg|
|
32
|
+
case arg
|
33
|
+
when child
|
34
|
+
arg
|
35
|
+
else
|
36
|
+
[arg].flatten.inject(child.new) { |a, e| a << e }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
child.instance_eval { alias new overridden_new }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
attr :members
|
44
|
+
|
45
|
+
%w{concat eql? push replace unshift}.each do |method|
|
46
|
+
define_method method do |ary|
|
47
|
+
super ary.map { |item| Kernel.send(type.to_s, item) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def <<(item)
|
52
|
+
super Kernel.send(type.to_s, item)
|
53
|
+
end
|
54
|
+
|
55
|
+
def create(*args)
|
56
|
+
self.<< type.new(*args)
|
57
|
+
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def type
|
64
|
+
self.class.type
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# When included in a document, this module will turn it into a static
|
4
|
+
# model which sources its records from a yaml file.
|
5
|
+
module Structure
|
6
|
+
class Document
|
7
|
+
module Static
|
8
|
+
class << self
|
9
|
+
private
|
10
|
+
|
11
|
+
def included(base)
|
12
|
+
base.key(:_id, Integer)
|
13
|
+
base.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
include Enumerable
|
19
|
+
|
20
|
+
# The path for the data file.
|
21
|
+
#
|
22
|
+
# This file should contain a YAML representation of the records.
|
23
|
+
#
|
24
|
+
# Overwrite this reader with an opiniated location to dry.
|
25
|
+
attr :data_path
|
26
|
+
|
27
|
+
# Returns all records.
|
28
|
+
def all
|
29
|
+
@records ||= data.map do |record|
|
30
|
+
record['_id'] ||= record.delete('id') || increment_id
|
31
|
+
new(record)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Yields each record to given block.
|
36
|
+
#
|
37
|
+
# Other enumerators will be made available by the Enumerable
|
38
|
+
# module.
|
39
|
+
def each(&block)
|
40
|
+
all.each { |record| block.call(record) }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Finds a record by its ID.
|
44
|
+
def find(id)
|
45
|
+
detect { |record| record._id == id }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sets the path for the data file.
|
49
|
+
def set_data_path(data_path)
|
50
|
+
@data_path = data_path
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def data
|
56
|
+
YAML.load_file(@data_path)
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
def increment_id
|
61
|
+
@increment_id = @increment_id.to_i + 1
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
begin
|
2
|
+
JSON::JSON_LOADED
|
3
|
+
rescue NameError
|
4
|
+
require 'json'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'active_support/inflector'
|
8
|
+
require 'certainty'
|
9
|
+
|
10
|
+
require 'structure/collection'
|
11
|
+
|
12
|
+
module Structure
|
13
|
+
# A document is a typed, nestable key/value container.
|
14
|
+
#
|
15
|
+
# class Person < Document
|
16
|
+
# key :name
|
17
|
+
# key :age, Integer
|
18
|
+
# one :location
|
19
|
+
# many :friends, :class_name => 'Person'
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
class Document
|
23
|
+
include Enumerable
|
24
|
+
|
25
|
+
autoload :Static,'structure/document/static'
|
26
|
+
|
27
|
+
# An attribute may be of the following data types.
|
28
|
+
TYPES = [Array, Boolean, Collection, Document, Float, Hash, Integer, String]
|
29
|
+
|
30
|
+
class << self
|
31
|
+
# Returns the default values for the attributes.
|
32
|
+
def defaults
|
33
|
+
@defaults ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Builds a Ruby object out of the JSON representation of a
|
37
|
+
# structure.
|
38
|
+
def json_create(object)
|
39
|
+
object.delete 'json_class'
|
40
|
+
new object
|
41
|
+
end
|
42
|
+
|
43
|
+
# Defines an attribute.
|
44
|
+
#
|
45
|
+
# Takes a name, an optional type, and an optional hash of options.
|
46
|
+
#
|
47
|
+
# If nothing is specified, type defaults to +String+.
|
48
|
+
#
|
49
|
+
# Available options are:
|
50
|
+
#
|
51
|
+
# * +:default+, which sets the default value for the attribute. If
|
52
|
+
# no default value is specified, it defaults to +nil+.
|
53
|
+
def key(name, *args)
|
54
|
+
name = name.to_sym
|
55
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
56
|
+
type = args.shift || String
|
57
|
+
default = options[:default]
|
58
|
+
|
59
|
+
if method_defined? name
|
60
|
+
raise NameError, "#{name} is already defined"
|
61
|
+
end
|
62
|
+
|
63
|
+
if (type.ancestors & TYPES).empty?
|
64
|
+
raise TypeError, "#{type} isn't a valid type"
|
65
|
+
end
|
66
|
+
|
67
|
+
if default.nil? || default.is_a?(type)
|
68
|
+
defaults[name] = default
|
69
|
+
else
|
70
|
+
raise TypeError, "#{default} isn't a #{type}"
|
71
|
+
end
|
72
|
+
|
73
|
+
module_eval do
|
74
|
+
define_method(name) { @attributes[name] }
|
75
|
+
|
76
|
+
define_method("#{name}=") do |value|
|
77
|
+
@attributes[name] = if value.is_a?(type) || value.nil?
|
78
|
+
value
|
79
|
+
else
|
80
|
+
Kernel.send(type.to_s, value)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
alias_method "#{name}?", name if type == Boolean
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Defines an attribute that represents a collection.
|
89
|
+
def many(name, options = {})
|
90
|
+
class_name = options.delete(:class_name) || name.to_s.classify
|
91
|
+
klass = constantize class_name
|
92
|
+
collection = Collection.new(klass)
|
93
|
+
|
94
|
+
key name, collection, options.merge(:default => collection.new)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Defines an attribute that represents another structure. Takes
|
98
|
+
# a name and optional hash of options.
|
99
|
+
def one(name, options = {})
|
100
|
+
class_name = options.delete(:class_name) || name.to_s.classify
|
101
|
+
klass = constantize class_name
|
102
|
+
|
103
|
+
unless klass < Document
|
104
|
+
raise TypeError, "#{klass} isn't a Document"
|
105
|
+
end
|
106
|
+
|
107
|
+
define_method("create_#{name}") do |*args|
|
108
|
+
self.send("#{name}=", klass.new(*args))
|
109
|
+
end
|
110
|
+
|
111
|
+
key name, klass, options
|
112
|
+
end
|
113
|
+
|
114
|
+
alias create new
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def constantize(name)
|
119
|
+
name.constantize
|
120
|
+
rescue
|
121
|
+
Object.class_eval <<-ruby
|
122
|
+
class #{name} < Structure::Document; end
|
123
|
+
ruby
|
124
|
+
retry
|
125
|
+
end
|
126
|
+
|
127
|
+
def inherited(child)
|
128
|
+
Kernel.send(:define_method, child.name) do |arg|
|
129
|
+
case arg
|
130
|
+
when child
|
131
|
+
arg
|
132
|
+
when Hash
|
133
|
+
child.new arg
|
134
|
+
else
|
135
|
+
raise TypeError, "can't convert #{arg.class} into #{child}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Creates a new structure.
|
142
|
+
#
|
143
|
+
# A hash, if provided, will seed its attributes.
|
144
|
+
def initialize(hash = {})
|
145
|
+
@attributes = self.class.defaults.inject({}) do |a, (k, v)|
|
146
|
+
a[k] = v.is_a?(Array) || v.is_a?(Collection) ? v.dup : v
|
147
|
+
a
|
148
|
+
end
|
149
|
+
|
150
|
+
hash.each { |k, v| self.send("#{k}=", v) }
|
151
|
+
end
|
152
|
+
|
153
|
+
# The attributes that make up the structure.
|
154
|
+
attr :attributes
|
155
|
+
|
156
|
+
# Returns a Rails-friendly JSON representation of the structure.
|
157
|
+
def as_json(options = nil)
|
158
|
+
subset = if options
|
159
|
+
if attrs = options[:only]
|
160
|
+
@attributes.slice(*Array.wrap(attrs))
|
161
|
+
elsif attrs = options[:except]
|
162
|
+
@attributes.except(*Array.wrap(attrs))
|
163
|
+
else
|
164
|
+
@attributes.dup
|
165
|
+
end
|
166
|
+
else
|
167
|
+
@attributes.dup
|
168
|
+
end
|
169
|
+
|
170
|
+
klass = self.class.name
|
171
|
+
{ JSON.create_id => klass }.
|
172
|
+
merge(subset)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Calls block once for each attribute in the structure, passing that
|
176
|
+
# attribute as a parameter.
|
177
|
+
def each(&block)
|
178
|
+
@attributes.each { |v| block.call(v) }
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns a hash representation of the structure.
|
182
|
+
def to_hash
|
183
|
+
@attributes.inject({}) do |a, (k, v)|
|
184
|
+
a[k] =
|
185
|
+
if v.respond_to?(:to_hash)
|
186
|
+
v.to_hash
|
187
|
+
elsif v.is_a?(Array) || v.is_a?(Collection)
|
188
|
+
v.map { |e| e.respond_to?(:to_hash) ? e.to_hash : e }
|
189
|
+
else
|
190
|
+
v
|
191
|
+
end
|
192
|
+
|
193
|
+
a
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Returns a JSON representation of the structure.
|
198
|
+
def to_json(*args)
|
199
|
+
klass = self.class.name
|
200
|
+
{ JSON.create_id => klass }.
|
201
|
+
merge(@attributes).
|
202
|
+
to_json(*args)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Compares this object with another object for equality. A Structure
|
206
|
+
# is equal to the other object when latter is of the same class and
|
207
|
+
# the two objects' attributes are the same.
|
208
|
+
def ==(other)
|
209
|
+
other.is_a?(self.class) && @attributes == other.attributes
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
data/lib/structure/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = '0.
|
1
|
+
module Structure
|
2
|
+
VERSION = '0.16.0'
|
3
3
|
end
|
data/lib/structure.rb
CHANGED
@@ -1,189 +1 @@
|
|
1
|
-
|
2
|
-
JSON::JSON_LOADED
|
3
|
-
rescue NameError
|
4
|
-
require 'json'
|
5
|
-
end
|
6
|
-
|
7
|
-
# Fabricate a +Boolean+ class.
|
8
|
-
unless defined? Boolean
|
9
|
-
module Boolean; end
|
10
|
-
[TrueClass, FalseClass].each { |klass| klass.send :include, Boolean }
|
11
|
-
end
|
12
|
-
|
13
|
-
# = Structure
|
14
|
-
#
|
15
|
-
# Structure is a Struct-like key/value container.
|
16
|
-
#
|
17
|
-
# class Person < Structure
|
18
|
-
# key :name
|
19
|
-
# many :friends
|
20
|
-
# end
|
21
|
-
#
|
22
|
-
class Structure
|
23
|
-
include Enumerable
|
24
|
-
|
25
|
-
autoload :Static, 'structure/static'
|
26
|
-
|
27
|
-
# Available data type.
|
28
|
-
TYPES = [Array, Boolean, Float, Hash, Integer, String, Structure]
|
29
|
-
|
30
|
-
class << self
|
31
|
-
# Defines an attribute that represents an array of objects.
|
32
|
-
def many(name, options = {})
|
33
|
-
key name, Array, { :default => [] }.merge(options)
|
34
|
-
end
|
35
|
-
|
36
|
-
# Defines an attribute that represents another structure.
|
37
|
-
def one(name)
|
38
|
-
key name, Structure
|
39
|
-
end
|
40
|
-
|
41
|
-
# Builds a structure out of its JSON representation.
|
42
|
-
def json_create(object)
|
43
|
-
object.delete 'json_class'
|
44
|
-
new object
|
45
|
-
end
|
46
|
-
|
47
|
-
# Defines an attribute.
|
48
|
-
#
|
49
|
-
# Takes a name, an optional type, and an optional hash of options.
|
50
|
-
#
|
51
|
-
# The type can be +Array+, +Boolean+, +Float+, +Hash+, +Integer+,
|
52
|
-
# +String+, a +Structure+, or a subclass thereof. If none is
|
53
|
-
# specified, this defaults to +String+.
|
54
|
-
#
|
55
|
-
# Available options are:
|
56
|
-
#
|
57
|
-
# * +:default+, which sets the default value for the attribute.
|
58
|
-
def key(name, *args)
|
59
|
-
name = name.to_sym
|
60
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
61
|
-
type = args.shift || String
|
62
|
-
default = options[:default]
|
63
|
-
|
64
|
-
if method_defined? name
|
65
|
-
raise NameError, "#{name} is already defined"
|
66
|
-
end
|
67
|
-
|
68
|
-
if (type.ancestors & TYPES).empty?
|
69
|
-
raise TypeError, "#{type} is not a valid type"
|
70
|
-
end
|
71
|
-
|
72
|
-
if default.nil? || default.is_a?(type)
|
73
|
-
default_attributes[name] = default
|
74
|
-
else
|
75
|
-
msg = "#{default} isn't a#{'n' if type.name.match(/^[AI]/)} #{type}"
|
76
|
-
raise TypeError, msg
|
77
|
-
end
|
78
|
-
|
79
|
-
module_eval do
|
80
|
-
# A proc that typecasts value based on type.
|
81
|
-
typecaster =
|
82
|
-
case type.name
|
83
|
-
when 'Boolean'
|
84
|
-
lambda { |value|
|
85
|
-
# This should take care of the different representations
|
86
|
-
# of truth we might be feeding into the model.
|
87
|
-
#
|
88
|
-
# Any string other than "0" or "false" will evaluate to
|
89
|
-
# true.
|
90
|
-
#
|
91
|
-
# Any integer other than 0 will evaluate to true.
|
92
|
-
#
|
93
|
-
# Otherwise, we do the double-bang trick to non-boolean
|
94
|
-
# values.
|
95
|
-
case value
|
96
|
-
when Boolean
|
97
|
-
value
|
98
|
-
when String
|
99
|
-
value !~ /0|false/i
|
100
|
-
when Integer
|
101
|
-
value != 0
|
102
|
-
else
|
103
|
-
!!value
|
104
|
-
end
|
105
|
-
}
|
106
|
-
when /Hash|Structure/
|
107
|
-
# We could possibly check if the value responds to #to_hash
|
108
|
-
# and cast to hash if it does, but I don't see any use case
|
109
|
-
# for this right now.
|
110
|
-
lambda { |value|
|
111
|
-
unless value.is_a? type
|
112
|
-
raise TypeError, "#{value} is not a #{type}"
|
113
|
-
end
|
114
|
-
value
|
115
|
-
}
|
116
|
-
else
|
117
|
-
lambda { |value| Kernel.send(type.to_s, value) }
|
118
|
-
end
|
119
|
-
|
120
|
-
# Define attribute accessors.
|
121
|
-
define_method(name) { @attributes[name] }
|
122
|
-
|
123
|
-
define_method("#{name}=") do |value|
|
124
|
-
@attributes[name] = value.nil? ? nil : typecaster.call(value)
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
# Returns a hash of all attributes with default values.
|
130
|
-
def default_attributes
|
131
|
-
@default_attributes ||= {}
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
# Creates a new structure.
|
136
|
-
#
|
137
|
-
# A hash, if provided, will seed its attributes.
|
138
|
-
def initialize(hash = {})
|
139
|
-
@attributes = {}
|
140
|
-
self.class.default_attributes.each do |key, value|
|
141
|
-
@attributes[key] = value.is_a?(Array) ? value.dup : value
|
142
|
-
end
|
143
|
-
|
144
|
-
hash.each { |key, value| self.send("#{key}=", value) }
|
145
|
-
end
|
146
|
-
|
147
|
-
# A hash that stores the attributes of the structure.
|
148
|
-
attr :attributes
|
149
|
-
|
150
|
-
# Returns a Rails-friendly JSON representation of the structure.
|
151
|
-
def as_json(options = nil)
|
152
|
-
subset = if options
|
153
|
-
if attrs = options[:only]
|
154
|
-
@attributes.slice(*Array.wrap(attrs))
|
155
|
-
elsif attrs = options[:except]
|
156
|
-
@attributes.except(*Array.wrap(attrs))
|
157
|
-
else
|
158
|
-
@attributes.dup
|
159
|
-
end
|
160
|
-
else
|
161
|
-
@attributes.dup
|
162
|
-
end
|
163
|
-
|
164
|
-
klass = self.class.name
|
165
|
-
{ JSON.create_id => klass }.
|
166
|
-
merge(subset)
|
167
|
-
end
|
168
|
-
|
169
|
-
# Calls block once for each attribute in the structure, passing that
|
170
|
-
# attribute as a parameter.
|
171
|
-
def each(&block)
|
172
|
-
@attributes.each { |value| block.call(value) }
|
173
|
-
end
|
174
|
-
|
175
|
-
# Returns a JSON representation of the structure.
|
176
|
-
def to_json(*args)
|
177
|
-
klass = self.class.name
|
178
|
-
{ JSON.create_id => klass }.
|
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 latter is of the same class and
|
185
|
-
# the two objects' attributes are the same.
|
186
|
-
def ==(other)
|
187
|
-
other.is_a?(self.class) && @attributes == other.attributes
|
188
|
-
end
|
189
|
-
end
|
1
|
+
require 'structure/document'
|
data/structure.gemspec
CHANGED
@@ -3,19 +3,23 @@ $:.push File.expand_path('../lib', __FILE__)
|
|
3
3
|
require 'structure/version'
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
|
-
s.name =
|
6
|
+
s.name = 'structure'
|
7
7
|
s.version = Structure::VERSION
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
|
-
s.authors = [
|
10
|
-
s.email = [
|
11
|
-
s.homepage =
|
12
|
-
s.summary =
|
13
|
-
s.description =
|
9
|
+
s.authors = ['Hakan Ensari']
|
10
|
+
s.email = ['code@papercavalier.com']
|
11
|
+
s.homepage = 'http://github.com/hakanensari/structure'
|
12
|
+
s.summary = 'A typed, nestable key/value container'
|
13
|
+
s.description = 'A typed, nestable key/value container'
|
14
14
|
|
15
|
-
s.rubyforge_project =
|
15
|
+
s.rubyforge_project = 'structure'
|
16
|
+
|
17
|
+
s.add_dependency 'certainty', '~> 0.2.0'
|
18
|
+
s.add_dependency 'activesupport', '~> 3.0'
|
19
|
+
s.add_dependency 'i18n', '~> 0.6.0'
|
16
20
|
|
17
21
|
s.files = `git ls-files`.split("\n")
|
18
22
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
23
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
-
s.require_paths = [
|
24
|
+
s.require_paths = ['lib']
|
21
25
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path('../helper.rb', __FILE__)
|
2
|
+
|
3
|
+
class Foo < Document
|
4
|
+
key :bar
|
5
|
+
end
|
6
|
+
|
7
|
+
class TestCollection < Test::Unit::TestCase
|
8
|
+
def setup
|
9
|
+
Structure::Collection.new(Foo)
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_subclassing
|
13
|
+
assert FooCollection < Structure::Collection
|
14
|
+
assert_equal Foo, FooCollection.type
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_conversion
|
18
|
+
item = Foo.new
|
19
|
+
|
20
|
+
assert_equal item, FooCollection([item]).first
|
21
|
+
assert_kind_of FooCollection, FooCollection([item])
|
22
|
+
|
23
|
+
assert_equal item, FooCollection(item).first
|
24
|
+
assert_kind_of FooCollection, FooCollection(item)
|
25
|
+
|
26
|
+
assert_raise(TypeError) { FooCollection('foo') }
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_enumeration
|
30
|
+
assert_respond_to FooCollection.new, :map
|
31
|
+
assert_kind_of FooCollection, FooCollection.new.map! { |e| e }
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require File.expand_path('../helper.rb', __FILE__)
|
2
|
+
|
3
|
+
class Person < Document
|
4
|
+
key :name
|
5
|
+
key :single, Boolean, :default => true
|
6
|
+
one :location
|
7
|
+
many :friends, :class_name => 'Person'
|
8
|
+
end
|
9
|
+
|
10
|
+
class Location < Document
|
11
|
+
key :lon, Float
|
12
|
+
key :lat, Float
|
13
|
+
end
|
14
|
+
|
15
|
+
class TestDocument < Test::Unit::TestCase
|
16
|
+
def test_enumeration
|
17
|
+
assert_respond_to Person.new, :map
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_accessors
|
21
|
+
assert_respond_to Person.new, :name
|
22
|
+
assert_respond_to Person.new, :name=
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_converter
|
26
|
+
assert_kind_of Person, Person(Person.new)
|
27
|
+
assert_kind_of Person, Person(:name => 'John')
|
28
|
+
assert_raise(TypeError) { Person('John') }
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_errors
|
32
|
+
assert_raise(NameError) { Person.key :class }
|
33
|
+
assert_raise(TypeError) { Person.key :foo, Object }
|
34
|
+
assert_raise(TypeError) { Person.key :foo, :default => 1 }
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_defaults
|
38
|
+
assert_equal true, Person.create.single?
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_typecheck
|
42
|
+
location = Location.new
|
43
|
+
|
44
|
+
location.lon = '100'
|
45
|
+
assert_equal 100.0, location.lon
|
46
|
+
|
47
|
+
location.lon = nil
|
48
|
+
assert_nil location.lon
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_one
|
52
|
+
person = Person.new
|
53
|
+
|
54
|
+
person.location = Location.new(:lon => 2.0)
|
55
|
+
assert_equal 2.0, person.location.lon
|
56
|
+
|
57
|
+
person.create_location :lon => 1.0
|
58
|
+
assert_equal 1.0, person.location.lon
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_many
|
62
|
+
person = Person.new
|
63
|
+
|
64
|
+
person.friends.create
|
65
|
+
person.friends.create :name => 'John'
|
66
|
+
assert_equal 2, person.friends.size
|
67
|
+
assert_equal 0, person.friends.last.friends.size
|
68
|
+
|
69
|
+
friend = Person.new
|
70
|
+
|
71
|
+
person.friends = [friend]
|
72
|
+
assert_equal 1, person.friends.size
|
73
|
+
assert_equal 0, friend.friends.size
|
74
|
+
|
75
|
+
person.friends << friend
|
76
|
+
assert_equal 2, person.friends.size
|
77
|
+
assert_equal 0, friend.friends.size
|
78
|
+
assert_equal 0, person.friends.last.friends.size
|
79
|
+
|
80
|
+
person.friends.clear
|
81
|
+
assert_equal 0, person.friends.size
|
82
|
+
assert person.friends.empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_to_hash
|
86
|
+
person = Person.new
|
87
|
+
person.friends.create :name => 'John'
|
88
|
+
assert_equal 'John', person.to_hash[:friends].first[:name]
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_json
|
92
|
+
person = Person.new
|
93
|
+
json = person.to_json
|
94
|
+
assert_equal person, JSON.parse(json)
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_json_with_nested_structures
|
98
|
+
person = Person.new
|
99
|
+
person.friends << Person.new
|
100
|
+
person.location = Location.new
|
101
|
+
json = person.to_json
|
102
|
+
assert JSON.parse(json).friends.first.is_a? Person
|
103
|
+
assert JSON.parse(json).location.is_a? Location
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_json_with_active_support
|
107
|
+
require 'active_support/ordered_hash'
|
108
|
+
require 'active_support/json'
|
109
|
+
person = Person.new
|
110
|
+
assert person.as_json(:only => :name).has_key?(:name)
|
111
|
+
assert !person.as_json(:except => :name).has_key?(:name)
|
112
|
+
end
|
113
|
+
end
|
data/test/helper.rb
CHANGED
data/test/static_test.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
require File.expand_path('../helper.rb', __FILE__)
|
2
2
|
|
3
|
-
class City <
|
3
|
+
class City < Document
|
4
4
|
include Static
|
5
5
|
|
6
6
|
key :name
|
7
7
|
many :neighborhoods
|
8
8
|
end
|
9
9
|
|
10
|
-
class Neighborhood <
|
10
|
+
class Neighborhood < Document
|
11
11
|
key :name
|
12
12
|
end
|
13
13
|
|
14
|
-
class Dummy <
|
14
|
+
class Dummy < Document
|
15
15
|
include Static
|
16
16
|
|
17
17
|
key :name
|
@@ -57,6 +57,6 @@ class TestStatic < Test::Unit::TestCase
|
|
57
57
|
|
58
58
|
def test_nesting
|
59
59
|
fixture City, 'cities_with_neighborhoods'
|
60
|
-
|
60
|
+
assert_kind_of Neighborhood, City.first.neighborhoods.first
|
61
61
|
end
|
62
62
|
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.16.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,9 +9,42 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-08-
|
13
|
-
dependencies:
|
14
|
-
|
12
|
+
date: 2011-08-24 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: certainty
|
16
|
+
requirement: &70198704238520 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.2.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70198704238520
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activesupport
|
27
|
+
requirement: &70198704237620 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70198704237620
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: i18n
|
38
|
+
requirement: &70198704236860 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.6.0
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70198704236860
|
47
|
+
description: A typed, nestable key/value container
|
15
48
|
email:
|
16
49
|
- code@papercavalier.com
|
17
50
|
executables: []
|
@@ -20,22 +53,24 @@ extra_rdoc_files: []
|
|
20
53
|
files:
|
21
54
|
- .gitignore
|
22
55
|
- .travis.yml
|
23
|
-
- CHANGELOG.md
|
24
56
|
- Gemfile
|
25
57
|
- LICENSE
|
26
58
|
- README.md
|
27
59
|
- Rakefile
|
28
60
|
- lib/structure.rb
|
29
|
-
- lib/structure/
|
61
|
+
- lib/structure/collection.rb
|
62
|
+
- lib/structure/document.rb
|
63
|
+
- lib/structure/document/static.rb
|
30
64
|
- lib/structure/version.rb
|
31
65
|
- structure.gemspec
|
66
|
+
- test/collection_test.rb
|
67
|
+
- test/document_test.rb
|
32
68
|
- test/fixtures/cities.yml
|
33
69
|
- test/fixtures/cities_with_neighborhoods.yml
|
34
70
|
- test/fixtures/cities_without_ids.yml
|
35
71
|
- test/helper.rb
|
36
72
|
- test/static_test.rb
|
37
|
-
|
38
|
-
homepage: http://code.papercavalier.com/structure
|
73
|
+
homepage: http://github.com/hakanensari/structure
|
39
74
|
licenses: []
|
40
75
|
post_install_message:
|
41
76
|
rdoc_options: []
|
@@ -49,23 +84,27 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
84
|
version: '0'
|
50
85
|
segments:
|
51
86
|
- 0
|
52
|
-
hash:
|
87
|
+
hash: -2067006556322233036
|
53
88
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
89
|
none: false
|
55
90
|
requirements:
|
56
91
|
- - ! '>='
|
57
92
|
- !ruby/object:Gem::Version
|
58
93
|
version: '0'
|
94
|
+
segments:
|
95
|
+
- 0
|
96
|
+
hash: -2067006556322233036
|
59
97
|
requirements: []
|
60
98
|
rubyforge_project: structure
|
61
99
|
rubygems_version: 1.8.6
|
62
100
|
signing_key:
|
63
101
|
specification_version: 3
|
64
|
-
summary: A
|
102
|
+
summary: A typed, nestable key/value container
|
65
103
|
test_files:
|
104
|
+
- test/collection_test.rb
|
105
|
+
- test/document_test.rb
|
66
106
|
- test/fixtures/cities.yml
|
67
107
|
- test/fixtures/cities_with_neighborhoods.yml
|
68
108
|
- test/fixtures/cities_without_ids.yml
|
69
109
|
- test/helper.rb
|
70
110
|
- test/static_test.rb
|
71
|
-
- test/structure_test.rb
|
data/CHANGELOG.md
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
# CHANGELOG
|
2
|
-
|
3
|
-
## 0.15.0
|
4
|
-
* bring back static module
|
5
|
-
|
6
|
-
## 0.14.0
|
7
|
-
* rename .attribute back to .key
|
8
|
-
* shorten .embeds_many and .embeds_one to .many and .one
|
9
|
-
* remove presence methods
|
10
|
-
* add options to .many
|
11
|
-
|
12
|
-
## 0.13.0
|
13
|
-
|
14
|
-
* remove static module
|
15
|
-
* rename .key to .attribute
|
16
|
-
|
17
|
-
# 0.12.0
|
18
|
-
|
19
|
-
* add static module
|
20
|
-
|
21
|
-
# 0.11.0
|
22
|
-
|
23
|
-
* .key now emulates DataMapper.property
|
24
|
-
|
25
|
-
# 0.10.0
|
26
|
-
|
27
|
-
* rename .has_one and .has_many to .embeds_one and .embeds_many to make room
|
28
|
-
for associations
|
29
|
-
|
30
|
-
# 0.9.0
|
31
|
-
|
32
|
-
* add presence method
|
33
|
-
|
34
|
-
# 0.8.0
|
35
|
-
|
36
|
-
* make JSON patch compatible with Active Support
|
37
|
-
* remove URI from list of types
|
38
|
-
|
39
|
-
The rest is history.
|
data/lib/structure/static.rb
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
# When included in a structure, this module turns it into a static
|
2
|
-
# model, the data of which is sourced from a yaml file.
|
3
|
-
#
|
4
|
-
# This is a basic implementation and does not handle nested structures.
|
5
|
-
# See test.
|
6
|
-
class Structure
|
7
|
-
module Static
|
8
|
-
def self.included(base)
|
9
|
-
base.key(:_id, Integer)
|
10
|
-
base.extend(ClassMethods)
|
11
|
-
end
|
12
|
-
|
13
|
-
module ClassMethods
|
14
|
-
include Enumerable
|
15
|
-
|
16
|
-
# The path for the data file.
|
17
|
-
#
|
18
|
-
# This file should contain a YAML representation of the records.
|
19
|
-
#
|
20
|
-
# Overwrite this reader with an opiniated location to dry.
|
21
|
-
attr :data_path
|
22
|
-
|
23
|
-
# Returns all records.
|
24
|
-
def all
|
25
|
-
@records ||= data.map do |record|
|
26
|
-
record["_id"] ||= record.delete("id") || increment_id
|
27
|
-
new(record)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
# Yields each record to given block.
|
32
|
-
#
|
33
|
-
# Other enumerators will be made available by the Enumerable
|
34
|
-
# module.
|
35
|
-
def each(&block)
|
36
|
-
all.each { |record| block.call(record) }
|
37
|
-
end
|
38
|
-
|
39
|
-
# Finds a record by its ID.
|
40
|
-
def find(id)
|
41
|
-
detect { |record| record._id == id }
|
42
|
-
end
|
43
|
-
|
44
|
-
# Sets the path for the data file.
|
45
|
-
def set_data_path(data_path)
|
46
|
-
@data_path = data_path
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
def data
|
52
|
-
YAML.load_file(@data_path)
|
53
|
-
end
|
54
|
-
|
55
|
-
|
56
|
-
def increment_id
|
57
|
-
@increment_id = @increment_id.to_i + 1
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
data/test/structure_test.rb
DELETED
@@ -1,124 +0,0 @@
|
|
1
|
-
require File.expand_path('../helper.rb', __FILE__)
|
2
|
-
|
3
|
-
class Book < Structure
|
4
|
-
key :title
|
5
|
-
key :published, Boolean, :default => true
|
6
|
-
key :pages, Integer
|
7
|
-
end
|
8
|
-
|
9
|
-
class Person < Structure
|
10
|
-
key :name
|
11
|
-
one :partner
|
12
|
-
many :friends
|
13
|
-
many :parents, :default => 2.times.map { Person.new }
|
14
|
-
end
|
15
|
-
|
16
|
-
class TestStructure < Test::Unit::TestCase
|
17
|
-
def test_enumeration
|
18
|
-
assert_respond_to Book.new, :map
|
19
|
-
end
|
20
|
-
|
21
|
-
def test_accessors
|
22
|
-
book = Book.new
|
23
|
-
assert_respond_to book, :title
|
24
|
-
assert_respond_to book, :title=
|
25
|
-
end
|
26
|
-
|
27
|
-
def test_key_errors
|
28
|
-
assert_raise(NameError) { Book.key :class }
|
29
|
-
assert_raise(TypeError) { Book.key :foo, Object }
|
30
|
-
assert_raise(TypeError) { Book.key :foo, :default => 1 }
|
31
|
-
end
|
32
|
-
|
33
|
-
def test_default_attributes
|
34
|
-
exp = { :title => nil,
|
35
|
-
:published => true,
|
36
|
-
:pages => nil }
|
37
|
-
assert_equal exp, Book.default_attributes
|
38
|
-
end
|
39
|
-
|
40
|
-
def test_initialization
|
41
|
-
book = Book.new(:title => 'Foo', :pages => 100)
|
42
|
-
assert_equal 'Foo', book.title
|
43
|
-
assert_equal 100, book.pages
|
44
|
-
end
|
45
|
-
|
46
|
-
def test_typecasting
|
47
|
-
book = Book.new
|
48
|
-
|
49
|
-
book.pages = "100"
|
50
|
-
assert_equal 100, book.pages
|
51
|
-
|
52
|
-
book.pages = nil
|
53
|
-
assert_nil book.pages
|
54
|
-
|
55
|
-
book.title = 1
|
56
|
-
book.title = '1'
|
57
|
-
end
|
58
|
-
|
59
|
-
def test_boolean_typecasting
|
60
|
-
book = Book.new
|
61
|
-
|
62
|
-
book.published = 'false'
|
63
|
-
assert book.published == false
|
64
|
-
|
65
|
-
book.published = 'FALSE'
|
66
|
-
assert book.published == false
|
67
|
-
|
68
|
-
book.published = '0'
|
69
|
-
assert book.published == false
|
70
|
-
|
71
|
-
book.published = 'foo'
|
72
|
-
assert book.published == true
|
73
|
-
|
74
|
-
book.published = 0
|
75
|
-
assert book.published == false
|
76
|
-
|
77
|
-
book.published = 10
|
78
|
-
assert book.published == true
|
79
|
-
end
|
80
|
-
|
81
|
-
def test_defaults
|
82
|
-
assert_equal nil, Book.new.title
|
83
|
-
assert_equal true, Book.new.published
|
84
|
-
assert_equal nil, Person.new.partner
|
85
|
-
assert_equal [], Person.new.friends
|
86
|
-
end
|
87
|
-
|
88
|
-
def test_array
|
89
|
-
person = Person.new
|
90
|
-
friend = Person.new
|
91
|
-
person.friends << person
|
92
|
-
assert_equal 1, person.friends.count
|
93
|
-
assert_equal 0, friend.friends.count
|
94
|
-
end
|
95
|
-
|
96
|
-
def test_many
|
97
|
-
person = Person.new
|
98
|
-
assert_equal 2, person.parents.size
|
99
|
-
end
|
100
|
-
|
101
|
-
def test_json
|
102
|
-
book = Book.new(:title => 'Foo')
|
103
|
-
json = book.to_json
|
104
|
-
assert_equal book, JSON.parse(json)
|
105
|
-
end
|
106
|
-
|
107
|
-
def test_json_with_nested_structures
|
108
|
-
person = Person.new
|
109
|
-
person.friends << Person.new
|
110
|
-
person.partner = Person.new
|
111
|
-
json = person.to_json
|
112
|
-
assert JSON.parse(json).friends.first.is_a? Person
|
113
|
-
assert JSON.parse(json).partner.is_a? Person
|
114
|
-
end
|
115
|
-
|
116
|
-
def test_json_with_active_support
|
117
|
-
require 'active_support/ordered_hash'
|
118
|
-
require 'active_support/json'
|
119
|
-
|
120
|
-
book = Book.new
|
121
|
-
assert book.as_json(:only => :title).has_key?(:title)
|
122
|
-
assert !book.as_json(:except => :title).has_key?(:title)
|
123
|
-
end
|
124
|
-
end
|