ormivore 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +14 -0
- data/Gemfile.lock +77 -0
- data/Guardfile +17 -0
- data/README.md +135 -0
- data/Rakefile +11 -0
- data/app/adapters/account_storage_ar_adapter.rb +16 -0
- data/app/adapters/account_storage_memory_adapter.rb +7 -0
- data/app/adapters/address_storage_ar_adapter.rb +8 -0
- data/app/adapters/address_storage_memory_adapter.rb +7 -0
- data/app/connection_manager.rb +17 -0
- data/app/console.rb +15 -0
- data/app/converters/account_sql_storage_converter.rb +33 -0
- data/app/converters/address_sql_storage_converter.rb +51 -0
- data/app/converters/noop_converter.rb +15 -0
- data/app/entities/account.rb +20 -0
- data/app/entities/address.rb +24 -0
- data/app/ports/account_storage_port.rb +5 -0
- data/app/ports/address_storage_port.rb +5 -0
- data/app/repos/account_repo.rb +7 -0
- data/app/repos/address_repo.rb +7 -0
- data/app/require_helpers.rb +34 -0
- data/db/database.yml +7 -0
- data/lib/console.rb +13 -0
- data/lib/init.rb +9 -0
- data/lib/ormivore/ar_adapter.rb +111 -0
- data/lib/ormivore/entity.rb +199 -0
- data/lib/ormivore/errors.rb +21 -0
- data/lib/ormivore/memory_adapter.rb +99 -0
- data/lib/ormivore/port.rb +95 -0
- data/lib/ormivore/repo.rb +58 -0
- data/lib/ormivore/version.rb +3 -0
- data/spec/adapters/account_storage_ar_adapter_spec.rb +13 -0
- data/spec/adapters/account_storage_memory_adapter_spec.rb +12 -0
- data/spec/adapters/address_storage_ar_adapter_spec.rb +14 -0
- data/spec/adapters/address_storage_memory_adapter_spec.rb +13 -0
- data/spec/adapters/ar_helpers.rb +9 -0
- data/spec/adapters/memory_helpers.rb +5 -0
- data/spec/adapters/shared.rb +146 -0
- data/spec/adapters/shared_account.rb +15 -0
- data/spec/adapters/shared_address.rb +21 -0
- data/spec/converters/account_sql_storage_converter_spec.rb +28 -0
- data/spec/converters/address_sql_storage_converter_spec.rb +48 -0
- data/spec/entities/account_spec.rb +13 -0
- data/spec/entities/address_spec.rb +17 -0
- data/spec/entities/shared.rb +114 -0
- data/spec/factories.rb +18 -0
- data/spec/integration/account_repo_ar_integration_spec.rb +12 -0
- data/spec/integration/account_repo_memory_integration_spec.rb +11 -0
- data/spec/integration/address_repo_ar_integration_spec.rb +13 -0
- data/spec/integration/address_repo_memory_integration_spec.rb +13 -0
- data/spec/integration/shared.rb +74 -0
- data/spec/integration/shared_account.rb +17 -0
- data/spec/integration/shared_address.rb +23 -0
- data/spec/ports/account_storage_port_spec.rb +6 -0
- data/spec/ports/address_storage_port_spec.rb +6 -0
- data/spec/ports/shared.rb +50 -0
- data/spec/repos/account_repo_spec.rb +6 -0
- data/spec/repos/address_repo_spec.rb +6 -0
- data/spec/repos/shared.rb +92 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_db_helper.rb +54 -0
- data/spec/spec_helper.rb +8 -0
- metadata +187 -0
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source :rubygems
|
2
|
+
|
3
|
+
gem 'nested_exceptions'
|
4
|
+
|
5
|
+
group :development do
|
6
|
+
gem 'rake'
|
7
|
+
gem 'activesupport', '~>3.2.3'
|
8
|
+
gem 'activerecord', '~>3.2.3'
|
9
|
+
gem 'sqlite3'
|
10
|
+
gem 'rspec'
|
11
|
+
gem 'factory_girl'
|
12
|
+
gem 'database_cleaner'
|
13
|
+
gem 'guard-rspec'
|
14
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.2.13)
|
5
|
+
activesupport (= 3.2.13)
|
6
|
+
builder (~> 3.0.0)
|
7
|
+
activerecord (3.2.13)
|
8
|
+
activemodel (= 3.2.13)
|
9
|
+
activesupport (= 3.2.13)
|
10
|
+
arel (~> 3.0.2)
|
11
|
+
tzinfo (~> 0.3.29)
|
12
|
+
activesupport (3.2.13)
|
13
|
+
i18n (= 0.6.1)
|
14
|
+
multi_json (~> 1.0)
|
15
|
+
arel (3.0.2)
|
16
|
+
builder (3.0.4)
|
17
|
+
coderay (1.0.9)
|
18
|
+
database_cleaner (1.0.1)
|
19
|
+
diff-lcs (1.2.4)
|
20
|
+
factory_girl (4.2.0)
|
21
|
+
activesupport (>= 3.0.0)
|
22
|
+
ffi (1.8.1)
|
23
|
+
formatador (0.2.4)
|
24
|
+
guard (1.8.0)
|
25
|
+
formatador (>= 0.2.4)
|
26
|
+
listen (>= 1.0.0)
|
27
|
+
lumberjack (>= 1.0.2)
|
28
|
+
pry (>= 0.9.10)
|
29
|
+
thor (>= 0.14.6)
|
30
|
+
guard-rspec (3.0.1)
|
31
|
+
guard (>= 1.8)
|
32
|
+
rspec (~> 2.13)
|
33
|
+
i18n (0.6.1)
|
34
|
+
listen (1.1.4)
|
35
|
+
rb-fsevent (>= 0.9.3)
|
36
|
+
rb-inotify (>= 0.9)
|
37
|
+
rb-kqueue (>= 0.2)
|
38
|
+
lumberjack (1.0.3)
|
39
|
+
method_source (0.8.1)
|
40
|
+
multi_json (1.7.4)
|
41
|
+
nested_exceptions (1.0.1)
|
42
|
+
pry (0.9.12.2)
|
43
|
+
coderay (~> 1.0.5)
|
44
|
+
method_source (~> 0.8)
|
45
|
+
slop (~> 3.4)
|
46
|
+
rake (10.0.4)
|
47
|
+
rb-fsevent (0.9.3)
|
48
|
+
rb-inotify (0.9.0)
|
49
|
+
ffi (>= 0.5.0)
|
50
|
+
rb-kqueue (0.2.0)
|
51
|
+
ffi (>= 0.5.0)
|
52
|
+
rspec (2.13.0)
|
53
|
+
rspec-core (~> 2.13.0)
|
54
|
+
rspec-expectations (~> 2.13.0)
|
55
|
+
rspec-mocks (~> 2.13.0)
|
56
|
+
rspec-core (2.13.1)
|
57
|
+
rspec-expectations (2.13.0)
|
58
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
59
|
+
rspec-mocks (2.13.1)
|
60
|
+
slop (3.4.5)
|
61
|
+
sqlite3 (1.3.7)
|
62
|
+
thor (0.18.1)
|
63
|
+
tzinfo (0.3.37)
|
64
|
+
|
65
|
+
PLATFORMS
|
66
|
+
ruby
|
67
|
+
|
68
|
+
DEPENDENCIES
|
69
|
+
activerecord (~> 3.2.3)
|
70
|
+
activesupport (~> 3.2.3)
|
71
|
+
database_cleaner
|
72
|
+
factory_girl
|
73
|
+
guard-rspec
|
74
|
+
nested_exceptions
|
75
|
+
rake
|
76
|
+
rspec
|
77
|
+
sqlite3
|
data/Guardfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# More info at https://github.com/guard/guard#readme
|
2
|
+
|
3
|
+
notification :tmux
|
4
|
+
notification :terminal_title
|
5
|
+
|
6
|
+
guard :rspec,
|
7
|
+
:all_on_start => true,
|
8
|
+
:run_all => { :cli => '--color' },
|
9
|
+
:cli => '--color --format nested' do
|
10
|
+
watch(%r{^spec/.+_spec\.rb$})
|
11
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
12
|
+
watch(%r{^spec/(.+)/shared.*\.rb$}) { |m| "spec/#{m[1]}" }
|
13
|
+
watch(%r{^spec/(.+)/.*helpers\.rb$}) { |m| "spec/#{m[1]}" }
|
14
|
+
watch(%r{spec/spec_(db_)?helper.rb}) { 'spec' }
|
15
|
+
watch('spec/factories.rb') { 'spec' }
|
16
|
+
watch(%r{^lib/(.+)\.rb$}) { 'spec' }
|
17
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
# ORMivore
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/olek/ormivore.png)](http://travis-ci.org/olek/ormivore)
|
4
|
+
|
5
|
+
## Synopsis
|
6
|
+
|
7
|
+
ORM framework for **long lived** ruby projects with a twist.
|
8
|
+
|
9
|
+
## Caution
|
10
|
+
|
11
|
+
ORMivore is highly opinionated and does not quite follow conventional
|
12
|
+
rules of wisdom in ruby/rails community. If you want to stay 'in sync'
|
13
|
+
with mainstream 'rails engineers' - stay away (and stop reading - it may
|
14
|
+
cause permanent brain damage).
|
15
|
+
|
16
|
+
## Audience
|
17
|
+
|
18
|
+
If you want to be able to maintain your app in couple years, or want
|
19
|
+
to get legacy app back under control, ORMivore may have something
|
20
|
+
for you.
|
21
|
+
|
22
|
+
If quick throw-away projects that require no extension/maintenance in
|
23
|
+
the future are your goal, ORMivore is a waste of time (for you).
|
24
|
+
|
25
|
+
## Stability
|
26
|
+
|
27
|
+
ORMivore is in the R&D 'alpha' stage. In other words, it is my
|
28
|
+
playground for experimenting with new approach to ORM. Changes are
|
29
|
+
definitely not backward compatible. If you decide to use it, prepare to
|
30
|
+
roll up your sleeves, and I disclaim any potential indirect harm to kittens.
|
31
|
+
|
32
|
+
If I still need to spell it out - ORMivore is not production ready just yet.
|
33
|
+
|
34
|
+
## Motivation
|
35
|
+
|
36
|
+
If you have seen legacy Rails app, chances are you noticed:
|
37
|
+
|
38
|
+
- persistance logic hopelessly coupled with business logic
|
39
|
+
- business rules are forming one clump
|
40
|
+
- copious amounts of logged SQL that is hard to relate back to application code
|
41
|
+
- bad performance and high database load caused by inefficient SQL
|
42
|
+
queries
|
43
|
+
|
44
|
+
ORMivore is designed to helps you either avoid those issues on your project, or to
|
45
|
+
slowly eliminate them on your legacy app.
|
46
|
+
|
47
|
+
## Philosophy summary
|
48
|
+
|
49
|
+
- Everything the system does FOR you, the system also does TO you
|
50
|
+
- Actions that will lead to long term pain better be painful
|
51
|
+
immediately, and never sugar-coated with short term gain
|
52
|
+
- Long term maintenability trumps supersonic initial development
|
53
|
+
- There is much to be learned from functional programming
|
54
|
+
|
55
|
+
## Philosophy explained
|
56
|
+
|
57
|
+
Many ORMs simply do too good of a job - they isolate developers from
|
58
|
+
storage so much that developers choose to pretend it is just an inconvenient
|
59
|
+
abstraction, and ignore it as much as possible. That causes huge loss of
|
60
|
+
database efficiency. ORMivore does not abstract storage too far away -
|
61
|
+
it does only the bare minimum. No automatic associations management, no
|
62
|
+
callbacks, no frills at all.
|
63
|
+
|
64
|
+
OOD is great, but frequently overused. ORMivore uses plain data
|
65
|
+
structures (functional style) to communicate between different layers,
|
66
|
+
solving problem of messy circular dependencies.
|
67
|
+
|
68
|
+
Mutable entities have state, and that makes reasoning about them more
|
69
|
+
difficult. Immutable entities are easy to reason about and discourage
|
70
|
+
placing any "how to" behavior on them, making them a good place for
|
71
|
+
"what is" messages. As a side benefit, immutable entities play well in
|
72
|
+
multi-treaded environments.
|
73
|
+
|
74
|
+
Ports and adapters pattern (hexagonal architecture) is great for
|
75
|
+
isolating persistance logic from entities. It also allows for some
|
76
|
+
degree of substitutability of different storages.
|
77
|
+
|
78
|
+
While STI and polymorphic relations are bad for your health, your legacy
|
79
|
+
database is probably littered with them, and ORMivore provides mean to
|
80
|
+
map those to domain objects in a 'classless' way.
|
81
|
+
That makes it possible to introduce ORMivore entities in the same
|
82
|
+
project/process as legacy ActiveRecord models.
|
83
|
+
|
84
|
+
## Installation
|
85
|
+
|
86
|
+
Well, I have not built gem for this project just yet, but when I do, just add 'gem
|
87
|
+
"ormivore"' to Gemfile, or run 'gem install ormivore'.
|
88
|
+
|
89
|
+
## Code Example
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
new_account = Account.new(firstname: 'John', lastname: 'Doe')
|
93
|
+
|
94
|
+
saved_account = repo.persist(account)
|
95
|
+
|
96
|
+
changed_account = saved_account.apply(firstname: 'Jane')
|
97
|
+
|
98
|
+
saved_changed_account = repo.persist(changed_account)
|
99
|
+
|
100
|
+
reloaded_account = repo.find_by_id(saved_changed_account.id)
|
101
|
+
```
|
102
|
+
|
103
|
+
## Tests
|
104
|
+
|
105
|
+
To run tests on ORMivore and embedded sample app, run something along
|
106
|
+
those lines:
|
107
|
+
|
108
|
+
```bash
|
109
|
+
gem install bundle
|
110
|
+
|
111
|
+
bundle install --path vendor/bundle
|
112
|
+
|
113
|
+
bundle exec rake spec
|
114
|
+
```
|
115
|
+
|
116
|
+
## Contributors
|
117
|
+
|
118
|
+
At this point, this is the playground of experimentation, and it does
|
119
|
+
not have to be just mine! Forks and pull requests are welcome. Of
|
120
|
+
course, I reserve the right to politely decline or ruthlessly
|
121
|
+
'refactor'.
|
122
|
+
|
123
|
+
## License
|
124
|
+
|
125
|
+
One word - MIT.
|
126
|
+
|
127
|
+
## Apologies
|
128
|
+
|
129
|
+
> Documentation is like sex: when it is good, it is very, very good; and when it is bad, it is better than nothing.
|
130
|
+
|
131
|
+
This README is certainly not enough, but it is indeed better than nothing.
|
132
|
+
|
133
|
+
English is not my 'mother tongue'. I am sure this document is littered
|
134
|
+
with mistaekes. If your english is any better than mine - please help me
|
135
|
+
fix them.
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
|
3
|
+
desc 'Run specs'
|
4
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
5
|
+
# task.pattern = 'spec/{models,javascripts}/**/*_spec.rb'
|
6
|
+
# task.rspec_opts = ['--color', '--format progress', '--timeout 0.1']
|
7
|
+
task.rspec_opts = ['--color', '--format progress']
|
8
|
+
# task.spec_files = FileList['spec/**/*_spec.rb']
|
9
|
+
end
|
10
|
+
|
11
|
+
task :default => :spec
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module App
|
2
|
+
class AccountStorageArAdapter
|
3
|
+
include ORMivore::ArAdapter
|
4
|
+
|
5
|
+
self.table_name = 'accounts'
|
6
|
+
self.default_converter_class = AccountSqlStorageConverter
|
7
|
+
|
8
|
+
expand_on_create do |attrs|
|
9
|
+
{
|
10
|
+
login: attrs[:email],
|
11
|
+
crypted_password: 'Unknown'
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ConnectionManager
|
2
|
+
def self.establish_connection(env, logger=nil)
|
3
|
+
raise unless env
|
4
|
+
|
5
|
+
unless ActiveRecord::Base.connected?
|
6
|
+
configuration = YAML::load(
|
7
|
+
File.open(File.join(RequireHelpers.root, 'db', 'database.yml'))
|
8
|
+
)[env.to_s]
|
9
|
+
|
10
|
+
puts "Connecting to database #{configuration['database']}"
|
11
|
+
|
12
|
+
ActiveRecord::Base.establish_connection(configuration)
|
13
|
+
|
14
|
+
ActiveRecord::Base.logger = logger
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/app/console.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Allows running a lightweight version of rails console
|
2
|
+
# Start it like this:
|
3
|
+
#
|
4
|
+
# irb -r ./lib/console.rb
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'bundler/setup'
|
8
|
+
|
9
|
+
Bundler.require(:default)
|
10
|
+
|
11
|
+
require_relative 'require_helpers'
|
12
|
+
|
13
|
+
RequireHelpers.require_all
|
14
|
+
|
15
|
+
ConnectionManager.establish_connection(YAML::load(File.open('./db/database.yml')), Logger.new(STDOUT))
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module App
|
2
|
+
class AccountSqlStorageConverter
|
3
|
+
STATUS_MAP = Hash.new { |h, k|
|
4
|
+
raise ArgumentError, "Status #{k.inspect} not known"
|
5
|
+
}.update(
|
6
|
+
active: 1,
|
7
|
+
inactive: 2,
|
8
|
+
deleted: 3
|
9
|
+
).freeze
|
10
|
+
|
11
|
+
REVERSE_STATUS_MAP = Hash.new { |h, k|
|
12
|
+
raise ArgumentError, "Status #{k.inspect} not known"
|
13
|
+
}.update(
|
14
|
+
Hash[STATUS_MAP.to_a.map(&:reverse)]
|
15
|
+
).freeze
|
16
|
+
|
17
|
+
def attributes_list_to_storage(list)
|
18
|
+
list
|
19
|
+
end
|
20
|
+
|
21
|
+
def from_storage(attrs)
|
22
|
+
attrs.dup.tap { |copy|
|
23
|
+
copy[:status] = REVERSE_STATUS_MAP[copy[:status]] if copy[:status]
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_storage(attrs)
|
28
|
+
attrs.dup.tap { |copy|
|
29
|
+
copy[:status] = STATUS_MAP[copy[:status]] if copy[:status]
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module App
|
2
|
+
class AddressSqlStorageConverter
|
3
|
+
ADDRESS_TYPE_MAP = Hash.new { |h, k|
|
4
|
+
raise ArgumentError, "Addressable type #{k.inspect} not known"
|
5
|
+
}.update(
|
6
|
+
shipping: 'ShippingAddress',
|
7
|
+
billing: 'BillingAddress'
|
8
|
+
).freeze
|
9
|
+
|
10
|
+
REVERSE_ADDRESS_TYPE_MAP = Hash.new { |h, k|
|
11
|
+
raise ArgumentError, "Addressable type #{k.inspect} not known"
|
12
|
+
}.update(
|
13
|
+
Hash[ADDRESS_TYPE_MAP.to_a.map(&:reverse)]
|
14
|
+
).freeze
|
15
|
+
|
16
|
+
def attributes_list_to_storage(list)
|
17
|
+
list.dup.tap { |converted|
|
18
|
+
if converted.delete(:account_id)
|
19
|
+
converted << :addressable_id << :addressable_type
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def from_storage(attrs)
|
25
|
+
attrs.dup.tap { |copy|
|
26
|
+
copy[:type] = REVERSE_ADDRESS_TYPE_MAP[copy[:type]] if copy[:type]
|
27
|
+
addressable_id, addressable_type = copy.delete(:addressable_id), copy.delete(:addressable_type)
|
28
|
+
if addressable_id && addressable_type
|
29
|
+
replacement_attr = case addressable_type
|
30
|
+
when 'Account'
|
31
|
+
:account_id
|
32
|
+
else
|
33
|
+
raise ORMivore::BadAttributesError, "Unknown addressable_type #{addressable_type.inspect}"
|
34
|
+
end
|
35
|
+
copy[replacement_attr] = addressable_id
|
36
|
+
end
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_storage(attrs)
|
41
|
+
attrs.dup.tap { |copy|
|
42
|
+
copy[:type] = ADDRESS_TYPE_MAP[copy[:type]] if copy[:type]
|
43
|
+
account_id = copy.delete(:account_id)
|
44
|
+
if account_id
|
45
|
+
copy[:addressable_id] = account_id
|
46
|
+
copy[:addressable_type] = 'Account'
|
47
|
+
end
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|