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.
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