gom 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +111 -0
- data/Rakefile +48 -0
- data/lib/gom.rb +7 -0
- data/lib/gom/object.rb +23 -0
- data/lib/gom/object/id.rb +30 -0
- data/lib/gom/object/injector.rb +45 -0
- data/lib/gom/object/inspector.rb +55 -0
- data/lib/gom/object/mapping.rb +61 -0
- data/lib/gom/object/proxy.rb +44 -0
- data/lib/gom/spec.rb +4 -0
- data/lib/gom/spec/acceptance/adapter_with_stateful_storage.rb +111 -0
- data/lib/gom/spec/acceptance/read_only_adapter_with_stateless_storage.rb +50 -0
- data/lib/gom/storage.rb +35 -0
- data/lib/gom/storage/adapter.rb +51 -0
- data/lib/gom/storage/configuration.rb +65 -0
- data/lib/gom/storage/fetcher.rb +69 -0
- data/lib/gom/storage/remover.rb +47 -0
- data/lib/gom/storage/saver.rb +59 -0
- data/spec/acceptance/adapter_spec.rb +10 -0
- data/spec/acceptance/object_spec.rb +33 -0
- data/spec/fake_adapter.rb +37 -0
- data/spec/lib/gom/object/id_spec.rb +56 -0
- data/spec/lib/gom/object/injector_spec.rb +51 -0
- data/spec/lib/gom/object/inspector_spec.rb +30 -0
- data/spec/lib/gom/object/mapping_spec.rb +158 -0
- data/spec/lib/gom/object/proxy_spec.rb +91 -0
- data/spec/lib/gom/object_spec.rb +40 -0
- data/spec/lib/gom/storage/adapter_spec.rb +73 -0
- data/spec/lib/gom/storage/configuration_spec.rb +92 -0
- data/spec/lib/gom/storage/fetcher_spec.rb +89 -0
- data/spec/lib/gom/storage/remover_spec.rb +47 -0
- data/spec/lib/gom/storage/saver_spec.rb +86 -0
- data/spec/lib/gom/storage_spec.rb +106 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/storage.configuration +4 -0
- metadata +138 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Philipp Brüll
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
|
2
|
+
= Generic Object Mapper
|
3
|
+
|
4
|
+
The Generic Object Mapper maps ruby objects to different storage engines and vice versa. The interface is designed to
|
5
|
+
be small and try to avoid any unnecessary dependencies between GOM and your code. On the other side, the storage engine
|
6
|
+
is plugged-in via an adapter interface. Currently, the following adapters are provided.
|
7
|
+
|
8
|
+
* filesystem - http://github.com/phifty/gom-filesystem-adapter
|
9
|
+
* couchdb - http://github.com/phifty/gom-couchdb-adapter
|
10
|
+
|
11
|
+
== Configuration
|
12
|
+
|
13
|
+
At the beginning of your program the configuration should be read with the following command.
|
14
|
+
|
15
|
+
GOM::Storage::Configuration.read filename
|
16
|
+
|
17
|
+
The configuration file should be written in the YML format and look like...
|
18
|
+
|
19
|
+
storage_name:
|
20
|
+
adapter: filesystem
|
21
|
+
directory: /var/project-name/data
|
22
|
+
|
23
|
+
Look at the adapter pages to see the adapter-specific configuration values.
|
24
|
+
|
25
|
+
== How to use
|
26
|
+
|
27
|
+
=== Storing an object
|
28
|
+
|
29
|
+
To store an object just pass it to <tt>GOM::Storage.store</tt>.
|
30
|
+
|
31
|
+
class Book
|
32
|
+
|
33
|
+
attr_accessor :author_name
|
34
|
+
attr_accessor :pages
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
book = Book.new
|
39
|
+
book.author_name = "Mr. Storyteller"
|
40
|
+
book.pages = 1253
|
41
|
+
|
42
|
+
GOM::Storage.store book, :storage_name
|
43
|
+
|
44
|
+
The storage name doesn't have to be specified. If it's missing, the object's previously use storage or the default
|
45
|
+
storage is used.
|
46
|
+
|
47
|
+
There is no base class needed for your model class. GOM inspects your object, reads all the instance variables and
|
48
|
+
passes the values to specified store adapter. The first time an object is stored, an id is generated and assigned to
|
49
|
+
the object. This id an be determined by calling <tt>GOM::Object.id</tt>.
|
50
|
+
|
51
|
+
book_id = GOM::Object.id book
|
52
|
+
# book_id => "storage_name:1234..."
|
53
|
+
|
54
|
+
=== Fetching an object
|
55
|
+
|
56
|
+
Once an object is stored, it can be easily brought back to life by using it's id to fetch it from the storage.
|
57
|
+
|
58
|
+
book = GOM::Storage.fetch book_id
|
59
|
+
|
60
|
+
The storage name is encoded (prefixed) in the id and don't have to be specified. The classname of the object was also
|
61
|
+
saved during the storage and the fetch instantiate a new object using the constructor. If the constructor requires
|
62
|
+
arguments, <tt>nil</tt> will be passed for each of them. The internal state (the instance variables) will be
|
63
|
+
overwritten anyway.
|
64
|
+
|
65
|
+
=== Removing an object
|
66
|
+
|
67
|
+
To remove an object from the storage, simply pass it to <tt>GOM::Storage.remove</tt>.
|
68
|
+
|
69
|
+
GOM::Storage.remove book
|
70
|
+
|
71
|
+
It's also possible to use just the id to remove the assigned object.
|
72
|
+
|
73
|
+
GOM::Storage.remove book_id
|
74
|
+
|
75
|
+
== Relations
|
76
|
+
|
77
|
+
GOM does make a distinction between object properties and object relations. The properties are more atomic values that
|
78
|
+
can be stored in a key/value-way and relations are links to more complex objects. Since in Ruby everything is an object,
|
79
|
+
it's necessary to mark the relations. This is done by the following way.
|
80
|
+
|
81
|
+
class Book
|
82
|
+
|
83
|
+
attr_accessor :author
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
class Author
|
88
|
+
|
89
|
+
attr_accessor :name
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
author = Author.new
|
94
|
+
author.name = "Mr. Storyteller"
|
95
|
+
|
96
|
+
book = Book.new
|
97
|
+
book.author = GOM::Object.reference author
|
98
|
+
|
99
|
+
The <tt>GOM::Object.reference</tt> call creates a proxy to the referenced object, that passes every call to it. For
|
100
|
+
example, the call
|
101
|
+
|
102
|
+
book.author.name
|
103
|
+
|
104
|
+
will return the instance variable <tt>@name</tt> ("Mr. Storyteller") from the author object.
|
105
|
+
|
106
|
+
== Development
|
107
|
+
|
108
|
+
Development has been done test-driven and the code follows at most the Clean Code paradigms. Code smells has been
|
109
|
+
removed by using the reek[http://github.com/kevinrutherford/reek] code smell detector.
|
110
|
+
|
111
|
+
This project is still experimental and under development. Any bug report and contribution is welcome!
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'rspec'
|
3
|
+
gem 'reek'
|
4
|
+
require 'rspec'
|
5
|
+
require 'rake/rdoctask'
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
require 'reek/rake/task'
|
8
|
+
|
9
|
+
task :default => :spec
|
10
|
+
|
11
|
+
namespace :gem do
|
12
|
+
|
13
|
+
desc "Builds the gem"
|
14
|
+
task :build do
|
15
|
+
system "gem build *.gemspec && mkdir -p pkg/ && mv *.gem pkg/"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Builds and installs the gem"
|
19
|
+
task :install => :build do
|
20
|
+
system "gem install pkg/"
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
Reek::Rake::Task.new do |task|
|
26
|
+
task.fail_on_error = true
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Generate the rdoc"
|
30
|
+
Rake::RDocTask.new do |rdoc|
|
31
|
+
rdoc.rdoc_files.add [ "README.rdoc", "lib/**/*.rb" ]
|
32
|
+
rdoc.main = "README.rdoc"
|
33
|
+
rdoc.title = ""
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Run all specs in spec directory"
|
37
|
+
RSpec::Core::RakeTask.new do |task|
|
38
|
+
task.pattern = "spec/gom/**/*_spec.rb"
|
39
|
+
end
|
40
|
+
|
41
|
+
namespace :spec do
|
42
|
+
|
43
|
+
desc "Run all integration specs in spec/acceptance directory"
|
44
|
+
RSpec::Core::RakeTask.new(:acceptance) do |task|
|
45
|
+
task.pattern = "spec/acceptance/**/*_spec.rb"
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/lib/gom.rb
ADDED
data/lib/gom/object.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
module GOM
|
3
|
+
|
4
|
+
module Object
|
5
|
+
|
6
|
+
autoload :Id, File.join(File.dirname(__FILE__), "object", "id")
|
7
|
+
autoload :Injector, File.join(File.dirname(__FILE__), "object", "injector")
|
8
|
+
autoload :Inspector, File.join(File.dirname(__FILE__), "object", "inspector")
|
9
|
+
autoload :Mapping, File.join(File.dirname(__FILE__), "object", "mapping")
|
10
|
+
autoload :Proxy, File.join(File.dirname(__FILE__), "object", "proxy")
|
11
|
+
|
12
|
+
def self.id(object)
|
13
|
+
id = Mapping.id_by_object object
|
14
|
+
id ? id.to_s : nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.reference(object)
|
18
|
+
Proxy.new object
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
module GOM
|
3
|
+
|
4
|
+
module Object
|
5
|
+
|
6
|
+
# Value class for object ids.
|
7
|
+
class Id
|
8
|
+
|
9
|
+
attr_accessor :storage_name
|
10
|
+
attr_accessor :object_id
|
11
|
+
|
12
|
+
def initialize(id_or_storage_name = nil, object_id = nil)
|
13
|
+
@storage_name, @object_id = id_or_storage_name.is_a?(String) ?
|
14
|
+
(object_id.is_a?(String) ? [ id_or_storage_name, object_id ] : id_or_storage_name.split(":")) :
|
15
|
+
[ nil, nil ]
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
other.is_a?(self.class) && @storage_name == other.storage_name && @object_id == other.object_id
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"#{@storage_name}:#{@object_id}"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
module GOM
|
3
|
+
|
4
|
+
module Object
|
5
|
+
|
6
|
+
# Injects the given properties into the given object.s
|
7
|
+
class Injector
|
8
|
+
|
9
|
+
attr_reader :object
|
10
|
+
|
11
|
+
def initialize(object, object_hash)
|
12
|
+
@object, @object_hash = object, object_hash
|
13
|
+
end
|
14
|
+
|
15
|
+
def perform
|
16
|
+
clear_instance_variables
|
17
|
+
write_properties
|
18
|
+
write_relations
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def clear_instance_variables
|
24
|
+
@object.instance_variables.each do |name|
|
25
|
+
@object.send :remove_instance_variable, name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def write_properties
|
30
|
+
(@object_hash[:properties] || { }).each do |name, value|
|
31
|
+
@object.instance_variable_set :"@#{name}", value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def write_relations
|
36
|
+
(@object_hash[:relations] || { }).each do |name, value|
|
37
|
+
@object.instance_variable_set :"@#{name}", value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
|
2
|
+
module GOM
|
3
|
+
|
4
|
+
module Object
|
5
|
+
|
6
|
+
# Inspect an object and returns it's class and it's properties
|
7
|
+
class Inspector
|
8
|
+
|
9
|
+
attr_reader :object
|
10
|
+
attr_reader :object_hash
|
11
|
+
|
12
|
+
def initialize(object)
|
13
|
+
@object = object
|
14
|
+
@object_hash = { }
|
15
|
+
end
|
16
|
+
|
17
|
+
def perform
|
18
|
+
read_class
|
19
|
+
read_properties
|
20
|
+
read_relations
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def read_class
|
26
|
+
@object_hash[:class] = @object.class.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def read_properties
|
30
|
+
@object_hash[:properties] = { }
|
31
|
+
read_instance_variables do |key, value|
|
32
|
+
@object_hash[:properties][key] = value unless value.is_a?(GOM::Object::Proxy)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def read_relations
|
37
|
+
@object_hash[:relations] = { }
|
38
|
+
read_instance_variables do |key, value|
|
39
|
+
@object_hash[:relations][key] = value if value.is_a?(GOM::Object::Proxy)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def read_instance_variables
|
44
|
+
@object.instance_variables.each do |name|
|
45
|
+
key = name.to_s.sub(/^@/, "").to_sym
|
46
|
+
value = @object.instance_variable_get name
|
47
|
+
yield key, value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
module GOM
|
3
|
+
|
4
|
+
module Object
|
5
|
+
|
6
|
+
# Provides a mapping between objects and ids
|
7
|
+
class Mapping
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@map = { }
|
11
|
+
end
|
12
|
+
|
13
|
+
def put(object, id)
|
14
|
+
@map[object] = id
|
15
|
+
end
|
16
|
+
|
17
|
+
def object_by_id(id)
|
18
|
+
@map.respond_to?(:key) ? @map.key(id) : @map.index(id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def id_by_object(object)
|
22
|
+
@map[object]
|
23
|
+
end
|
24
|
+
|
25
|
+
def remove_by_id(id)
|
26
|
+
@map.delete object_by_id(id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_by_object(object)
|
30
|
+
@map.delete object
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.singleton
|
34
|
+
@mapping ||= self.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.put(object, id)
|
38
|
+
self.singleton.put object, id
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.object_by_id(id)
|
42
|
+
self.singleton.object_by_id id
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.id_by_object(object)
|
46
|
+
self.singleton.id_by_object object
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.remove_by_id(id)
|
50
|
+
self.singleton.remove_by_id id
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.remove_by_object(object)
|
54
|
+
self.singleton.remove_by_object object
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
module GOM
|
3
|
+
|
4
|
+
module Object
|
5
|
+
|
6
|
+
# The proxy that fetches an object if it's needed and simply passes method calls to it.
|
7
|
+
class Proxy
|
8
|
+
|
9
|
+
def initialize(object_or_id)
|
10
|
+
@object, @id = object_or_id.is_a?(GOM::Object::Id) ?
|
11
|
+
[ nil, object_or_id ] :
|
12
|
+
[ object_or_id, nil ]
|
13
|
+
end
|
14
|
+
|
15
|
+
def object
|
16
|
+
fetch_object unless @object
|
17
|
+
@object
|
18
|
+
end
|
19
|
+
|
20
|
+
def id
|
21
|
+
fetch_id unless @id
|
22
|
+
@id
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(method_name, *arguments, &block)
|
26
|
+
fetch_object unless @object
|
27
|
+
@object.send method_name, *arguments, &block
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def fetch_object
|
33
|
+
@object = GOM::Storage::Fetcher.new(@id).object
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch_id
|
37
|
+
@id = GOM::Object::Mapping.id_by_object @object
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|