gom 0.1.0
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/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
|