preserves 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 +7 -0
- data/.gitignore +23 -0
- data/.irbrc +9 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +178 -0
- data/Rakefile +2 -0
- data/SETUP.sh +15 -0
- data/TODO.md +122 -0
- data/lib/preserves.rb +24 -0
- data/lib/preserves/mapper.rb +108 -0
- data/lib/preserves/mapper/belongs_to.rb +25 -0
- data/lib/preserves/mapper/has_many.rb +25 -0
- data/lib/preserves/mapper/relation.rb +25 -0
- data/lib/preserves/mapping.rb +73 -0
- data/lib/preserves/repository.rb +69 -0
- data/lib/preserves/selection.rb +65 -0
- data/lib/preserves/sql.rb +12 -0
- data/lib/preserves/sql/result_set.rb +21 -0
- data/lib/preserves/version.rb +3 -0
- data/preserves.gemspec +27 -0
- data/spec/repository_spec.rb +260 -0
- data/spec/spec_helper.rb +120 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f8431d69f3de10e3ee28a0e6e2c96b86afadfeee
|
4
|
+
data.tar.gz: 6c87236337e01dbb1c5e95a95920fc82ef8f7f18
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3e693759454aea1c88303fd6ed6b5e916991ad1491b3217ee9f1558b1c99b094c1be2eed26db95e26ab25cf82d7734829fa3292b4f531626071b1a5c5930aac7
|
7
|
+
data.tar.gz: 25d5b244aebae726bc06607b192c094672e5b96199501a9bb6f915d0bb21c2de5aa27d226f1efb9dbace3445f4fe78ad8ba86060ea2babecc384f80d2f87ef43
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
vendor/bundle
|
data/.irbrc
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# NOTE: For this file to work, your ~/.irbrc file must contain snippet from http://www.samuelmullen.com/2010/04/irb-global-local-irbrc/.
|
2
|
+
|
3
|
+
|
4
|
+
# Allow reloading our gem.
|
5
|
+
def reload!
|
6
|
+
@gem_name = Dir["#{Dir.pwd}/*.gemspec"].first.split('/').last.sub('.gemspec', '')
|
7
|
+
files = $LOADED_FEATURES.select { |feat| feat =~ %r[/#{@gem_name}/] }
|
8
|
+
files.each { |file| load file }
|
9
|
+
end
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 BoochTek, LLC
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
Preserves
|
2
|
+
=========
|
3
|
+
|
4
|
+
Preserves is a minimalist ORM (object-relational mapper) for Ruby, using the
|
5
|
+
Repository and Data Mapper patterns.
|
6
|
+
|
7
|
+
We're trying to answer these questions:
|
8
|
+
|
9
|
+
* How simple can we make an ORM that is still useful?
|
10
|
+
* Developers have to know SQL anyway, so why try to hide the SQL from them?
|
11
|
+
* Is the complexity of a typical ORM really better than the complexity of SQL?
|
12
|
+
* ORMs are a leaky abstraction. What if we made it so leaky that it doesn't matter?
|
13
|
+
|
14
|
+
This ORM is based on a few strong opinions:
|
15
|
+
|
16
|
+
* The Data Mapper pattern is generally better than the Active Record pattern.
|
17
|
+
* Unless you're just writing a CRUD front-end, with little interesting behavior.
|
18
|
+
* Declaring attributes in the domain model is better than hiding them elsewhere.
|
19
|
+
* Declaring relationships in one place and attributes in another is true madness.
|
20
|
+
* NoSQL as a main data store is usually misguided.
|
21
|
+
* PostgreSQL can do just about anything you need, using SQL.
|
22
|
+
* Projects are unlikely to need to abstract SQL to allow them to use different RDBMSes.
|
23
|
+
* Developer workstations are fast enough to run "full" RDBMSes.
|
24
|
+
* If you're not using "interesting" features, then you're probably using "standard" SQL.
|
25
|
+
|
26
|
+
The Data Mapper pattern provides several advantages:
|
27
|
+
|
28
|
+
* Domain objects don't have to know anything about the database or its schema.
|
29
|
+
* Instead, the mapper knows about the domain objects and the database.
|
30
|
+
* DB schema can change without having to change to domain objects; only the mapper changes.
|
31
|
+
* The domain objects are self-contained.
|
32
|
+
* Don't have to look elsewhere to understand everything a class contains.
|
33
|
+
* Better meets the Single Responsibility Principle (SRP).
|
34
|
+
* Domain model classes handle business logic.
|
35
|
+
* Repository classes handle persistence.
|
36
|
+
* Mapper classes handle mapping database fields to object attributes.
|
37
|
+
|
38
|
+
It's been pointed out that Preserves might not in fact even be an ORM, because it doesn't have a complete model of the relations between objects.
|
39
|
+
|
40
|
+
|
41
|
+
Installation
|
42
|
+
------------
|
43
|
+
|
44
|
+
Add this line to your application's Gemfile:
|
45
|
+
|
46
|
+
gem 'preserves'
|
47
|
+
|
48
|
+
And then execute:
|
49
|
+
|
50
|
+
$ bundle
|
51
|
+
|
52
|
+
Or install it yourself as:
|
53
|
+
|
54
|
+
$ gem install preserves
|
55
|
+
|
56
|
+
|
57
|
+
Example Usage
|
58
|
+
-------------
|
59
|
+
|
60
|
+
First, create your domain model class. You can use a [Struct], an
|
61
|
+
[OpenStruct], a [Virtus] model, or a plain old Ruby object (PORO) class.
|
62
|
+
We'll use a Struct in the examples, so we can initialize the fields easily.
|
63
|
+
|
64
|
+
~~~ ruby
|
65
|
+
User = Struct.new(:id, :name, :age) do
|
66
|
+
end
|
67
|
+
~~~
|
68
|
+
|
69
|
+
Next, configure the Preserves data store.
|
70
|
+
|
71
|
+
~~~ ruby
|
72
|
+
Preserves.data_store = Preserves::PostgreSQL("my_database")
|
73
|
+
~~~
|
74
|
+
|
75
|
+
Then create a repository linked to the domain model class.
|
76
|
+
By default, all attributes will be assumed to be Strings.
|
77
|
+
For other attribute types, you'll need to supply the mapping.
|
78
|
+
(We'll have some default mappings determined from the DB or model later.)
|
79
|
+
Your repository should then define methods to access model objects
|
80
|
+
in the database. (These will mostly be like ActiveRecord scopes.)
|
81
|
+
|
82
|
+
~~~ ruby
|
83
|
+
UserRepository = Preserves.repository(model: User) do
|
84
|
+
mapping do
|
85
|
+
map id: 'username' # The database field named 'username' corresponds to the 'id' attribute in the model.
|
86
|
+
map :age, Integer # The 'age' field should be mapped to an Integer in the model.
|
87
|
+
end
|
88
|
+
|
89
|
+
# We'll likely provide `insert`, but this gives an idea of how minimal we'll be to start off.
|
90
|
+
def insert(user)
|
91
|
+
result = query("INSERT INTO 'users' (username, name, age) VALUES ($1, $2, $3)",
|
92
|
+
user.id, user.name, user.age)
|
93
|
+
raise "Could not insert User #{user.id} into database" unless result.size == 1
|
94
|
+
end
|
95
|
+
|
96
|
+
def older_than(age)
|
97
|
+
select("SELECT *, username AS id FROM 'users' WHERE age > $1 ORDER BY $2", age, :name)
|
98
|
+
end
|
99
|
+
|
100
|
+
def with_id(id)
|
101
|
+
select("SELECT *, username AS id FROM 'users' WHERE username = $1", id)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
~~~
|
105
|
+
|
106
|
+
Now we can create model objects and use the repository to save them to and
|
107
|
+
retrieve them from the database:
|
108
|
+
|
109
|
+
~~~ ruby
|
110
|
+
craig = User.new("booch", "Craig", 42)
|
111
|
+
UserRepository.insert(craig)
|
112
|
+
users_over_40 = UserRepository.older_than(40) # Returns an Enumerable set of User objects.
|
113
|
+
beth = UserRepository.with_id("beth").one # Returns a single User object or nil.
|
114
|
+
~~~
|
115
|
+
|
116
|
+
|
117
|
+
API Summary
|
118
|
+
-----------
|
119
|
+
|
120
|
+
NOTE: This project is in very early exploratory stages. The API **will** change.
|
121
|
+
|
122
|
+
|
123
|
+
### Repository ###
|
124
|
+
|
125
|
+
Most of the API you'll use will be in the your repository object.
|
126
|
+
The mixin provides the following methods:
|
127
|
+
|
128
|
+
~~~ ruby
|
129
|
+
fetch(id) # Fetch a single domain model object by its primary key.
|
130
|
+
[id] # Fetch a single domain model object by its primary key.
|
131
|
+
query(sql_string) # Runs SQL and returns a Preserves::SQL::ResultSet.
|
132
|
+
select(sql_string) # Runs SQL and returns a Preserves::Selection.
|
133
|
+
select(sql_string, param1, param2) # Include bind params for the SQL query.
|
134
|
+
select(sql_string, association_name: sql_result) # Include associations.
|
135
|
+
~~~
|
136
|
+
|
137
|
+
|
138
|
+
### Preserves::SQL::ResultSet ###
|
139
|
+
|
140
|
+
~~~ ruby
|
141
|
+
result.size # Number of rows that were affected by the SQL query.
|
142
|
+
~~~
|
143
|
+
|
144
|
+
|
145
|
+
### Preserves::Selection ###
|
146
|
+
|
147
|
+
A Selection is an Enumerable, representing the results of a SELECT query,
|
148
|
+
mapped to domain model objects.
|
149
|
+
Most of your interactions with Selections will be through the Enumerable interface.
|
150
|
+
|
151
|
+
~~~ ruby
|
152
|
+
selection.each # Iterates through the resulting domain objects.
|
153
|
+
selection.first # Returns the first result. Returns nil if there are no results.
|
154
|
+
selection.first! # Returns the first result. Raises an exception if there are no results.
|
155
|
+
selection.last # Returns the last result. Returns nil if there are no results.
|
156
|
+
selection.last! # Returns the last result. Raises an exception if there are no results.
|
157
|
+
selection.only # Returns the only result. Returns nil if there are no results. Raises an exception if there's more than 1 result. (Aliased as `one`.)
|
158
|
+
selection.only! # Returns the only result. Raises an exception if there's not exactly 1 result. (Aliased as `one!`.)
|
159
|
+
~~~
|
160
|
+
|
161
|
+
|
162
|
+
Contributing
|
163
|
+
------------
|
164
|
+
|
165
|
+
1. Fork the [project repo].
|
166
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
167
|
+
3. Make sure tests pass (`rspec` or `rake spec`).
|
168
|
+
4. Commit your changes (`git commit -am 'Add some feature'`).
|
169
|
+
5. Push to the branch (`git push origin my-new-feature`).
|
170
|
+
6. Create a new [pull request].
|
171
|
+
|
172
|
+
|
173
|
+
[Struct]: http://ruby-doc.org/core-2.2.0/Struct.html
|
174
|
+
[OpenStruct]: http://ruby-doc.org/stdlib-2.2.0/libdoc/ostruct/rdoc/OpenStruct.html
|
175
|
+
[Virtus]: https://github.com/solnic/virtus#readme
|
176
|
+
|
177
|
+
[project repo]: https://github.com/boochtek/ruby_preserves/fork
|
178
|
+
[pull request]: https://github.com/boochtek/ruby_preserves/pulls
|
data/Rakefile
ADDED
data/SETUP.sh
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
case "$(uname)" in
|
4
|
+
Darwin)
|
5
|
+
brew install postgresql
|
6
|
+
ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents
|
7
|
+
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
|
8
|
+
bundle --path vendor/bundle
|
9
|
+
createdb 'preserves_test'
|
10
|
+
;;
|
11
|
+
*)
|
12
|
+
echo "Don't know how to install on this system. Please submit a pull request."
|
13
|
+
exit
|
14
|
+
;;
|
15
|
+
esac
|
data/TODO.md
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
TODO
|
2
|
+
====
|
3
|
+
|
4
|
+
|
5
|
+
Presentation
|
6
|
+
------------
|
7
|
+
|
8
|
+
* Create the short URL. (NO UNDERSCORES on TinyURL)
|
9
|
+
* http://craigbuchek.com/ruby-preserves-rubyconf
|
10
|
+
* http://tinyurl.com/ruby-preserves-rubyconf
|
11
|
+
* https://rawgit.com/booch/presentations/Ruby_Preserves-RubyConf-2015-11-15/Ruby_Preserves/slides.html
|
12
|
+
|
13
|
+
|
14
|
+
ASAP
|
15
|
+
----
|
16
|
+
|
17
|
+
* README: Document has_many and belongs_to.
|
18
|
+
* Advise to avoid belongs_to mappings, if possible.
|
19
|
+
* Especially don't want circular dependencies.
|
20
|
+
* README: Show an example of pagination.
|
21
|
+
* README: Show how to use a different repository for tests, if necessary.
|
22
|
+
* Since we require SQL, we can't really do in-memory.
|
23
|
+
* So the repository would not be a Preserves repository.
|
24
|
+
* Saving.
|
25
|
+
* Not sure if we should just have subclasses use query().
|
26
|
+
* I'm starting to lean that way.
|
27
|
+
* We probably have all the info we need to build the INSERT programmatically.
|
28
|
+
* But that would violate our "just use SQL everywhere" mantra.
|
29
|
+
* insert / update / save / delete
|
30
|
+
* Preserves.repository() should return a module to mix in, and not take a block.
|
31
|
+
* And should not be singletons.
|
32
|
+
* Might use method_missing on class to allow usage as if it's a singleton.
|
33
|
+
* Use the Module Factory pattern.
|
34
|
+
|
35
|
+
|
36
|
+
Soonish
|
37
|
+
-------
|
38
|
+
|
39
|
+
* Convenience methods.
|
40
|
+
* create_table
|
41
|
+
* scope
|
42
|
+
* More coercions.
|
43
|
+
* Boolean
|
44
|
+
* Date
|
45
|
+
* Rename to serialize/deserialize.
|
46
|
+
* Move serializers to their own class(es).
|
47
|
+
* Or should we use solnic/coercible gem?
|
48
|
+
* Allow a way to specify more type mappings/serializers.
|
49
|
+
* Registration?
|
50
|
+
* Get default mappings from DB schema.
|
51
|
+
* INTEGER
|
52
|
+
* DATE
|
53
|
+
* TIME
|
54
|
+
* Prepared statements.
|
55
|
+
* Can we just prepare every SQL query we run?
|
56
|
+
* Have a cache mapping the SQL query string to the prepared statement.
|
57
|
+
* Would obviously want to make this a LRU cache eventually.
|
58
|
+
* Ensure we can use PostgreSQL arrays.
|
59
|
+
* Be consistent between strings and symbols.
|
60
|
+
* Can we initialize the domain model objects, instead of using setters?
|
61
|
+
* Would initialize with a Hash of attributes and values.
|
62
|
+
* Might allow both variants, to work with different kinds of classes.
|
63
|
+
* Would need a way to know if the model class supports the initializer we'd be using.
|
64
|
+
* Allow strings in place of class names for specifying repositories.
|
65
|
+
* Because we'll have circular references for belongs_to/has_many pairs.
|
66
|
+
* Or should we not allow that, because it's bad for OOP to have circular dependencies?
|
67
|
+
* Better exceptions -- add some Exception classes.
|
68
|
+
* Have Selection class lazily do mapping, instead of eagerly in the repository?
|
69
|
+
* Unit tests.
|
70
|
+
* We currently only have integration/acceptance tests.
|
71
|
+
* Pluralize.
|
72
|
+
* Will have to move it to its own file and make it public.
|
73
|
+
* Add has_many :through relations.
|
74
|
+
* Might already work with the existing code, and just need testing.
|
75
|
+
* Allow, but don't require, join table to have an associated Repository object.
|
76
|
+
* Use ActiveRecord syntax, but store them separately in Mapper.
|
77
|
+
* Should we catch exceptions from the DB?
|
78
|
+
* Should we reraise them with our own exception class?
|
79
|
+
* Should we swallow them?
|
80
|
+
* Cleanup.
|
81
|
+
* Clean up Mapper a bit more.
|
82
|
+
* Setting up the DB in spec_helper is terrible.
|
83
|
+
* At least move it to a separate file.
|
84
|
+
* Better documentation.
|
85
|
+
* README isn't great at explaining how to use it.
|
86
|
+
* Should make recommendations on how to use this.
|
87
|
+
* In Rails, recommend putting repositories in app/repositories/.
|
88
|
+
* Add that to LOAD_PATH, but don't auto-load.
|
89
|
+
* Repository file should require domain model file, but never vice-versa.
|
90
|
+
* Recommend they consider using 'Users' instead of 'UserRepository'.
|
91
|
+
* Handle Virtus models.
|
92
|
+
* Get list of default mappings from model attributes list.
|
93
|
+
* New up the object with all attributes, instead of setting them individually.
|
94
|
+
* Will probably make this a separate gem.
|
95
|
+
* Can layer on top, or inject extra strategies for object creation and default mappings.
|
96
|
+
* Identity map.
|
97
|
+
* For cases where we're creating a bunch of objects, but some already exist.
|
98
|
+
* Allow a way to specify that a model is a value type (which doesn't have an identity).
|
99
|
+
* Does it make sense to have these in the database?
|
100
|
+
* Test for mapping both type and name.
|
101
|
+
* Probably already works.
|
102
|
+
* Fit into ActiveModel.
|
103
|
+
* Would require picking one base class for models.
|
104
|
+
* Would lose the ability to use POROs (at least when using ActiveModel).
|
105
|
+
* Would require mutual dependencies.
|
106
|
+
* The model will have to call to the repo to persist itself.
|
107
|
+
* The repo will need to know about the model.
|
108
|
+
* Maybe there's a way to actually break this, since our mapping doesn't need it immediately.
|
109
|
+
* Would require also including a validation layer (I think).
|
110
|
+
* Connection pooling.
|
111
|
+
* Is there a way we could do dirty tracking/updating?
|
112
|
+
* This would require some help from the model.
|
113
|
+
* So we'd probably make it optional, depending on whether the model supports it.
|
114
|
+
* Is there a way we could do optimistic locking?
|
115
|
+
* Is there a way we could do lazy loading of associations?
|
116
|
+
* Transactions / Unit of Work.
|
117
|
+
* Composite keys.
|
118
|
+
* Use Mutant for testing.
|
119
|
+
* Use a CI service.
|
120
|
+
* Use Ruby 2.1 keyword arguments.
|
121
|
+
* Use cursors.
|
122
|
+
* Performance testing.
|
data/lib/preserves.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "preserves/version"
|
2
|
+
require "preserves/repository"
|
3
|
+
require "preserves/sql"
|
4
|
+
|
5
|
+
|
6
|
+
module Preserves
|
7
|
+
def self.repository(options={}, &block)
|
8
|
+
repository = Repository.new(options)
|
9
|
+
repository.instance_eval(&block)
|
10
|
+
repository
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.data_store=(connection)
|
14
|
+
@data_store = connection
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.data_store
|
18
|
+
@data_store or raise "You must define a default data store"
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.PostgreSQL(db_name)
|
22
|
+
SQL.connection(dbname: db_name)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# Terminology note: A field is associated with a single row/record. A column pertains to all rows/records.
|
2
|
+
|
3
|
+
require "preserves/mapping"
|
4
|
+
require "preserves/mapper/has_many"
|
5
|
+
require "preserves/mapper/belongs_to"
|
6
|
+
|
7
|
+
|
8
|
+
module Preserves
|
9
|
+
class Mapper
|
10
|
+
|
11
|
+
attr_accessor :mapping
|
12
|
+
|
13
|
+
def initialize(mapping)
|
14
|
+
self.mapping = mapping
|
15
|
+
end
|
16
|
+
|
17
|
+
def map(result, relations={})
|
18
|
+
result.map do |record|
|
19
|
+
map_one(record, relations)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def map_one(record, relations={})
|
24
|
+
mapping.model_class.new.tap do |object|
|
25
|
+
map_attributes(object, record)
|
26
|
+
map_relations(object, record, relations)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def primary_key_attribute
|
33
|
+
column_name_to_attribute_name(mapping.primary_key)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def map_attributes(object, record)
|
39
|
+
record.each_pair do |column_name, field_value|
|
40
|
+
attribute_name = column_name_to_attribute_name(column_name)
|
41
|
+
if object.respond_to?("#{attribute_name}=")
|
42
|
+
object.send("#{attribute_name}=", field_value_to_attribute_value(attribute_name, field_value))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def column_name_to_attribute_name(column_name)
|
48
|
+
mapping.name_mappings.fetch(column_name) { column_name }
|
49
|
+
end
|
50
|
+
|
51
|
+
def field_value_to_attribute_value(attribute_name, field_value)
|
52
|
+
attribute_type = attribute_name_to_attribute_type(attribute_name)
|
53
|
+
coerce(field_value, to: attribute_type)
|
54
|
+
end
|
55
|
+
|
56
|
+
def attribute_value_to_field_value(attribute_name, attribute_value)
|
57
|
+
attribute_type = attribute_name_to_attribute_type(attribute_name)
|
58
|
+
uncoerce(attribute_value, to: attribute_type)
|
59
|
+
end
|
60
|
+
|
61
|
+
def attribute_name_to_attribute_type(attribute_name)
|
62
|
+
mapping.type_mappings.fetch(attribute_name.to_sym) { String }
|
63
|
+
end
|
64
|
+
|
65
|
+
def map_relations(object, record, relations)
|
66
|
+
has_many_relations = relations.select{ |k, _v| mapping.has_many_mappings.keys.include?(k) }
|
67
|
+
map_has_many_relations(object, record, has_many_relations)
|
68
|
+
belongs_to_relations = relations.select{ |k, _v| mapping.belongs_to_mappings.keys.include?(k) }
|
69
|
+
map_belongs_to_relations(object, record, belongs_to_relations)
|
70
|
+
# TODO: Raise an exception if any of the relations weren't found in any of the relation mappings.
|
71
|
+
end
|
72
|
+
|
73
|
+
def map_has_many_relations(object, record, relations)
|
74
|
+
# TODO: Ensure that there's a setter for every relation_name before we iterate through the relations.
|
75
|
+
relations.each do |relation_name, relation_result_set|
|
76
|
+
Mapper::HasMany.new(object, record, relation_name, relation_result_set, mapping).map!
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def map_belongs_to_relations(object, record, relations)
|
81
|
+
# TODO: Ensure that there's a setter for every relation_name before we iterate through the relations.
|
82
|
+
relations.each do |relation_name, relation_result_set|
|
83
|
+
Mapper::BelongsTo.new(object, record, relation_name, relation_result_set, mapping).map!
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def coerce(field_value, options={})
|
88
|
+
return nil if field_value.nil?
|
89
|
+
|
90
|
+
if options[:to] == Integer
|
91
|
+
Integer(field_value)
|
92
|
+
else
|
93
|
+
field_value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def uncoerce(attribute_value, options={})
|
98
|
+
return "NULL" if attribute_value.nil?
|
99
|
+
|
100
|
+
if options[:to] == String
|
101
|
+
"'#{attribute_value}'"
|
102
|
+
else
|
103
|
+
attribute_value.to_s
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|