ormivore 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/Gemfile +14 -0
  2. data/Gemfile.lock +77 -0
  3. data/Guardfile +17 -0
  4. data/README.md +135 -0
  5. data/Rakefile +11 -0
  6. data/app/adapters/account_storage_ar_adapter.rb +16 -0
  7. data/app/adapters/account_storage_memory_adapter.rb +7 -0
  8. data/app/adapters/address_storage_ar_adapter.rb +8 -0
  9. data/app/adapters/address_storage_memory_adapter.rb +7 -0
  10. data/app/connection_manager.rb +17 -0
  11. data/app/console.rb +15 -0
  12. data/app/converters/account_sql_storage_converter.rb +33 -0
  13. data/app/converters/address_sql_storage_converter.rb +51 -0
  14. data/app/converters/noop_converter.rb +15 -0
  15. data/app/entities/account.rb +20 -0
  16. data/app/entities/address.rb +24 -0
  17. data/app/ports/account_storage_port.rb +5 -0
  18. data/app/ports/address_storage_port.rb +5 -0
  19. data/app/repos/account_repo.rb +7 -0
  20. data/app/repos/address_repo.rb +7 -0
  21. data/app/require_helpers.rb +34 -0
  22. data/db/database.yml +7 -0
  23. data/lib/console.rb +13 -0
  24. data/lib/init.rb +9 -0
  25. data/lib/ormivore/ar_adapter.rb +111 -0
  26. data/lib/ormivore/entity.rb +199 -0
  27. data/lib/ormivore/errors.rb +21 -0
  28. data/lib/ormivore/memory_adapter.rb +99 -0
  29. data/lib/ormivore/port.rb +95 -0
  30. data/lib/ormivore/repo.rb +58 -0
  31. data/lib/ormivore/version.rb +3 -0
  32. data/spec/adapters/account_storage_ar_adapter_spec.rb +13 -0
  33. data/spec/adapters/account_storage_memory_adapter_spec.rb +12 -0
  34. data/spec/adapters/address_storage_ar_adapter_spec.rb +14 -0
  35. data/spec/adapters/address_storage_memory_adapter_spec.rb +13 -0
  36. data/spec/adapters/ar_helpers.rb +9 -0
  37. data/spec/adapters/memory_helpers.rb +5 -0
  38. data/spec/adapters/shared.rb +146 -0
  39. data/spec/adapters/shared_account.rb +15 -0
  40. data/spec/adapters/shared_address.rb +21 -0
  41. data/spec/converters/account_sql_storage_converter_spec.rb +28 -0
  42. data/spec/converters/address_sql_storage_converter_spec.rb +48 -0
  43. data/spec/entities/account_spec.rb +13 -0
  44. data/spec/entities/address_spec.rb +17 -0
  45. data/spec/entities/shared.rb +114 -0
  46. data/spec/factories.rb +18 -0
  47. data/spec/integration/account_repo_ar_integration_spec.rb +12 -0
  48. data/spec/integration/account_repo_memory_integration_spec.rb +11 -0
  49. data/spec/integration/address_repo_ar_integration_spec.rb +13 -0
  50. data/spec/integration/address_repo_memory_integration_spec.rb +13 -0
  51. data/spec/integration/shared.rb +74 -0
  52. data/spec/integration/shared_account.rb +17 -0
  53. data/spec/integration/shared_address.rb +23 -0
  54. data/spec/ports/account_storage_port_spec.rb +6 -0
  55. data/spec/ports/address_storage_port_spec.rb +6 -0
  56. data/spec/ports/shared.rb +50 -0
  57. data/spec/repos/account_repo_spec.rb +6 -0
  58. data/spec/repos/address_repo_spec.rb +6 -0
  59. data/spec/repos/shared.rb +92 -0
  60. data/spec/spec.opts +3 -0
  61. data/spec/spec_db_helper.rb +54 -0
  62. data/spec/spec_helper.rb +8 -0
  63. 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,7 @@
1
+ module App
2
+ class AccountStorageMemoryAdapter
3
+ include ORMivore::MemoryAdapter
4
+
5
+ self.default_converter_class = NoopConverter
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ module App
2
+ class AddressStorageArAdapter
3
+ include ORMivore::ArAdapter
4
+
5
+ self.table_name = 'addresses'
6
+ self.default_converter_class = AddressSqlStorageConverter
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module App
2
+ class AddressStorageMemoryAdapter
3
+ include ORMivore::MemoryAdapter
4
+
5
+ self.default_converter_class = NoopConverter
6
+ end
7
+ end
@@ -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
@@ -0,0 +1,15 @@
1
+ module App
2
+ class NoopConverter
3
+ def attributes_list_to_storage(list)
4
+ list
5
+ end
6
+
7
+ def from_storage(attrs)
8
+ attrs
9
+ end
10
+
11
+ def to_storage(attrs)
12
+ attrs
13
+ end
14
+ end
15
+ end