structure 0.16.0 → 0.17.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +5 -7
- data/README.md +3 -17
- data/lib/structure/rails.rb +19 -0
- data/lib/structure/static.rb +40 -0
- data/lib/structure/version.rb +2 -2
- data/lib/structure.rb +153 -1
- data/structure.gemspec +1 -5
- data/test/structure_test.rb +140 -0
- metadata +10 -52
- data/lib/structure/collection.rb +0 -67
- data/lib/structure/document/static.rb +0 -66
- data/lib/structure/document.rb +0 -212
- data/test/collection_test.rb +0 -33
- data/test/document_test.rb +0 -113
- data/test/fixtures/cities_with_neighborhoods.yml +0 -7
- data/test/helper.rb +0 -13
- data/test/static_test.rb +0 -62
data/Gemfile
CHANGED
@@ -2,13 +2,11 @@ source :rubygems
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
-
gem '
|
5
|
+
gem 'activesupport', '~> 3.0'
|
6
|
+
gem 'json', :platform => [:mri_18, :jruby, :rbx]
|
6
7
|
gem 'rake'
|
7
8
|
|
8
|
-
|
9
|
-
gem 'ruby-debug',
|
10
|
-
|
11
|
-
|
12
|
-
platforms :mri_19 do
|
13
|
-
gem 'ruby-debug19', :require => 'ruby-debug' unless ENV['CI']
|
9
|
+
unless ENV['CI']
|
10
|
+
gem 'ruby-debug', :platforms => :mri_18
|
11
|
+
gem 'ruby-debug19', :platforms => :mri_19
|
14
12
|
end
|
data/README.md
CHANGED
@@ -4,25 +4,11 @@
|
|
4
4
|
|
5
5
|
Structure is a typed, nestable key/value container.
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
require 'structure'
|
12
|
-
|
13
|
-
Define a model.
|
14
|
-
|
15
|
-
Document = Structure::Document
|
16
|
-
|
17
|
-
class Person < Document
|
18
|
-
key :name
|
19
|
-
many :friends, :class_name => 'Person'
|
7
|
+
class Person < Structure
|
8
|
+
key :name, String
|
9
|
+
many :friends
|
20
10
|
end
|
21
11
|
|
22
|
-
person = Person.create(:name => 'John')
|
23
|
-
person.friends << Person.create(:name => 'Jane')
|
24
|
-
person.friends.size # 1
|
25
|
-
|
26
12
|
Please see [the project page] [1] for more detailed info.
|
27
13
|
|
28
14
|
[1]: http://code.papercavalier.com/structure/
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Structure
|
2
|
+
# Converts structure to a JSON representation.
|
3
|
+
def as_json(options = nil)
|
4
|
+
subset = if options
|
5
|
+
if only = options[:only]
|
6
|
+
attributes.slice(*Array.wrap(only))
|
7
|
+
elsif except = options[:except]
|
8
|
+
attributes.except(*Array.wrap(except))
|
9
|
+
else
|
10
|
+
attributes.dup
|
11
|
+
end
|
12
|
+
else
|
13
|
+
attributes.dup
|
14
|
+
end
|
15
|
+
|
16
|
+
{ JSON.create_id => self.class.name }.
|
17
|
+
merge(subset)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# This module provides the class methods that render a structure
|
4
|
+
# static, where records are sourced from a YAML file.
|
5
|
+
class Structure
|
6
|
+
module Static
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def self.extended(base)
|
10
|
+
base.key(:_id, Integer)
|
11
|
+
end
|
12
|
+
|
13
|
+
# The data file path.
|
14
|
+
attr :data_path
|
15
|
+
|
16
|
+
# Returns all records.
|
17
|
+
def all
|
18
|
+
@all ||= YAML.load_file(data_path).map do |hsh|
|
19
|
+
hsh['_id'] ||= hsh.delete('id') || hsh.delete('ID') || incr_id
|
20
|
+
new(hsh)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Yields each record to given block.
|
25
|
+
def each(&block)
|
26
|
+
all.each { |item| block.call(item) }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Finds a record by its ID.
|
30
|
+
def find(id)
|
31
|
+
super() { |item| item._id == id }
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def incr_id
|
37
|
+
@id_cnt = @id_cnt.to_i + 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/structure/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = '0.
|
1
|
+
class Structure
|
2
|
+
VERSION = '0.17.1'
|
3
3
|
end
|
data/lib/structure.rb
CHANGED
@@ -1 +1,153 @@
|
|
1
|
-
|
1
|
+
begin
|
2
|
+
JSON::JSON_LOADED
|
3
|
+
rescue NameError
|
4
|
+
require 'json'
|
5
|
+
end
|
6
|
+
|
7
|
+
# A structure is a nestable key/value container.
|
8
|
+
#
|
9
|
+
# class Person < Structure
|
10
|
+
# key :name
|
11
|
+
# key :age, Integer
|
12
|
+
# many :friends
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
class Structure
|
16
|
+
include Enumerable
|
17
|
+
|
18
|
+
autoload :Static,'structure/static'
|
19
|
+
|
20
|
+
class << self
|
21
|
+
# Returns attribute keys and their default values.
|
22
|
+
def defaults
|
23
|
+
@defaults ||= {}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Builds a structure out of the JSON representation of a
|
27
|
+
# structure.
|
28
|
+
def json_create(hsh)
|
29
|
+
hsh.delete 'json_class'
|
30
|
+
new hsh
|
31
|
+
end
|
32
|
+
|
33
|
+
# Defines an attribute.
|
34
|
+
#
|
35
|
+
# Takes a name and, optionally, a type and options hash.
|
36
|
+
#
|
37
|
+
# The type should be a Ruby class.
|
38
|
+
#
|
39
|
+
# Available options are:
|
40
|
+
#
|
41
|
+
# * +:default+, which specifies a default value for the attribute.
|
42
|
+
def key(name, *args)
|
43
|
+
name = name.to_sym
|
44
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
45
|
+
type = args.shift
|
46
|
+
default = options[:default]
|
47
|
+
|
48
|
+
if method_defined?(name)
|
49
|
+
raise NameError, "#{name} is taken"
|
50
|
+
end
|
51
|
+
|
52
|
+
unless type.nil? || type.is_a?(Class)
|
53
|
+
raise TypeError, "#{type} isn't a Class"
|
54
|
+
end
|
55
|
+
|
56
|
+
if default.nil? || default.is_a?(type)
|
57
|
+
defaults[name] = default
|
58
|
+
else
|
59
|
+
raise TypeError, "#{default} isn't a #{type}"
|
60
|
+
end
|
61
|
+
|
62
|
+
define_method(name) { attributes[name] }
|
63
|
+
|
64
|
+
if type.nil?
|
65
|
+
define_method("#{name}=") { |val| attributes[name] = val }
|
66
|
+
elsif Kernel.respond_to? type.to_s
|
67
|
+
define_method("#{name}=") do |val|
|
68
|
+
attributes[name] =
|
69
|
+
if val.nil? || val.is_a?(type)
|
70
|
+
val
|
71
|
+
else
|
72
|
+
Kernel.send(type.to_s, val)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
else
|
76
|
+
define_method("#{name}=") do |val|
|
77
|
+
attributes[name] =
|
78
|
+
if val.nil? || val.is_a?(type)
|
79
|
+
val
|
80
|
+
else
|
81
|
+
raise TypeError, "#{val} isn't a #{type}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# A shorthand that defines an attribute that is an array.
|
88
|
+
def many(name)
|
89
|
+
key name, Array, :default => []
|
90
|
+
end
|
91
|
+
|
92
|
+
# Renders the structure static by setting the path for a YAML file
|
93
|
+
# that stores the records.
|
94
|
+
def set_data_file(path)
|
95
|
+
extend Static unless self.respond_to? :all
|
96
|
+
|
97
|
+
@data_path = path
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Creates a new structure.
|
102
|
+
#
|
103
|
+
# A hash, if provided, will seed the attributes.
|
104
|
+
def initialize(hsh = {})
|
105
|
+
@attributes = self.class.defaults.inject({}) do |a, (k, v)|
|
106
|
+
a[k] = v.is_a?(Array) ? v.dup : v
|
107
|
+
a
|
108
|
+
end
|
109
|
+
|
110
|
+
hsh.each { |k, v| self.send("#{k}=", v) }
|
111
|
+
end
|
112
|
+
|
113
|
+
# The attributes that make up the structure.
|
114
|
+
attr :attributes
|
115
|
+
|
116
|
+
# Calls block once for each attribute in the structure, passing that
|
117
|
+
# attribute as a parameter.
|
118
|
+
def each(&block)
|
119
|
+
attributes.each { |v| block.call(v) }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Converts structure to a hash.
|
123
|
+
def to_hash
|
124
|
+
attributes.inject({}) do |a, (k, v)|
|
125
|
+
a[k] =
|
126
|
+
if v.respond_to? :to_hash
|
127
|
+
v.to_hash
|
128
|
+
elsif v.is_a? Array
|
129
|
+
v.map { |e| e.respond_to?(:to_hash) ? e.to_hash : e }
|
130
|
+
else
|
131
|
+
v
|
132
|
+
end
|
133
|
+
|
134
|
+
a
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Converts structure to a JSON representation.
|
139
|
+
def to_json(*args)
|
140
|
+
{ JSON.create_id => self.class.name }.
|
141
|
+
merge(attributes).
|
142
|
+
to_json(*args)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Compares this object with another object for equality. A Structure
|
146
|
+
# is equal to the other object when both are of the same class and
|
147
|
+
# the their attributes are the same.
|
148
|
+
def ==(other)
|
149
|
+
other.is_a?(self.class) && attributes == other.attributes
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
require 'structure/rails' if defined?(Rails)
|
data/structure.gemspec
CHANGED
@@ -10,14 +10,10 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.email = ['code@papercavalier.com']
|
11
11
|
s.homepage = 'http://github.com/hakanensari/structure'
|
12
12
|
s.summary = 'A typed, nestable key/value container'
|
13
|
-
s.description = '
|
13
|
+
s.description = 'Structure is a typed, nestable key/value container.'
|
14
14
|
|
15
15
|
s.rubyforge_project = 'structure'
|
16
16
|
|
17
|
-
s.add_dependency 'certainty', '~> 0.2.0'
|
18
|
-
s.add_dependency 'activesupport', '~> 3.0'
|
19
|
-
s.add_dependency 'i18n', '~> 0.6.0'
|
20
|
-
|
21
17
|
s.files = `git ls-files`.split("\n")
|
22
18
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
23
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
@@ -0,0 +1,140 @@
|
|
1
|
+
$:.push File.expand_path('../../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'ruby-debug'
|
7
|
+
rescue LoadError
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'structure'
|
11
|
+
require 'test/unit'
|
12
|
+
|
13
|
+
class Test::Unit::TestCase
|
14
|
+
def self.test(name, &block)
|
15
|
+
test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
|
16
|
+
if method_defined? test_name
|
17
|
+
raise "#{test_name} is already defined in #{self}"
|
18
|
+
end
|
19
|
+
define_method test_name, &block
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Person < Structure
|
24
|
+
key :name
|
25
|
+
key :age, Integer
|
26
|
+
many :friends
|
27
|
+
end
|
28
|
+
|
29
|
+
class TestStructure < Test::Unit::TestCase
|
30
|
+
test "should enumerate" do
|
31
|
+
assert_respond_to Person.new, :map
|
32
|
+
end
|
33
|
+
|
34
|
+
test "should define accessors" do
|
35
|
+
assert_respond_to Person.new, :name
|
36
|
+
assert_respond_to Person.new, :name=
|
37
|
+
end
|
38
|
+
|
39
|
+
test "should raise errors" do
|
40
|
+
assert_raise(NameError) { Person.key :class }
|
41
|
+
assert_raise(TypeError) { Person.key :foo, Module.new }
|
42
|
+
assert_raise(TypeError) { Person.key :foo, String, :default => 1 }
|
43
|
+
end
|
44
|
+
|
45
|
+
test "should store defaults" do
|
46
|
+
assert_equal [], Person.new.friends
|
47
|
+
end
|
48
|
+
|
49
|
+
test "should typecheck" do
|
50
|
+
person = Person.new
|
51
|
+
person.age = '18'
|
52
|
+
assert_equal 18, person.age
|
53
|
+
|
54
|
+
person.age = nil
|
55
|
+
assert_nil person.age
|
56
|
+
end
|
57
|
+
|
58
|
+
test "should handle arrays" do
|
59
|
+
person = Person.new
|
60
|
+
assert_equal [], person.friends
|
61
|
+
|
62
|
+
person.friends << Person.new
|
63
|
+
assert_equal 1, person.friends.size
|
64
|
+
assert_equal 0, person.friends.first.friends.size
|
65
|
+
end
|
66
|
+
|
67
|
+
test "should translate to hash" do
|
68
|
+
person = Person.new(:name => 'John')
|
69
|
+
person.friends << Person.new(:name => 'Jane')
|
70
|
+
assert_equal 'John', person.to_hash[:name]
|
71
|
+
assert_equal 'Jane', person.to_hash[:friends].first[:name]
|
72
|
+
end
|
73
|
+
|
74
|
+
test "should translate to JSON" do
|
75
|
+
person = Person.new
|
76
|
+
person.friends << Person.new
|
77
|
+
json = person.to_json
|
78
|
+
assert_kind_of Person, JSON.parse(json)
|
79
|
+
assert_kind_of Person, JSON.parse(json).friends.first
|
80
|
+
end
|
81
|
+
|
82
|
+
test "should translate to JSON in a Rails app" do
|
83
|
+
person = Person.new
|
84
|
+
assert_equal false, person.respond_to?(:as_json)
|
85
|
+
|
86
|
+
require 'active_support/ordered_hash'
|
87
|
+
require 'active_support/json'
|
88
|
+
require 'structure/rails'
|
89
|
+
assert_equal true, person.as_json(:only => :name).has_key?(:name)
|
90
|
+
assert_equal false, person.as_json(:except => :name).has_key?(:name)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class City < Structure
|
95
|
+
key :name
|
96
|
+
end
|
97
|
+
|
98
|
+
class Stadt < Structure
|
99
|
+
key :name
|
100
|
+
end
|
101
|
+
|
102
|
+
class TestStatic < Test::Unit::TestCase
|
103
|
+
def fix(klass, path)
|
104
|
+
klass.instance_variable_set(:@all, nil)
|
105
|
+
klass.instance_variable_set(:@id_cnt, nil)
|
106
|
+
fixture = File.expand_path("../fixtures/#{path}.yml", __FILE__)
|
107
|
+
klass.set_data_file(fixture)
|
108
|
+
end
|
109
|
+
|
110
|
+
test "should enumerate at the class level" do
|
111
|
+
fix City, 'cities'
|
112
|
+
assert_respond_to City, :map
|
113
|
+
end
|
114
|
+
|
115
|
+
test "should return all records" do
|
116
|
+
fix City, 'cities'
|
117
|
+
cities = City.all
|
118
|
+
assert_kind_of City, cities.first
|
119
|
+
assert_equal 2, cities.size
|
120
|
+
end
|
121
|
+
|
122
|
+
test "should find a record" do
|
123
|
+
fix City, 'cities'
|
124
|
+
assert 'New York', City.find(1).name
|
125
|
+
assert_nil City.find(4)
|
126
|
+
end
|
127
|
+
|
128
|
+
test "should work if records contain no id field" do
|
129
|
+
fix City, 'cities_without_ids'
|
130
|
+
assert_equal 'New York', City.find(1).name
|
131
|
+
assert_equal 'Paris', City.find(3).name
|
132
|
+
end
|
133
|
+
|
134
|
+
test "should auto increment independently in each structure" do
|
135
|
+
fix City, 'cities_without_ids'
|
136
|
+
fix Stadt, 'cities_without_ids'
|
137
|
+
assert_equal 'New York', City.find(1).name
|
138
|
+
assert_equal 'New York', Stadt.find(1).name
|
139
|
+
end
|
140
|
+
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.17.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,42 +9,9 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-08-
|
13
|
-
dependencies:
|
14
|
-
|
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
|
12
|
+
date: 2011-08-26 00:00:00.000000000Z
|
13
|
+
dependencies: []
|
14
|
+
description: Structure is a typed, nestable key/value container.
|
48
15
|
email:
|
49
16
|
- code@papercavalier.com
|
50
17
|
executables: []
|
@@ -58,18 +25,13 @@ files:
|
|
58
25
|
- README.md
|
59
26
|
- Rakefile
|
60
27
|
- lib/structure.rb
|
61
|
-
- lib/structure/
|
62
|
-
- lib/structure/
|
63
|
-
- lib/structure/document/static.rb
|
28
|
+
- lib/structure/rails.rb
|
29
|
+
- lib/structure/static.rb
|
64
30
|
- lib/structure/version.rb
|
65
31
|
- structure.gemspec
|
66
|
-
- test/collection_test.rb
|
67
|
-
- test/document_test.rb
|
68
32
|
- test/fixtures/cities.yml
|
69
|
-
- test/fixtures/cities_with_neighborhoods.yml
|
70
33
|
- test/fixtures/cities_without_ids.yml
|
71
|
-
- test/
|
72
|
-
- test/static_test.rb
|
34
|
+
- test/structure_test.rb
|
73
35
|
homepage: http://github.com/hakanensari/structure
|
74
36
|
licenses: []
|
75
37
|
post_install_message:
|
@@ -84,7 +46,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
46
|
version: '0'
|
85
47
|
segments:
|
86
48
|
- 0
|
87
|
-
hash: -
|
49
|
+
hash: -4025252562895677644
|
88
50
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
51
|
none: false
|
90
52
|
requirements:
|
@@ -93,7 +55,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
55
|
version: '0'
|
94
56
|
segments:
|
95
57
|
- 0
|
96
|
-
hash: -
|
58
|
+
hash: -4025252562895677644
|
97
59
|
requirements: []
|
98
60
|
rubyforge_project: structure
|
99
61
|
rubygems_version: 1.8.6
|
@@ -101,10 +63,6 @@ signing_key:
|
|
101
63
|
specification_version: 3
|
102
64
|
summary: A typed, nestable key/value container
|
103
65
|
test_files:
|
104
|
-
- test/collection_test.rb
|
105
|
-
- test/document_test.rb
|
106
66
|
- test/fixtures/cities.yml
|
107
|
-
- test/fixtures/cities_with_neighborhoods.yml
|
108
67
|
- test/fixtures/cities_without_ids.yml
|
109
|
-
- test/
|
110
|
-
- test/static_test.rb
|
68
|
+
- test/structure_test.rb
|
data/lib/structure/collection.rb
DELETED
@@ -1,67 +0,0 @@
|
|
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
|
@@ -1,66 +0,0 @@
|
|
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
|
data/lib/structure/document.rb
DELETED
@@ -1,212 +0,0 @@
|
|
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/test/collection_test.rb
DELETED
@@ -1,33 +0,0 @@
|
|
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
|
data/test/document_test.rb
DELETED
@@ -1,113 +0,0 @@
|
|
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
DELETED
data/test/static_test.rb
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
require File.expand_path('../helper.rb', __FILE__)
|
2
|
-
|
3
|
-
class City < Document
|
4
|
-
include Static
|
5
|
-
|
6
|
-
key :name
|
7
|
-
many :neighborhoods
|
8
|
-
end
|
9
|
-
|
10
|
-
class Neighborhood < Document
|
11
|
-
key :name
|
12
|
-
end
|
13
|
-
|
14
|
-
class Dummy < Document
|
15
|
-
include Static
|
16
|
-
|
17
|
-
key :name
|
18
|
-
end
|
19
|
-
|
20
|
-
class TestStatic < Test::Unit::TestCase
|
21
|
-
def fixture(klass, path)
|
22
|
-
klass.instance_variable_set(:@records, nil)
|
23
|
-
klass.instance_variable_set(:@increment_id, nil)
|
24
|
-
fixture = File.expand_path("../fixtures/#{path}.yml", __FILE__)
|
25
|
-
klass.set_data_path(fixture)
|
26
|
-
end
|
27
|
-
|
28
|
-
def test_class_enumeration
|
29
|
-
assert_respond_to City, :map
|
30
|
-
end
|
31
|
-
|
32
|
-
def test_all
|
33
|
-
fixture City, 'cities'
|
34
|
-
cities = City.all
|
35
|
-
assert_kind_of City, cities.first
|
36
|
-
assert_equal 2, cities.size
|
37
|
-
end
|
38
|
-
|
39
|
-
def test_find
|
40
|
-
fixture City, 'cities'
|
41
|
-
assert 'New York', City.find(1).name
|
42
|
-
assert_nil City.find(4)
|
43
|
-
end
|
44
|
-
|
45
|
-
def test_data_without_ids
|
46
|
-
fixture City, 'cities_without_ids'
|
47
|
-
assert_equal 'New York', City.find(1).name
|
48
|
-
assert_equal 'Paris', City.find(3).name
|
49
|
-
end
|
50
|
-
|
51
|
-
def test_auto_increment
|
52
|
-
fixture City, 'cities_without_ids'
|
53
|
-
fixture Dummy, 'cities_without_ids'
|
54
|
-
assert_equal 'New York', City.find(1).name
|
55
|
-
assert_equal 'New York', Dummy.find(1).name
|
56
|
-
end
|
57
|
-
|
58
|
-
def test_nesting
|
59
|
-
fixture City, 'cities_with_neighborhoods'
|
60
|
-
assert_kind_of Neighborhood, City.first.neighborhoods.first
|
61
|
-
end
|
62
|
-
end
|