lotus-model 0.0.0 → 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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +6 -0
- data/.yardopts +5 -0
- data/EXAMPLE.md +217 -0
- data/Gemfile +14 -2
- data/README.md +303 -3
- data/Rakefile +17 -1
- data/lib/lotus-model.rb +1 -0
- data/lib/lotus/entity.rb +157 -0
- data/lib/lotus/model.rb +23 -2
- data/lib/lotus/model/adapters/abstract.rb +167 -0
- data/lib/lotus/model/adapters/implementation.rb +111 -0
- data/lib/lotus/model/adapters/memory/collection.rb +132 -0
- data/lib/lotus/model/adapters/memory/command.rb +90 -0
- data/lib/lotus/model/adapters/memory/query.rb +457 -0
- data/lib/lotus/model/adapters/memory_adapter.rb +149 -0
- data/lib/lotus/model/adapters/sql/collection.rb +209 -0
- data/lib/lotus/model/adapters/sql/command.rb +67 -0
- data/lib/lotus/model/adapters/sql/query.rb +615 -0
- data/lib/lotus/model/adapters/sql_adapter.rb +154 -0
- data/lib/lotus/model/mapper.rb +101 -0
- data/lib/lotus/model/mapping.rb +23 -0
- data/lib/lotus/model/mapping/coercer.rb +80 -0
- data/lib/lotus/model/mapping/collection.rb +336 -0
- data/lib/lotus/model/version.rb +4 -1
- data/lib/lotus/repository.rb +620 -0
- data/lotus-model.gemspec +15 -11
- data/test/entity_test.rb +126 -0
- data/test/fixtures.rb +81 -0
- data/test/model/adapters/abstract_test.rb +75 -0
- data/test/model/adapters/implementation_test.rb +22 -0
- data/test/model/adapters/memory/query_test.rb +91 -0
- data/test/model/adapters/memory_adapter_test.rb +1044 -0
- data/test/model/adapters/sql/query_test.rb +121 -0
- data/test/model/adapters/sql_adapter_test.rb +1078 -0
- data/test/model/mapper_test.rb +94 -0
- data/test/model/mapping/coercer_test.rb +27 -0
- data/test/model/mapping/collection_test.rb +82 -0
- data/test/repository_test.rb +283 -0
- data/test/test_helper.rb +30 -0
- data/test/version_test.rb +7 -0
- metadata +109 -11
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'lotus/model/adapters/abstract'
|
2
|
+
require 'lotus/model/adapters/implementation'
|
3
|
+
require 'lotus/model/adapters/sql/collection'
|
4
|
+
require 'lotus/model/adapters/sql/command'
|
5
|
+
require 'lotus/model/adapters/sql/query'
|
6
|
+
require 'sequel'
|
7
|
+
|
8
|
+
module Lotus
|
9
|
+
module Model
|
10
|
+
module Adapters
|
11
|
+
# Adapter for SQL databases
|
12
|
+
#
|
13
|
+
# In order to use it with a specific database, you must require the Ruby
|
14
|
+
# gem before of loading Lotus::Model.
|
15
|
+
#
|
16
|
+
# @see Lotus::Model::Adapters::Implementation
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
# @since 0.1.0
|
20
|
+
class SqlAdapter < Abstract
|
21
|
+
include Implementation
|
22
|
+
|
23
|
+
# Initialize the adapter.
|
24
|
+
#
|
25
|
+
# Lotus::Model uses Sequel. For a complete reference of the connection
|
26
|
+
# URI, please see: http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html
|
27
|
+
#
|
28
|
+
# @param mapper [Object] the database mapper
|
29
|
+
# @param uri [String] the connection uri for the database
|
30
|
+
#
|
31
|
+
# @return [Lotus::Model::Adapters::SqlAdapter]
|
32
|
+
#
|
33
|
+
# @raise [Lotus::Model::Adapters::DatabaseAdapterNotFound] if the given
|
34
|
+
# URI refers to an unknown or not registered adapter.
|
35
|
+
#
|
36
|
+
# @raise [URI::InvalidURIError] if the given URI is malformed
|
37
|
+
#
|
38
|
+
# @see Lotus::Model::Mapper
|
39
|
+
# @see http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
# @since 0.1.0
|
43
|
+
def initialize(mapper, uri)
|
44
|
+
super
|
45
|
+
@connection = Sequel.connect(@uri)
|
46
|
+
rescue Sequel::AdapterNotFound => e
|
47
|
+
raise DatabaseAdapterNotFound.new(e.message)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Creates a record in the database for the given entity.
|
51
|
+
# It assigns the `id` attribute, in case of success.
|
52
|
+
#
|
53
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
54
|
+
# @param entity [#id=] the entity to create
|
55
|
+
#
|
56
|
+
# @return [Object] the entity
|
57
|
+
#
|
58
|
+
# @api private
|
59
|
+
# @since 0.1.0
|
60
|
+
def create(collection, entity)
|
61
|
+
entity.id = command(
|
62
|
+
query(collection)
|
63
|
+
).create(entity)
|
64
|
+
entity
|
65
|
+
end
|
66
|
+
|
67
|
+
# Updates a record in the database corresponding to the given entity.
|
68
|
+
#
|
69
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
70
|
+
# @param entity [#id] the entity to update
|
71
|
+
#
|
72
|
+
# @return [Object] the entity
|
73
|
+
#
|
74
|
+
# @api private
|
75
|
+
# @since 0.1.0
|
76
|
+
def update(collection, entity)
|
77
|
+
command(
|
78
|
+
_find(collection, entity.id)
|
79
|
+
).update(entity)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Deletes a record in the database corresponding to the given entity.
|
83
|
+
#
|
84
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
85
|
+
# @param entity [#id] the entity to delete
|
86
|
+
#
|
87
|
+
# @api private
|
88
|
+
# @since 0.1.0
|
89
|
+
def delete(collection, entity)
|
90
|
+
command(
|
91
|
+
_find(collection, entity.id)
|
92
|
+
).delete
|
93
|
+
end
|
94
|
+
|
95
|
+
# Deletes all the records from the given collection.
|
96
|
+
#
|
97
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
98
|
+
#
|
99
|
+
# @api private
|
100
|
+
# @since 0.1.0
|
101
|
+
def clear(collection)
|
102
|
+
command(query(collection)).clear
|
103
|
+
end
|
104
|
+
|
105
|
+
# Fabricates a command for the given query.
|
106
|
+
#
|
107
|
+
# @param query [Lotus::Model::Adapters::Sql::Query] the query object to
|
108
|
+
# act on.
|
109
|
+
#
|
110
|
+
# @return [Lotus::Model::Adapters::Sql::Command]
|
111
|
+
#
|
112
|
+
# @see Lotus::Model::Adapters::Sql::Command
|
113
|
+
#
|
114
|
+
# @api private
|
115
|
+
# @since 0.1.0
|
116
|
+
def command(query)
|
117
|
+
Sql::Command.new(query)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Fabricates a query
|
121
|
+
#
|
122
|
+
# @param collection [Symbol] the target collection (it must be mapped).
|
123
|
+
# @param blk [Proc] a block of code to be executed in the context of
|
124
|
+
# the query.
|
125
|
+
#
|
126
|
+
# @return [Lotus::Model::Adapters::Sql::Query]
|
127
|
+
#
|
128
|
+
# @see Lotus::Model::Adapters::Sql::Query
|
129
|
+
#
|
130
|
+
# @api private
|
131
|
+
# @since 0.1.0
|
132
|
+
def query(collection, context = nil, &blk)
|
133
|
+
Sql::Query.new(_collection(collection), context, &blk)
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
# Returns a collection from the given name.
|
139
|
+
#
|
140
|
+
# @param name [Symbol] a name of the collection (it must be mapped).
|
141
|
+
#
|
142
|
+
# @return [Lotus::Model::Adapters::Sql::Collection]
|
143
|
+
#
|
144
|
+
# @see Lotus::Model::Adapters::Sql::Collection
|
145
|
+
#
|
146
|
+
# @api private
|
147
|
+
# @since 0.1.0
|
148
|
+
def _collection(name)
|
149
|
+
Sql::Collection.new(@connection[name], _mapped_collection(name))
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'lotus/model/mapping'
|
2
|
+
|
3
|
+
module Lotus
|
4
|
+
module Model
|
5
|
+
# A persistence mapper that keep entities independent from database details.
|
6
|
+
#
|
7
|
+
# This is database independent. It can work with SQL, document, and even
|
8
|
+
# with key/value stores.
|
9
|
+
#
|
10
|
+
# @since 0.1.0
|
11
|
+
#
|
12
|
+
# @see http://martinfowler.com/eaaCatalog/dataMapper.html
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# require 'lotus/model'
|
16
|
+
#
|
17
|
+
# mapper = Lotus::Model::Mapper.new do
|
18
|
+
# collection :users do
|
19
|
+
# entity User
|
20
|
+
#
|
21
|
+
# attribute :id, Integer
|
22
|
+
# attribute :name, String
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# # This guarantees thread-safety and should happen as last thing before
|
27
|
+
# # to start the app code.
|
28
|
+
# mapper.load!
|
29
|
+
class Mapper
|
30
|
+
# @attr_reader collections [Hash] all the mapped collections
|
31
|
+
#
|
32
|
+
# @since 0.1.0
|
33
|
+
# @api private
|
34
|
+
attr_reader :collections
|
35
|
+
|
36
|
+
# Instantiate a mapper.
|
37
|
+
#
|
38
|
+
# It accepts an optional argument (`coercer`) a class that defines the
|
39
|
+
# policies for entities translations from/to the database.
|
40
|
+
#
|
41
|
+
# If provided, this class must implement the following interface:
|
42
|
+
#
|
43
|
+
# * #initialize(collection) # Lotus::Model::Mapping::Collection
|
44
|
+
# * #to_record(entity) # translates an entity to the database type
|
45
|
+
# * #from_record(record) # translates a record into an entity
|
46
|
+
# * #deserialize_*(value) # a set of methods, one for each database column.
|
47
|
+
#
|
48
|
+
# If not given, it uses `Lotus::Model::Mapping::Coercer`, by default.
|
49
|
+
#
|
50
|
+
#
|
51
|
+
#
|
52
|
+
# @param coercer [Class] an optional class that defines the policies for
|
53
|
+
# entity translations from/to the database.
|
54
|
+
#
|
55
|
+
# @param blk [Proc] an optional block of code that gets evaluated in the
|
56
|
+
# context of the current instance
|
57
|
+
#
|
58
|
+
# @return [Lotus::Model::Mapper]
|
59
|
+
#
|
60
|
+
# @since 0.1.0
|
61
|
+
def initialize(coercer = nil, &blk)
|
62
|
+
@coercer = coercer || Mapping::Coercer
|
63
|
+
@collections = {}
|
64
|
+
instance_eval(&blk) if block_given?
|
65
|
+
end
|
66
|
+
|
67
|
+
# Maps a collection.
|
68
|
+
#
|
69
|
+
# A collection is a set of homogeneous records. Think of a table of a SQL
|
70
|
+
# database or about collection of MongoDB.
|
71
|
+
#
|
72
|
+
# @param name [Symbol] the name of the mapped collection. If used with a
|
73
|
+
# SQL database it's the table name.
|
74
|
+
#
|
75
|
+
# @param blk [Proc] the block that maps the attributes of that collection.
|
76
|
+
#
|
77
|
+
# @since 0.1.0
|
78
|
+
#
|
79
|
+
# @see Lotus::Model::Mapping::Collection
|
80
|
+
def collection(name, &blk)
|
81
|
+
if block_given?
|
82
|
+
@collections[name] = Mapping::Collection.new(name, @coercer, &blk)
|
83
|
+
else
|
84
|
+
# TODO implement a getter with a private API.
|
85
|
+
@collections[name] or raise Mapping::UnmappedCollectionError.new(name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Loads the internals of the mapper, in order to guarantee thread safety.
|
90
|
+
#
|
91
|
+
# This method MUST be invoked as the last thing before of start using the
|
92
|
+
# application.
|
93
|
+
#
|
94
|
+
# @since 0.1.0
|
95
|
+
def load!
|
96
|
+
@collections.each {|_, collection| collection.load! }
|
97
|
+
self
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'lotus/model/mapping/collection'
|
2
|
+
require 'lotus/model/mapping/coercer'
|
3
|
+
|
4
|
+
module Lotus
|
5
|
+
module Model
|
6
|
+
# Mapping internal utilities
|
7
|
+
#
|
8
|
+
# @since 0.1.0
|
9
|
+
module Mapping
|
10
|
+
# Unmapped collection error.
|
11
|
+
#
|
12
|
+
# It gets raised when the application tries to access to a non-mapped
|
13
|
+
# collection.
|
14
|
+
#
|
15
|
+
# @since 0.1.0
|
16
|
+
class UnmappedCollectionError < ::StandardError
|
17
|
+
def initialize(name)
|
18
|
+
super("Cannot find collection: #{ name }")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'lotus/utils/kernel'
|
2
|
+
|
3
|
+
module Lotus
|
4
|
+
module Model
|
5
|
+
module Mapping
|
6
|
+
# Translates values from/to the database with the corresponding Ruby type.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
# @since 0.1.0
|
10
|
+
class Coercer
|
11
|
+
# Initialize a coercer for the given collection.
|
12
|
+
#
|
13
|
+
# @param collection [Lotus::Model::Mapping::Collection] the collection
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
# @since 0.1.0
|
17
|
+
def initialize(collection)
|
18
|
+
@collection = collection
|
19
|
+
_compile!
|
20
|
+
end
|
21
|
+
|
22
|
+
# Translates the given entity into a format compatible with the database.
|
23
|
+
#
|
24
|
+
# @param entity [Object] the entity
|
25
|
+
#
|
26
|
+
# @return [Hash]
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
# @since 0.1.0
|
30
|
+
def to_record(entity)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Translates the given record into a Ruby object.
|
34
|
+
#
|
35
|
+
# @param record [Hash]
|
36
|
+
#
|
37
|
+
# @return [Object]
|
38
|
+
#
|
39
|
+
# @api private
|
40
|
+
# @since 0.1.0
|
41
|
+
def from_record(record)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
# Compile itself for perfomance boost.
|
46
|
+
#
|
47
|
+
# @api private
|
48
|
+
# @since 0.1.0
|
49
|
+
def _compile!
|
50
|
+
code = @collection.attributes.map do |_,(klass,mapped)|
|
51
|
+
%{
|
52
|
+
def deserialize_#{ mapped }(value)
|
53
|
+
Lotus::Utils::Kernel.#{klass}(value)
|
54
|
+
end
|
55
|
+
}
|
56
|
+
end.join("\n")
|
57
|
+
|
58
|
+
instance_eval %{
|
59
|
+
def to_record(entity)
|
60
|
+
if entity.id
|
61
|
+
Hash[*[#{ @collection.attributes.map{|name,(_,mapped)| ":#{mapped},entity.#{name}"}.join(',') }]]
|
62
|
+
else
|
63
|
+
Hash[*[#{ @collection.attributes.reject{|name,_| name == @collection.identity }.map{|name,(_,mapped)| ":#{mapped},entity.#{name}"}.join(',') }]]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def from_record(record)
|
68
|
+
#{ @collection.entity }.new(
|
69
|
+
Hash[*[#{ @collection.attributes.map{|name,(klass,mapped)| ":#{name},Lotus::Utils::Kernel.#{klass}(record[:#{mapped}])"}.join(',') }]]
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
#{ code }
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,336 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Model
|
3
|
+
module Mapping
|
4
|
+
# Maps a collection and its attributes.
|
5
|
+
#
|
6
|
+
# A collection is a set of homogeneous records. Think of a table of a SQL
|
7
|
+
# database or about collection of MongoDB.
|
8
|
+
#
|
9
|
+
# This is database independent. It can work with SQL, document, and even
|
10
|
+
# with key/value stores.
|
11
|
+
#
|
12
|
+
# @since 0.1.0
|
13
|
+
#
|
14
|
+
# @see Lotus::Model::Mapper
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# require 'lotus/model'
|
18
|
+
#
|
19
|
+
# mapper = Lotus::Model::Mapper.new do
|
20
|
+
# collection :users do
|
21
|
+
# entity User
|
22
|
+
#
|
23
|
+
# attribute :id, Integer
|
24
|
+
# attribute :name, String
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
class Collection
|
28
|
+
# Repository name suffix
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
# @since 0.1.0
|
32
|
+
#
|
33
|
+
# @see Lotus::Repository
|
34
|
+
REPOSITORY_SUFFIX = 'Repository'.freeze
|
35
|
+
|
36
|
+
# Defines top level constant for attribute usage.
|
37
|
+
#
|
38
|
+
# @since 0.1.0
|
39
|
+
#
|
40
|
+
# @see Lotus::Model::Mapping::Collection#attribute
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# require 'lotus/model'
|
44
|
+
#
|
45
|
+
# mapper = Lotus::Model::Mapper.new do
|
46
|
+
# collection :articles do
|
47
|
+
# entity Article
|
48
|
+
#
|
49
|
+
# attribute :published, Boolean
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
class ::Boolean
|
53
|
+
end
|
54
|
+
|
55
|
+
# @attr_reader name [Symbol] the name of the collection
|
56
|
+
#
|
57
|
+
# @since 0.1.0
|
58
|
+
# @api private
|
59
|
+
attr_reader :name
|
60
|
+
|
61
|
+
# @attr_reader coercer_class [Class] the coercer class
|
62
|
+
#
|
63
|
+
# @since 0.1.0
|
64
|
+
# @api private
|
65
|
+
attr_reader :coercer_class
|
66
|
+
|
67
|
+
# @attr_reader attributes [Hash] the set of attributes
|
68
|
+
#
|
69
|
+
# @since 0.1.0
|
70
|
+
# @api private
|
71
|
+
attr_reader :attributes
|
72
|
+
|
73
|
+
# Instantiate a new collection
|
74
|
+
#
|
75
|
+
# @param name [Symbol] the name of the mapped collection. If used with a
|
76
|
+
# SQL database it's the table name.
|
77
|
+
#
|
78
|
+
# @param blk [Proc] the block that maps the attributes of that collection.
|
79
|
+
#
|
80
|
+
# @since 0.1.0
|
81
|
+
#
|
82
|
+
# @see Lotus::Model::Mapper#collection
|
83
|
+
def initialize(name, coercer_class, &blk)
|
84
|
+
@name, @coercer_class, @attributes = name, coercer_class, {}
|
85
|
+
instance_eval(&blk) if block_given?
|
86
|
+
end
|
87
|
+
|
88
|
+
# Defines the entity that is persisted with this collection.
|
89
|
+
#
|
90
|
+
# The entity can be any kind of object as long it implements the
|
91
|
+
# following interface: `#initialize(attributes = {})`.
|
92
|
+
#
|
93
|
+
# @param klass [Class] the entity persisted with this collection.
|
94
|
+
#
|
95
|
+
# @since 0.1.0
|
96
|
+
#
|
97
|
+
# @see Lotus::Entity
|
98
|
+
def entity(klass = nil)
|
99
|
+
if klass
|
100
|
+
@entity = klass
|
101
|
+
else
|
102
|
+
@entity
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Defines the identity for a collection.
|
107
|
+
#
|
108
|
+
# An identity is an unique value that identifies a record.
|
109
|
+
# If used with an SQL table it corresponds to the primary key.
|
110
|
+
#
|
111
|
+
# This is an optional feature.
|
112
|
+
# By default the system assumes that your identity is `:id`.
|
113
|
+
# If this is the case, you can omit the value, otherwise you have to
|
114
|
+
# specify it.
|
115
|
+
#
|
116
|
+
# @param name [Symbol] the name of the identity
|
117
|
+
#
|
118
|
+
# @since 0.1.0
|
119
|
+
#
|
120
|
+
# @example Default
|
121
|
+
# require 'lotus/model'
|
122
|
+
#
|
123
|
+
# # We have an SQL table `users` with a primary key `id`.
|
124
|
+
# #
|
125
|
+
# # This this is compliant to the mapper default, we can omit
|
126
|
+
# # `#identity`.
|
127
|
+
#
|
128
|
+
# mapper = Lotus::Model::Mapper.new do
|
129
|
+
# collection :users do
|
130
|
+
# entity User
|
131
|
+
#
|
132
|
+
# # attribute definitions..
|
133
|
+
# end
|
134
|
+
# end
|
135
|
+
#
|
136
|
+
# @example Custom identity
|
137
|
+
# require 'lotus/model'
|
138
|
+
#
|
139
|
+
# # We have an SQL table `articles` with a primary key `i_id`.
|
140
|
+
# #
|
141
|
+
# # This schema diverges from the expected default: `id`, that's why
|
142
|
+
# # we need to use #identity to let the mapper to recognize the
|
143
|
+
# # primary key.
|
144
|
+
#
|
145
|
+
# mapper = Lotus::Model::Mapper.new do
|
146
|
+
# collection :articles do
|
147
|
+
# entity Article
|
148
|
+
#
|
149
|
+
# # attribute definitions..
|
150
|
+
#
|
151
|
+
# identity :i_id
|
152
|
+
# end
|
153
|
+
# end
|
154
|
+
def identity(name = nil)
|
155
|
+
if name
|
156
|
+
@identity = name
|
157
|
+
else
|
158
|
+
@identity || :id
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Map an attribute.
|
163
|
+
#
|
164
|
+
# An attribute defines a property of an object.
|
165
|
+
# This is storage independent. For instance, it can map an SQL column,
|
166
|
+
# a MongoDB attribute or everything that makes sense for your database.
|
167
|
+
#
|
168
|
+
# Each attribute defines an Ruby type, to coerce that value from the
|
169
|
+
# database. This fixes a huge problem, because database types don't
|
170
|
+
# match Ruby types.
|
171
|
+
# Think of Redis, where everything is stored as a string or integer,
|
172
|
+
# the mapper translates values from/to the database.
|
173
|
+
#
|
174
|
+
# It supports the following types:
|
175
|
+
#
|
176
|
+
# * Array
|
177
|
+
# * Boolean
|
178
|
+
# * Date
|
179
|
+
# * DateTime
|
180
|
+
# * Float
|
181
|
+
# * Hash
|
182
|
+
# * Integer
|
183
|
+
# * Set
|
184
|
+
# * String
|
185
|
+
# * Time
|
186
|
+
#
|
187
|
+
# @param name [Symbol] the name of the attribute, as we want it to be
|
188
|
+
# mapped in the object
|
189
|
+
#
|
190
|
+
# @param klass [Class] the Ruby type that we want to assign as value
|
191
|
+
#
|
192
|
+
# @param options [Hash] a set of options to customize the mapping
|
193
|
+
# @option options [Symbol] :as the name of the original column
|
194
|
+
#
|
195
|
+
# @since 0.1.0
|
196
|
+
#
|
197
|
+
# @example Default schema
|
198
|
+
# require 'lotus/model'
|
199
|
+
#
|
200
|
+
# # Given the following schema:
|
201
|
+
# #
|
202
|
+
# # CREATE TABLE users (
|
203
|
+
# # id integer NOT NULL,
|
204
|
+
# # name varchar(64),
|
205
|
+
# # );
|
206
|
+
# #
|
207
|
+
# # And the following entity:
|
208
|
+
# #
|
209
|
+
# # class User
|
210
|
+
# # include Lotus::Entity
|
211
|
+
# # self.attributes = :name
|
212
|
+
# # end
|
213
|
+
#
|
214
|
+
# mapper = Lotus::Model::Mapper.new do
|
215
|
+
# collection :users do
|
216
|
+
# entity User
|
217
|
+
#
|
218
|
+
# attribute :id, Integer
|
219
|
+
# attribute :name, String
|
220
|
+
# end
|
221
|
+
# end
|
222
|
+
#
|
223
|
+
# # The first argument (`:name`) always corresponds to the `User`
|
224
|
+
# # attribute.
|
225
|
+
#
|
226
|
+
# # The second one (`:klass`) is the Ruby type that we want for our
|
227
|
+
# # attribute.
|
228
|
+
#
|
229
|
+
# # We don't need to use `:as` because the database columns match the
|
230
|
+
# # `User` attributes.
|
231
|
+
#
|
232
|
+
# @example Customized schema
|
233
|
+
# require 'lotus/model'
|
234
|
+
#
|
235
|
+
# # Given the following schema:
|
236
|
+
# #
|
237
|
+
# # CREATE TABLE articles (
|
238
|
+
# # i_id integer NOT NULL,
|
239
|
+
# # i_user_id integer NOT NULL,
|
240
|
+
# # s_title varchar(64),
|
241
|
+
# # comments_count varchar(8) # Not an error: it's for String => Integer coercion
|
242
|
+
# # );
|
243
|
+
# #
|
244
|
+
# # And the following entity:
|
245
|
+
# #
|
246
|
+
# # class Article
|
247
|
+
# # include Lotus::Entity
|
248
|
+
# # self.attributes = :user_id, :title, :comments_count
|
249
|
+
# # end
|
250
|
+
#
|
251
|
+
# mapper = Lotus::Model::Mapper.new do
|
252
|
+
# collection :articles do
|
253
|
+
# entity Article
|
254
|
+
#
|
255
|
+
# attribute :id, Integer, as: :i_id
|
256
|
+
# attribute :user_id, Integer, as: :i_user_id
|
257
|
+
# attribute :title, String, as: :s_title
|
258
|
+
# attribute :comments_count, Integer
|
259
|
+
#
|
260
|
+
# identity :i_id
|
261
|
+
# end
|
262
|
+
# end
|
263
|
+
#
|
264
|
+
# # The first argument (`:name`) always corresponds to the `Article`
|
265
|
+
# # attribute.
|
266
|
+
#
|
267
|
+
# # The second one (`:klass`) is the Ruby type that we want for our
|
268
|
+
# # attribute.
|
269
|
+
#
|
270
|
+
# # The third option (`:as`) is mandatory only when the database
|
271
|
+
# # column doesn't match the name of the mapped attribute.
|
272
|
+
# #
|
273
|
+
# # For instance: we need to use it for translate `:s_title` to
|
274
|
+
# # `:title`, but not for `:comments_count`.
|
275
|
+
def attribute(name, klass, options = {})
|
276
|
+
@attributes[name] = [klass, (options.fetch(:as) { name }).to_sym]
|
277
|
+
end
|
278
|
+
|
279
|
+
# Serializes an entity to be persisted in the database.
|
280
|
+
#
|
281
|
+
# @param entity [Object] an entity
|
282
|
+
#
|
283
|
+
# @api private
|
284
|
+
# @since 0.1.0
|
285
|
+
def serialize(entity)
|
286
|
+
@coercer.to_record(entity)
|
287
|
+
end
|
288
|
+
|
289
|
+
# Deserialize a set of records fetched from the database.
|
290
|
+
#
|
291
|
+
# @param records [Array] a set of raw records
|
292
|
+
#
|
293
|
+
# @api private
|
294
|
+
# @since 0.1.0
|
295
|
+
def deserialize(records)
|
296
|
+
records.map do |record|
|
297
|
+
@coercer.from_record(record)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Deserialize only one attribute from a raw value.
|
302
|
+
#
|
303
|
+
# @param attribute [Symbol] the attribute name
|
304
|
+
# @param value [Object,nil] the value to be coerced
|
305
|
+
#
|
306
|
+
# @api private
|
307
|
+
# @since 0.1.0
|
308
|
+
def deserialize_attribute(attribute, value)
|
309
|
+
@coercer.public_send(:"deserialize_#{ attribute }", value)
|
310
|
+
end
|
311
|
+
|
312
|
+
# Loads the internals of the mapper, in order to guarantee thread safety.
|
313
|
+
#
|
314
|
+
# @api private
|
315
|
+
# @since 0.1.0
|
316
|
+
def load!
|
317
|
+
@coercer = coercer_class.new(self)
|
318
|
+
configure_repository!
|
319
|
+
end
|
320
|
+
|
321
|
+
private
|
322
|
+
# Assigns a repository to an entity
|
323
|
+
#
|
324
|
+
# @see Lotus::Repository
|
325
|
+
#
|
326
|
+
# @api private
|
327
|
+
# @since 0.1.0
|
328
|
+
def configure_repository!
|
329
|
+
repository = Object.const_get("#{ entity.name }#{ REPOSITORY_SUFFIX }")
|
330
|
+
repository.collection = name
|
331
|
+
rescue NameError
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|