structure 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|