ormivore 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|
+
[](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
|