structure 0.15.1 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![travis](https://secure.travis-ci.org/hakanensari/structure.png)](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
|