easy_mapper 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 +14 -0
- data/.rspec +2 -0
- data/.rubocop.yaml +141 -0
- data/.travis.yml +5 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/postgre_scripts.sql +31 -0
- data/bin/setup +8 -0
- data/bin/sqlite3_scripts.sql +6 -0
- data/easy_mapper.gemspec +32 -0
- data/lib/easy_mapper.rb +5 -0
- data/lib/easy_mapper/adapters/postgre_adapter.rb +57 -0
- data/lib/easy_mapper/adapters/results/postgre_result.rb +29 -0
- data/lib/easy_mapper/adapters/results/sqlite_result.rb +33 -0
- data/lib/easy_mapper/adapters/sqlite_adapter.rb +46 -0
- data/lib/easy_mapper/associations/belongs_to.rb +13 -0
- data/lib/easy_mapper/associations/has_many.rb +13 -0
- data/lib/easy_mapper/associations/has_one.rb +15 -0
- data/lib/easy_mapper/config.rb +9 -0
- data/lib/easy_mapper/db_repository.rb +94 -0
- data/lib/easy_mapper/errors.rb +24 -0
- data/lib/easy_mapper/logger.rb +36 -0
- data/lib/easy_mapper/model.rb +98 -0
- data/lib/easy_mapper/model/class_macros.rb +80 -0
- data/lib/easy_mapper/model/query_methods.rb +9 -0
- data/lib/easy_mapper/query.rb +131 -0
- data/lib/easy_mapper/version.rb +3 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 34b66ef0506845311fae98997074beeacab3827f
|
4
|
+
data.tar.gz: 322c5141727f2a6109feaa494400f01f01a9e650
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 49de05fc016b174b053522bd89313c92532a48b67093be464746fa1eeb15ca99cff8a303e851c30dde315f341ab4aef0491311c690b26aa79c6fd380002c4a59
|
7
|
+
data.tar.gz: ec5adb7104e7123d62034febda1b184f091133e8083a317bc88eda1331ee8b9a88510410862762d3a979d9b44fed5636733dd91cedf3ce87ddbc847d2cde4927
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yaml
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
AllCops:
|
2
|
+
DisabledByDefault: true
|
3
|
+
Exclude:
|
4
|
+
- 'spec/**'
|
5
|
+
|
6
|
+
Rails:
|
7
|
+
enabled: false
|
8
|
+
|
9
|
+
# Commonly used screens these days easily fit more than 80 characters.
|
10
|
+
Metrics/LineLength:
|
11
|
+
Max: 120
|
12
|
+
|
13
|
+
# Too short methods lead to extraction of single-use methods, which can make
|
14
|
+
# the code easier to read (by naming things), but can also clutter the class
|
15
|
+
Metrics/MethodLength:
|
16
|
+
Max: 20
|
17
|
+
|
18
|
+
# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
|
19
|
+
Metrics/ClassLength:
|
20
|
+
Max: 1500
|
21
|
+
|
22
|
+
# No space makes the method definition shorter and differentiates
|
23
|
+
# from a regular assignment.
|
24
|
+
Layout/SpaceAroundEqualsInParameterDefault:
|
25
|
+
EnforcedStyle: no_space
|
26
|
+
|
27
|
+
# Single quotes being faster is hardly measurable and only affects parse time.
|
28
|
+
# Enforcing double quotes reduces the times where you need to change them
|
29
|
+
# when introducing an interpolation. Use single quotes only if their semantics
|
30
|
+
# are needed.
|
31
|
+
Style/StringLiterals:
|
32
|
+
EnforcedStyle: double_quotes
|
33
|
+
|
34
|
+
# We do not need to support Ruby 1.9, so this is good to use.
|
35
|
+
Style/SymbolArray:
|
36
|
+
Enabled: true
|
37
|
+
|
38
|
+
# Most readable form.
|
39
|
+
Layout/AlignHash:
|
40
|
+
EnforcedHashRocketStyle: table
|
41
|
+
EnforcedColonStyle: table
|
42
|
+
|
43
|
+
# Mixing the styles looks just silly.
|
44
|
+
Style/HashSyntax:
|
45
|
+
EnforcedStyle: ruby19_no_mixed_keys
|
46
|
+
|
47
|
+
# has_key? and has_value? are far more readable than key? and value?
|
48
|
+
Style/PreferredHashMethods:
|
49
|
+
Enabled: false
|
50
|
+
|
51
|
+
# String#% is by far the least verbose and only object oriented variant.
|
52
|
+
Style/FormatString:
|
53
|
+
EnforcedStyle: percent
|
54
|
+
|
55
|
+
Style/CollectionMethods:
|
56
|
+
Enabled: true
|
57
|
+
PreferredMethods:
|
58
|
+
# inject seems more common in the community.
|
59
|
+
reduce: "inject"
|
60
|
+
|
61
|
+
|
62
|
+
# Either allow this style or don't. Marking it as safe with parenthesis
|
63
|
+
# is silly. Let's try to live without them for now.
|
64
|
+
Style/ParenthesesAroundCondition:
|
65
|
+
AllowSafeAssignment: false
|
66
|
+
Lint/AssignmentInCondition:
|
67
|
+
AllowSafeAssignment: false
|
68
|
+
|
69
|
+
# A specialized exception class will take one or more arguments and construct the message from it.
|
70
|
+
# So both variants make sense.
|
71
|
+
Style/RaiseArgs:
|
72
|
+
Enabled: false
|
73
|
+
|
74
|
+
# Indenting the chained dots beneath each other is not supported by this cop,
|
75
|
+
# see https://github.com/bbatsov/rubocop/issues/1633
|
76
|
+
Layout/MultilineOperationIndentation:
|
77
|
+
Enabled: false
|
78
|
+
|
79
|
+
# Fail is an alias of raise. Avoid aliases, it's more cognitive load for no gain.
|
80
|
+
# The argument that fail should be used to abort the program is wrong too,
|
81
|
+
# there's Kernel#abort for that.
|
82
|
+
Style/SignalException:
|
83
|
+
EnforcedStyle: only_raise
|
84
|
+
|
85
|
+
# Suppressing exceptions can be perfectly fine, and be it to avoid to
|
86
|
+
# explicitly type nil into the rescue since that's what you want to return,
|
87
|
+
# or suppressing LoadError for optional dependencies
|
88
|
+
Lint/HandleExceptions:
|
89
|
+
Enabled: false
|
90
|
+
|
91
|
+
Layout/SpaceInsideBlockBraces:
|
92
|
+
# The space here provides no real gain in readability while consuming
|
93
|
+
# horizontal space that could be used for a better parameter name.
|
94
|
+
# Also {| differentiates better from a hash than { | does.
|
95
|
+
SpaceBeforeBlockParameters: false
|
96
|
+
|
97
|
+
# No trailing space differentiates better from the block:
|
98
|
+
# foo} means hash, foo } means block.
|
99
|
+
Layout/SpaceInsideHashLiteralBraces:
|
100
|
+
EnforcedStyle: no_space
|
101
|
+
|
102
|
+
# { ... } for multi-line blocks is okay, follow Weirichs rule instead:
|
103
|
+
# https://web.archive.org/web/20140221124509/http://onestepback.org/index.cgi/Tech/Ruby/BraceVsDoEnd.rdoc
|
104
|
+
Style/BlockDelimiters:
|
105
|
+
Enabled: false
|
106
|
+
|
107
|
+
# do / end blocks should be used for side effects,
|
108
|
+
# methods that run a block for side effects and have
|
109
|
+
# a useful return value are rare, assign the return
|
110
|
+
# value to a local variable for those cases.
|
111
|
+
Style/MethodCalledOnDoEndBlock:
|
112
|
+
Enabled: true
|
113
|
+
|
114
|
+
# Enforcing the names of variables? To single letter ones? Just no.
|
115
|
+
Style/SingleLineBlockParams:
|
116
|
+
Enabled: false
|
117
|
+
|
118
|
+
# Shadowing outer local variables with block parameters is often useful
|
119
|
+
# to not reinvent a new name for the same thing, it highlights the relation
|
120
|
+
# between the outer variable and the parameter. The cases where it's actually
|
121
|
+
# confusing are rare, and usually bad for other reasons already, for example
|
122
|
+
# because the method is too long.
|
123
|
+
Lint/ShadowingOuterLocalVariable:
|
124
|
+
Enabled: false
|
125
|
+
|
126
|
+
# Check with yard instead.
|
127
|
+
Style/Documentation:
|
128
|
+
Enabled: false
|
129
|
+
|
130
|
+
# This is just silly. Calling the argument `other` in all cases makes no sense.
|
131
|
+
Style/OpMethod:
|
132
|
+
Enabled: false
|
133
|
+
|
134
|
+
# There are valid cases, for example debugging Cucumber steps,
|
135
|
+
# also they'll fail CI anyway
|
136
|
+
Lint/Debugger:
|
137
|
+
Enabled: false
|
138
|
+
|
139
|
+
# Style preference
|
140
|
+
Style/MethodDefParentheses:
|
141
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Viktor Marinov
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# EasyMapper
|
2
|
+
|
3
|
+
Simple ORM library for ruby. This project is part of a ruby course in Faculty of Mathematics and Informatics in Sofia University.
|
4
|
+
|
5
|
+
[ORM Library Project Task](https://github.com/fmi/ruby-course-projects/blob/master/sample_projects/orm.md)
|
6
|
+
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'easy_mapper'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install easy_mapper
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
30
|
+
|
31
|
+
To run the tests you need to install `postgresql` and create a test database. Open `bin/postgre_scripts.sql` to find the queries to create the needed tables for the tests. Open `east_mapper_spec.rb` and enter the configuration for your database. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
32
|
+
|
33
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ViktorMarinov/easy_mapper.
|
38
|
+
|
39
|
+
## License
|
40
|
+
|
41
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'easy_mapper'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start(__FILE__)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
create table users (
|
2
|
+
id int primary key,
|
3
|
+
first_name varchar(30),
|
4
|
+
last_name varchar(30),
|
5
|
+
age int
|
6
|
+
);
|
7
|
+
|
8
|
+
create table address (
|
9
|
+
id int primary key,
|
10
|
+
city varchar(30),
|
11
|
+
street varchar(30)
|
12
|
+
);
|
13
|
+
|
14
|
+
create table employee (
|
15
|
+
id int primary key,
|
16
|
+
name varchar(30),
|
17
|
+
address_id int references address(id)
|
18
|
+
);
|
19
|
+
|
20
|
+
create table phone_book (
|
21
|
+
id int primary key,
|
22
|
+
country varchar(30)
|
23
|
+
);
|
24
|
+
|
25
|
+
create table phone_entry (
|
26
|
+
id int primary key,
|
27
|
+
name varchar(30),
|
28
|
+
phone varchar(15),
|
29
|
+
phone_book_id int references phone_book(id)
|
30
|
+
);
|
31
|
+
|
data/bin/setup
ADDED
data/easy_mapper.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'easy_mapper/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'easy_mapper'
|
9
|
+
spec.version = EasyMapper::VERSION
|
10
|
+
spec.authors = ['Viktor Marinov']
|
11
|
+
spec.email = ['viktor.marinov95@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = 'Simple ORM library for Ruby'
|
14
|
+
spec.description = 'Simple ORM library for Ruby'
|
15
|
+
spec.homepage = 'https://github.com/ViktorMarinov/easy_mapper'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.bindir = 'exe'
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.15'
|
26
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
28
|
+
|
29
|
+
spec.add_runtime_dependency 'sqlbuilder', '~> 0.1'
|
30
|
+
spec.add_runtime_dependency 'pg', '~> 0.19.0'
|
31
|
+
spec.add_runtime_dependency 'sqlite3', '~> 1.3.13'
|
32
|
+
end
|
data/lib/easy_mapper.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'pg'
|
2
|
+
require 'sqlbuilder'
|
3
|
+
|
4
|
+
require_relative 'results/postgre_result'
|
5
|
+
require_relative '../logger'
|
6
|
+
|
7
|
+
module EasyMapper
|
8
|
+
module Adapters
|
9
|
+
class PostgreAdapter
|
10
|
+
def initialize(
|
11
|
+
host: '127.0.0.1',
|
12
|
+
port: 5432,
|
13
|
+
database:,
|
14
|
+
user:,
|
15
|
+
password:
|
16
|
+
)
|
17
|
+
|
18
|
+
@connection_options = {
|
19
|
+
host: host,
|
20
|
+
port: port,
|
21
|
+
dbname: database,
|
22
|
+
user: user,
|
23
|
+
password: password
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def connect
|
28
|
+
@connection = PGconn.connect(@connection_options)
|
29
|
+
|
30
|
+
@connection.set_notice_processor do |warning|
|
31
|
+
Logger.logger.warn(warning)
|
32
|
+
end
|
33
|
+
|
34
|
+
@connection.type_map_for_results =
|
35
|
+
PG::BasicTypeMapForResults.new(@connection)
|
36
|
+
end
|
37
|
+
|
38
|
+
def execute(query)
|
39
|
+
Logger.logger.info("Executing query: #{query}")
|
40
|
+
Results::PostgreResult.new @connection.exec(query)
|
41
|
+
end
|
42
|
+
|
43
|
+
def sql_builder
|
44
|
+
Sqlbuilder::Builders::PostgresBuilder.new
|
45
|
+
end
|
46
|
+
|
47
|
+
def next_id(table_name)
|
48
|
+
seq_name = "#{table_name}_id_seq"
|
49
|
+
|
50
|
+
execute(sql_builder.sequence(seq_name).create_unless_exists)
|
51
|
+
|
52
|
+
query = sql_builder.sequence(seq_name).next_val
|
53
|
+
execute(query).single_value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module EasyMapper
|
2
|
+
module Adapters
|
3
|
+
module Results
|
4
|
+
class PostgreResult
|
5
|
+
def initialize(result)
|
6
|
+
@result = result
|
7
|
+
end
|
8
|
+
|
9
|
+
def single_hash
|
10
|
+
list.first
|
11
|
+
end
|
12
|
+
|
13
|
+
def single_value
|
14
|
+
@result.values.first.first
|
15
|
+
end
|
16
|
+
|
17
|
+
def values
|
18
|
+
@result.values
|
19
|
+
end
|
20
|
+
|
21
|
+
def list
|
22
|
+
@result.map do |row|
|
23
|
+
row.map { |key, value| [key.to_sym, value] }.to_h
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module EasyMapper
|
2
|
+
module Adapters
|
3
|
+
module Results
|
4
|
+
class SqliteResult
|
5
|
+
def initialize(result)
|
6
|
+
@result = result
|
7
|
+
end
|
8
|
+
|
9
|
+
def single_hash
|
10
|
+
list.first
|
11
|
+
end
|
12
|
+
|
13
|
+
def single_value
|
14
|
+
values.first.first
|
15
|
+
end
|
16
|
+
|
17
|
+
def values
|
18
|
+
@result.map(&:values)
|
19
|
+
end
|
20
|
+
|
21
|
+
def list
|
22
|
+
@result.map do |row|
|
23
|
+
row.map do |key, value|
|
24
|
+
key = key.to_sym if key.is_a? String
|
25
|
+
|
26
|
+
[key, value]
|
27
|
+
end.to_h
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'sqlite3'
|
2
|
+
require 'sqlbuilder'
|
3
|
+
|
4
|
+
require_relative 'results/sqlite_result'
|
5
|
+
require_relative '../logger'
|
6
|
+
|
7
|
+
module EasyMapper
|
8
|
+
module Adapters
|
9
|
+
class SqliteAdapter
|
10
|
+
def initialize(
|
11
|
+
host: nil,
|
12
|
+
port: nil,
|
13
|
+
database:,
|
14
|
+
user: nil,
|
15
|
+
password: nil
|
16
|
+
)
|
17
|
+
|
18
|
+
@database = database
|
19
|
+
end
|
20
|
+
|
21
|
+
def connect
|
22
|
+
@db = SQLite3::Database.new @database
|
23
|
+
@db.results_as_hash = true
|
24
|
+
end
|
25
|
+
|
26
|
+
def execute(query)
|
27
|
+
Logger.logger.info("Executing query: #{query}")
|
28
|
+
Results::SqliteResult.new @db.execute(query)
|
29
|
+
end
|
30
|
+
|
31
|
+
def sql_builder
|
32
|
+
Sqlbuilder::Builders::PostgresBuilder.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def next_id(table_name)
|
36
|
+
query = sql_builder.select
|
37
|
+
.column('seq')
|
38
|
+
.from('sqlite_sequence')
|
39
|
+
.where(name: table_name)
|
40
|
+
.build
|
41
|
+
|
42
|
+
execute(query).single_value + 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module EasyMapper
|
2
|
+
module Associations
|
3
|
+
class HasOne
|
4
|
+
attr_accessor :name, :cls, :id_column
|
5
|
+
|
6
|
+
def initialize(name, cls, id_column = nil)
|
7
|
+
@name = name
|
8
|
+
@cls = cls
|
9
|
+
|
10
|
+
id_column = "#{name}_id" unless id_column
|
11
|
+
@id_column = id_column
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require_relative 'config'
|
2
|
+
|
3
|
+
require 'sqlbuilder'
|
4
|
+
|
5
|
+
module EasyMapper
|
6
|
+
class DbRepository
|
7
|
+
def initialize(model, db_adapter)
|
8
|
+
@model = model
|
9
|
+
@db_adapter = db_adapter
|
10
|
+
@sql = db_adapter.sql_builder
|
11
|
+
|
12
|
+
@db_adapter.connect
|
13
|
+
end
|
14
|
+
|
15
|
+
def create(record)
|
16
|
+
query = @sql.insert
|
17
|
+
.into(@model.table_name)
|
18
|
+
.record(record)
|
19
|
+
.build
|
20
|
+
|
21
|
+
@db_adapter.execute(query)
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete(query_filters)
|
25
|
+
query = @sql.delete
|
26
|
+
.from(@model.table_name)
|
27
|
+
.where(query_filters)
|
28
|
+
.build
|
29
|
+
|
30
|
+
@db_adapter.execute(query)
|
31
|
+
end
|
32
|
+
|
33
|
+
def update(id, record)
|
34
|
+
values_to_update = record.reject { |key, _value| key.eql? :id }.to_h
|
35
|
+
|
36
|
+
query = @sql.update
|
37
|
+
.table(@model.table_name)
|
38
|
+
.where(id: id)
|
39
|
+
.set(values_to_update)
|
40
|
+
.build
|
41
|
+
|
42
|
+
@db_adapter.execute(query)
|
43
|
+
end
|
44
|
+
|
45
|
+
def find(query)
|
46
|
+
sql_builder = @sql.select
|
47
|
+
.column('*', from: 'model')
|
48
|
+
.from(@model.table_name, aliaz: 'model')
|
49
|
+
|
50
|
+
build_join(sql_builder)
|
51
|
+
|
52
|
+
sql_builder.where(query.where) unless query.where.empty?
|
53
|
+
sql_builder.order(query.order) unless query.order.empty?
|
54
|
+
sql_builder.limit(query.limit) if query.limit
|
55
|
+
sql_builder.offset(query.offset) if query.offset
|
56
|
+
|
57
|
+
sql_query = sql_builder.build
|
58
|
+
@db_adapter.execute(sql_query).list
|
59
|
+
end
|
60
|
+
|
61
|
+
def next_id
|
62
|
+
@db_adapter.next_id(@model.table_name)
|
63
|
+
end
|
64
|
+
|
65
|
+
def aggregate(aggregation, field, where_clause)
|
66
|
+
sql_builder = @sql.select
|
67
|
+
.from(@model.table_name)
|
68
|
+
.aggregation(aggregation, field)
|
69
|
+
|
70
|
+
sql_builder.where(where_clause) unless where_clause.empty?
|
71
|
+
|
72
|
+
sql_query = sql_builder.build
|
73
|
+
@db_adapter.execute(sql_query).single_value
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def build_join(sql_builder)
|
79
|
+
@model.associations_to_one.each do |assoc|
|
80
|
+
column = assoc.id_column.to_sym
|
81
|
+
|
82
|
+
assoc.cls.attributes.each do |attr|
|
83
|
+
sql_builder.column(
|
84
|
+
attr.to_s,
|
85
|
+
from: assoc.name.to_s,
|
86
|
+
as: "#{assoc.name}.#{attr}"
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
sql_builder.join(assoc.cls.table_name, on: { column => :id })
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module EasyMapper
|
2
|
+
module Errors
|
3
|
+
class EasyMapperError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class DeleteUnsavedRecordError < EasyMapperError
|
7
|
+
end
|
8
|
+
|
9
|
+
class UnknownAttributeError < EasyMapperError
|
10
|
+
def initialize(attribute_name)
|
11
|
+
super "Unknown attribute #{attribute_name}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class NoDatabaseConnectionError < EasyMapperError
|
16
|
+
end
|
17
|
+
|
18
|
+
class AnonymousClassError < EasyMapperError
|
19
|
+
def initialize
|
20
|
+
super 'Anonymous classes must provide a table name'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module EasyMapper
|
4
|
+
class Logger
|
5
|
+
class << self
|
6
|
+
attr_writer :device, :level
|
7
|
+
|
8
|
+
device = STDOUT
|
9
|
+
level = :DEBUG
|
10
|
+
|
11
|
+
def logger
|
12
|
+
unless @logger
|
13
|
+
@logger = ::Logger.new device
|
14
|
+
@logger.level = ::Logger.const_get(level)
|
15
|
+
@logger.datetime_format = '%Y-%m-%d %H:%M:%S '
|
16
|
+
end
|
17
|
+
|
18
|
+
@logger
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def device
|
24
|
+
@device = STDOUT unless @device
|
25
|
+
|
26
|
+
@device
|
27
|
+
end
|
28
|
+
|
29
|
+
def level
|
30
|
+
@level = :DEBUG unless @level
|
31
|
+
|
32
|
+
@level
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require_relative 'query'
|
2
|
+
require_relative 'model/class_macros'
|
3
|
+
require_relative 'model/query_methods'
|
4
|
+
require_relative 'db_repository'
|
5
|
+
require_relative 'errors'
|
6
|
+
|
7
|
+
module EasyMapper
|
8
|
+
module Model
|
9
|
+
def self.included(cls)
|
10
|
+
cls.extend ClassMacros
|
11
|
+
cls.extend QueryMethods
|
12
|
+
|
13
|
+
db_adapter = EasyMapper::Config.db_adapter
|
14
|
+
cls.repository EasyMapper::DbRepository.new(cls, db_adapter)
|
15
|
+
|
16
|
+
cls.class_exec do
|
17
|
+
def initialize(initial_values = {})
|
18
|
+
@object = initial_values.select do |key, _|
|
19
|
+
self.class.attributes.include? key
|
20
|
+
end
|
21
|
+
|
22
|
+
all_associations = associations_to_one + associations_to_many
|
23
|
+
defined_assoc_names = all_associations.map(&:name)
|
24
|
+
|
25
|
+
@associations = initial_values.select do |key, _|
|
26
|
+
defined_assoc_names.include? key
|
27
|
+
end
|
28
|
+
|
29
|
+
associations_to_many
|
30
|
+
.reject { |assoc| initial_values.include? assoc.name }
|
31
|
+
.each do |assoc|
|
32
|
+
@associations[assoc.name] = []
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def save
|
38
|
+
associations_to_one.each do |assoc_to_one|
|
39
|
+
result = @associations[assoc_to_one.name].save
|
40
|
+
@object[assoc_to_one.id_column] = result.id
|
41
|
+
end
|
42
|
+
|
43
|
+
if id
|
44
|
+
repository.update(id, @object)
|
45
|
+
else
|
46
|
+
@object[:id] = repository.next_id
|
47
|
+
repository.create(@object)
|
48
|
+
end
|
49
|
+
|
50
|
+
associations_to_many.each do |assoc_to_many|
|
51
|
+
@associations[assoc_to_many.name].each do |model|
|
52
|
+
model.public_send "#{assoc_to_many.mapped_by}=", id
|
53
|
+
model.save
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete
|
61
|
+
raise Errors::DeleteUnsavedRecordError unless id
|
62
|
+
|
63
|
+
associations_to_many.each do |assoc_to_many|
|
64
|
+
@associations[assoc_to_many.name].each(&:delete)
|
65
|
+
end
|
66
|
+
|
67
|
+
repository.delete(id: id)
|
68
|
+
|
69
|
+
associations_to_one.each do |assoc_to_one|
|
70
|
+
result = @associations[assoc_to_one.name].delete
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def ==(other)
|
75
|
+
return id == other.id if id && other.id
|
76
|
+
equal? other
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def primary_keys
|
82
|
+
self.class.primary_keys
|
83
|
+
end
|
84
|
+
|
85
|
+
def repository
|
86
|
+
self.class.repository
|
87
|
+
end
|
88
|
+
|
89
|
+
def associations_to_many
|
90
|
+
self.class.associations_to_many
|
91
|
+
end
|
92
|
+
|
93
|
+
def associations_to_one
|
94
|
+
self.class.associations_to_one
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require_relative '../query'
|
2
|
+
require_relative '../associations/has_one'
|
3
|
+
require_relative '../associations/has_many'
|
4
|
+
require_relative '../associations/belongs_to'
|
5
|
+
|
6
|
+
module EasyMapper
|
7
|
+
module Model
|
8
|
+
module ClassMacros
|
9
|
+
attr_accessor :associations_to_many
|
10
|
+
|
11
|
+
def attributes(*attributes)
|
12
|
+
return @attributes if attributes.empty?
|
13
|
+
@attributes = attributes + [:id]
|
14
|
+
|
15
|
+
@attributes.each do |attribute|
|
16
|
+
define_singleton_method "find_by_#{attribute}" do |value|
|
17
|
+
objects.where(attribute => value).exec
|
18
|
+
end
|
19
|
+
|
20
|
+
create_attr_accessor(attribute)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def repository(repository = nil)
|
25
|
+
return @repository unless repository
|
26
|
+
|
27
|
+
@repository = repository
|
28
|
+
end
|
29
|
+
|
30
|
+
def table_name(name = nil)
|
31
|
+
return @table_name unless name
|
32
|
+
@table_name = name
|
33
|
+
end
|
34
|
+
|
35
|
+
def has_one(attr_name, cls:, column: nil)
|
36
|
+
association = Associations::HasOne.new(attr_name, cls, column)
|
37
|
+
associations_to_one << association
|
38
|
+
|
39
|
+
create_association_accessor(attr_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_many(attr_name, cls:, mapped_by: nil)
|
43
|
+
association = Associations::HasMany.new(attr_name, cls, mapped_by)
|
44
|
+
associations_to_many << association
|
45
|
+
|
46
|
+
create_association_accessor(attr_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
def belongs_to(cls, attr_name:, id_column: nil)
|
50
|
+
id_column = "#{attr_name}_id" unless id_column
|
51
|
+
association = Associations::BelongsTo.new(cls, attr_name, id_column)
|
52
|
+
|
53
|
+
create_association_accessor(attr_name)
|
54
|
+
create_attr_accessor(id_column)
|
55
|
+
end
|
56
|
+
|
57
|
+
def associations_to_one
|
58
|
+
@associations_to_one = [] unless @associations_to_one
|
59
|
+
@associations_to_one
|
60
|
+
end
|
61
|
+
|
62
|
+
def associations_to_many
|
63
|
+
@associations_to_many = [] unless @associations_to_many
|
64
|
+
@associations_to_many
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def create_attr_accessor(attribute)
|
70
|
+
define_method(attribute) { @object[attribute] }
|
71
|
+
define_method("#{attribute}=") { |value| @object[attribute] = value }
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_association_accessor(name)
|
75
|
+
define_method(name) { @associations[name] }
|
76
|
+
define_method("#{name}=") { |value| @associations[name] = value }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module EasyMapper
|
2
|
+
class Query
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_accessor :model
|
6
|
+
|
7
|
+
''"
|
8
|
+
builder methods
|
9
|
+
"''
|
10
|
+
|
11
|
+
def initialize(model)
|
12
|
+
@model = model
|
13
|
+
@where = {}
|
14
|
+
@order = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def where(query = nil)
|
18
|
+
return @where unless query
|
19
|
+
|
20
|
+
if @where
|
21
|
+
@where.merge!(query)
|
22
|
+
else
|
23
|
+
@where = query
|
24
|
+
end
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def order(fields = nil)
|
30
|
+
return @order unless fields
|
31
|
+
|
32
|
+
if @order
|
33
|
+
@order.merge!(fields)
|
34
|
+
else
|
35
|
+
@order = fields
|
36
|
+
end
|
37
|
+
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def limit(value = nil)
|
42
|
+
return @limit unless value
|
43
|
+
|
44
|
+
@limit = value
|
45
|
+
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def offset(value = nil)
|
50
|
+
return @offset unless value
|
51
|
+
|
52
|
+
@offset = value
|
53
|
+
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
''"
|
58
|
+
kickers
|
59
|
+
"''
|
60
|
+
|
61
|
+
def exec
|
62
|
+
map_to_model_instances @model.repository.find(self)
|
63
|
+
end
|
64
|
+
|
65
|
+
def all
|
66
|
+
where({}).exec
|
67
|
+
end
|
68
|
+
|
69
|
+
def single_result
|
70
|
+
# TODO: return single result, raise exception if none or more
|
71
|
+
exec.first
|
72
|
+
end
|
73
|
+
|
74
|
+
def each(&block)
|
75
|
+
exec.each(&block)
|
76
|
+
end
|
77
|
+
|
78
|
+
def count(field = '*')
|
79
|
+
@model.repository.aggregate('COUNT', field, @where)
|
80
|
+
end
|
81
|
+
|
82
|
+
def avg(field)
|
83
|
+
@model.repository.aggregate('AVG', field, @where).to_f
|
84
|
+
end
|
85
|
+
|
86
|
+
def sum(field)
|
87
|
+
@model.repository.aggregate('SUM', field, @where)
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspect
|
91
|
+
exec.inspect
|
92
|
+
end
|
93
|
+
|
94
|
+
def delete_all
|
95
|
+
@model.repository.delete({})
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def map_to_model_instances(records)
|
101
|
+
records.map do |record|
|
102
|
+
associations = map_associations_to_many(record)
|
103
|
+
.merge(map_associations_to_one(record))
|
104
|
+
|
105
|
+
@model.new(record.merge(associations))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def map_associations_to_many(record)
|
110
|
+
@model.associations_to_many.map do |assoc_to_many|
|
111
|
+
[
|
112
|
+
assoc_to_many.name,
|
113
|
+
assoc_to_many.cls.objects.where(assoc_to_many.mapped_by => record[:id])
|
114
|
+
]
|
115
|
+
end.to_h
|
116
|
+
end
|
117
|
+
|
118
|
+
def map_associations_to_one(record)
|
119
|
+
@model.associations_to_one.map do |assoc|
|
120
|
+
assoc_record = record
|
121
|
+
.select { |key, _| key.to_s.include? "#{assoc.name}." }
|
122
|
+
.map { |k, v| [k.to_s.gsub("#{assoc.name}.", '').to_sym, v] }
|
123
|
+
.to_h
|
124
|
+
[
|
125
|
+
assoc.name,
|
126
|
+
assoc.cls.new(assoc_record)
|
127
|
+
]
|
128
|
+
end.to_h
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
metadata
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: easy_mapper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Viktor Marinov
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-09-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.15'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.15'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlbuilder
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.1'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.19.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.19.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.3.13
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.3.13
|
97
|
+
description: Simple ORM library for Ruby
|
98
|
+
email:
|
99
|
+
- viktor.marinov95@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- ".rubocop.yaml"
|
107
|
+
- ".travis.yml"
|
108
|
+
- Gemfile
|
109
|
+
- LICENSE.txt
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- bin/console
|
113
|
+
- bin/postgre_scripts.sql
|
114
|
+
- bin/setup
|
115
|
+
- bin/sqlite3_scripts.sql
|
116
|
+
- easy_mapper.gemspec
|
117
|
+
- lib/easy_mapper.rb
|
118
|
+
- lib/easy_mapper/adapters/postgre_adapter.rb
|
119
|
+
- lib/easy_mapper/adapters/results/postgre_result.rb
|
120
|
+
- lib/easy_mapper/adapters/results/sqlite_result.rb
|
121
|
+
- lib/easy_mapper/adapters/sqlite_adapter.rb
|
122
|
+
- lib/easy_mapper/associations/belongs_to.rb
|
123
|
+
- lib/easy_mapper/associations/has_many.rb
|
124
|
+
- lib/easy_mapper/associations/has_one.rb
|
125
|
+
- lib/easy_mapper/config.rb
|
126
|
+
- lib/easy_mapper/db_repository.rb
|
127
|
+
- lib/easy_mapper/errors.rb
|
128
|
+
- lib/easy_mapper/logger.rb
|
129
|
+
- lib/easy_mapper/model.rb
|
130
|
+
- lib/easy_mapper/model/class_macros.rb
|
131
|
+
- lib/easy_mapper/model/query_methods.rb
|
132
|
+
- lib/easy_mapper/query.rb
|
133
|
+
- lib/easy_mapper/version.rb
|
134
|
+
homepage: https://github.com/ViktorMarinov/easy_mapper
|
135
|
+
licenses:
|
136
|
+
- MIT
|
137
|
+
metadata: {}
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 2.6.11
|
155
|
+
signing_key:
|
156
|
+
specification_version: 4
|
157
|
+
summary: Simple ORM library for Ruby
|
158
|
+
test_files: []
|