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 CHANGED
@@ -6,3 +6,6 @@ rvm:
6
6
  - rbx
7
7
  - rbx-2.0
8
8
  - ree
9
+ branches:
10
+ only:
11
+ - master
data/Gemfile CHANGED
@@ -1,5 +1,14 @@
1
1
  source :rubygems
2
2
 
3
- gem 'activesupport', '>= 3.0'
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 Struct-like key/value container .
5
+ Structure is a typed, nestable key/value container.
6
6
 
7
- It will shine in the ephemeral landscape of API-backed data.
7
+ ## Usage
8
+
9
+ Install and require the gem.
8
10
 
9
11
  require 'structure'
10
12
 
11
- class Person < Structure
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
@@ -1,3 +1,3 @@
1
- class Structure
2
- VERSION = '0.15.1'
1
+ module Structure
2
+ VERSION = '0.16.0'
3
3
  end
data/lib/structure.rb CHANGED
@@ -1,189 +1 @@
1
- begin
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 = "structure"
6
+ s.name = 'structure'
7
7
  s.version = Structure::VERSION
8
8
  s.platform = Gem::Platform::RUBY
9
- s.authors = ["Hakan Ensari"]
10
- s.email = ["code@papercavalier.com"]
11
- s.homepage = "http://code.papercavalier.com/structure"
12
- s.summary = "A Struct-like key/value container"
13
- s.description = "Structure is a Struct-like key/value container."
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 = "structure"
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 = ["lib"]
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
@@ -1,6 +1,5 @@
1
1
  $:.push File.expand_path('../../lib', __FILE__)
2
2
 
3
- require 'rubygems'
4
3
  require 'bundler/setup'
5
4
 
6
5
  begin
@@ -10,3 +9,5 @@ end
10
9
 
11
10
  require 'structure'
12
11
  require 'test/unit'
12
+
13
+ Document = Structure::Document
data/test/static_test.rb CHANGED
@@ -1,17 +1,17 @@
1
1
  require File.expand_path('../helper.rb', __FILE__)
2
2
 
3
- class City < Structure
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 < Structure
10
+ class Neighborhood < Document
11
11
  key :name
12
12
  end
13
13
 
14
- class Dummy < Structure
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
- # assert_kind_of Neighborhood, City.first.neighborhoods.first
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.15.1
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-12 00:00:00.000000000Z
13
- dependencies: []
14
- description: Structure is a Struct-like key/value container.
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/static.rb
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
- - test/structure_test.rb
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: 2919723217978587663
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 Struct-like key/value container
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.
@@ -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
@@ -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