peacekeeper 0.0.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1fa3eff5db180174619150ac1f8004706e6cfa9e
4
+ data.tar.gz: 9668cfd0a54b84b6825338f9dda8c5b8d4c11e7f
5
+ SHA512:
6
+ metadata.gz: 73461cb3360790b227ddd4504858b112cd2db729d59af6c21893a9b702c5b0c3f04824f7a16250864464495c8923342f8110898cf545d5509f8ab94c1f51d7e9
7
+ data.tar.gz: fa72f99fef480695f45d4116c6b7226981ec95f21b25af8406058170eb09d946b3bf205ddd0224c911f0338f3eaf6cd6957494a96b1b0efb3005c11fef2233bf
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ ## What is Peacekeeper?
2
+
3
+ Peacekeeper is a simple delegation library that allows the creation of business objects (Peacekeeper models) that hide the data objects they pull from. This allows a clean separation of business logic and data logic, without any kind of imposition on how the data is stored. Business objects (Peacekeeper models) can pull from an API resource or database resource, or this can be changed at runtime.
4
+
5
+ Peacekeeper delegates any unknown methods from your business objects directly to their respective data object.
6
+
7
+ examples usages for each DB type:
8
+
9
+ ```ruby
10
+ # API resource using Nasreddin
11
+ # lib/models/car_model.rb
12
+ class CarModel < Peacekeeper::Model
13
+ self.data_source = :api
14
+ # application specific business logic
15
+ end
16
+
17
+ # lib/data/api/car.rb
18
+ class Car < Nasreddin::Resource('cars')
19
+ # data retrieval logic
20
+ end
21
+
22
+ # Database resource using Sequel
23
+ # lib/models/car_model.rb
24
+ class CarModel < Peacekeeper::Model
25
+ self.data_source = :sequel # Can also use :active_record
26
+ # application specific business logic
27
+ end
28
+
29
+ # lib/data/sequel/car.rb
30
+ class Car < Sequel::Model
31
+ # data retrieval logic
32
+ end
33
+
34
+ # lib/data/active_record/car.rb
35
+ class Car < ActiveRecord::Base
36
+ # data retrieval logic
37
+ end
38
+ ```
39
+
40
+ # Setting up Peacekeeper
41
+ Peacekeeper requires that your data source be configured. This can be done in a
42
+ yaml file:
43
+
44
+ ```yaml
45
+ default: &default
46
+ adapter: <%= defined?(JRUBY_VERSION) ? 'jdbc:mysql':'mysql2' %>
47
+ username: root
48
+ password:
49
+ host: localhost
50
+ xa: false
51
+
52
+ development:
53
+ <<: *default
54
+ database: dev
55
+ test:
56
+ <<: *default
57
+ database: test
58
+ build:
59
+ <<: *default
60
+ database: build
61
+ staging:
62
+ <<: *default
63
+ database: staging
64
+ production:
65
+ <<: *default
66
+ database: production
67
+ ```
68
+
69
+ # Sinatra
70
+ If you are using Sinatra, you can setup Peacekeeper with something like this in
71
+ your lib dir.
72
+
73
+ ```ruby
74
+ # lib/database.rb
75
+ dbcfg = File.read(File.expand_path('config/database.yml', APP_ROOT))
76
+ dbcfg = ERB.new(dbcfg).result(binding)
77
+ Peacekeeper::Model.config = YAML.load(dbcfg)[ENV['RACK_ENV']]
78
+ Sequel.default_timezone = :utc
79
+ ```
80
+
81
+ # Rails
82
+ If you are using Rails, you can setup Peacekeeper with something like this in
83
+ your application.rb.
84
+
85
+ ```ruby
86
+ # config/application.rb
87
+ class Application < Rails::Application
88
+ dbcfg = File.read(File.expand_path('config/database.yml', config.root))
89
+ dbcfg = ERB.new(dbcfg).result(binding)
90
+ Peacekeeper::Model.config = YAML.load(dbcfg)[Rails.env]
91
+ end
92
+ ```
93
+
94
+ # Testing with Peacekeeper
95
+ Peacekeeper also makes testing easy with the :mock ORM type, this can be set in your test code which sets Peacekeeper to return mock objects from any delegated methods.
96
+ You will need a mocking library installed that responds to mock(). If you are using rspec, add this to your spec_helper.rb file:
97
+
98
+ ```ruby
99
+ RSpec.configure do |config|
100
+ config.before(:all) do
101
+ Peacekeeper::Model.config[:mock_library] = 'mocha'
102
+ Peacekeeper::Model.data_source = :mock
103
+ end
104
+ end
105
+ ```
106
+
107
+ example usage:
108
+ ```ruby
109
+ describe 'CarModel' do
110
+ CarModel.data_source = :mock
111
+ # test business logic
112
+ end
113
+ ```
114
+
115
+ # License
116
+
117
+ Copyright (C) 2012-2013 Burnside Digital
118
+
119
+ Licensed under the BSD 2-Clause License. See COPYING for license details.
data/Rakefile CHANGED
@@ -1 +1,12 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
+ require 'yard'
3
+
4
+ desc 'Run all the tests'
5
+ task :test do
6
+ sh 'bacon -a'
7
+ end
8
+
9
+ YARD::Rake::YardocTask.new do |t|
10
+ t.name = 'doc'
11
+ t.files = Dir["#{File.dirname(__FILE__)}/lib/**/*"]
12
+ end
data/lib/peacekeeper.rb CHANGED
@@ -1,5 +1,4 @@
1
- require "peacekeeper/version"
2
-
3
- module Peacekeeper
4
- # Your code goes here...
5
- end
1
+ require 'peacekeeper/version'
2
+ require 'peacekeeper/loader'
3
+ require 'peacekeeper/model_delegation'
4
+ require 'peacekeeper/model'
@@ -0,0 +1,112 @@
1
+ module Peacekeeper
2
+ class Loader
3
+ attr_reader :source, :config
4
+
5
+ def initialize(config)
6
+ @config = config
7
+ @source = config[:source]
8
+ end
9
+
10
+ def load_source
11
+ case source
12
+ when :sequel
13
+ require 'sequel'
14
+ Sequel::Model.db = Sequel::DATABASES.find { |db| db.uri == sequel_db_uri } || Sequel.connect(sequel_db_uri)
15
+ when :active_record
16
+ require 'active_record'
17
+ ActiveRecord::Base.establish_connection(active_record_config)
18
+ when :api
19
+ require 'nasreddin'
20
+ when :mock
21
+ require config[:mock_library] if config[:mock_library]
22
+ #data_class # Trigger mock data_class creation
23
+ end
24
+ end
25
+
26
+
27
+ # Construct uri to connect to database
28
+ # Sequel: http://sequel.rubyforge.org/rdoc/files/doc/opening_databases_rdoc.html
29
+ def sequel_db_uri
30
+ # Set the protocol (DB engine; i.e. mysql, sqlite3, postgres, etc.)
31
+ protocol = config['protocol'] || config['adapter'] || 'sqlite'
32
+ if RUBY_ENGINE == 'jruby'
33
+ protocol = "jdbc:#{protocol}" unless protocol.start_with? "jdbc:"
34
+ end
35
+
36
+ # Set the path (hostname & database name)
37
+ path = "#{config['host'] || config['path']}/#{config['database']}"
38
+ path = '' if path == '/' # Clear path if 'host', 'path', and 'database' are all unset
39
+
40
+ # Set the user and password
41
+ if RUBY_ENGINE == 'jruby' && protocol == 'jdbc:mysql'
42
+ # Special case for JRuby and MySQL
43
+ user_pass = "?user=#{config['username']}&password=#{config['password']}"
44
+ server_path = "#{path}#{user_pass}"
45
+ else
46
+ user_pass = "#{config['username']}:#{config['password']}@"
47
+ user_pass = '' if user_pass == ':@' # Clear user_pass if both 'username' and 'password' are unset
48
+ server_path = "#{user_pass}#{path}"
49
+ end
50
+
51
+ # Finally, put the protocol and path components together
52
+ server_path = "/#{server_path}" unless server_path.empty?
53
+ uri = "#{protocol}:/#{server_path}"
54
+ uri = 'jdbc:sqlite::memory:' if uri == 'jdbc:sqlite:/' && RUBY_ENGINE == 'jruby'
55
+ if config['options']
56
+ if uri =~ /\?/
57
+ uri += "&#{paramize(config['options'])}"
58
+ else
59
+ uri += "?#{paramize(config['options'])}"
60
+ end
61
+ end
62
+ uri
63
+ end
64
+
65
+ def active_record_config
66
+ protocol = config['protocol'] || config['adapter'] || 'sqlite3'
67
+ # Set the adapter (DB engine; i.e. mysql, sqlite3, postgres, etc.)
68
+
69
+ database = config['database']
70
+ ar_config = {
71
+ adapter: protocol,
72
+ database: database
73
+ }
74
+ ar_config['host'] = config['host'] if config['host']
75
+ ar_config['username'] = config['username'] if config['username']
76
+ ar_config['password'] = config['password'] if config['password']
77
+ ar_config['driver'] = config['driver'] if config['driver']
78
+ ar_config
79
+ end
80
+
81
+ def paramize(options)
82
+ params = options.map { |k, v| "#{k}=#{v}" }
83
+ "#{params.join('&')}"
84
+ end
85
+ end
86
+
87
+ module DataLoader
88
+ def self.data_class(config)
89
+ source = config[:source]
90
+ data_lib_name = config[:data_lib_name]
91
+ data_name = config[:data_name]
92
+ if source.nil?
93
+ nil
94
+ elsif source == :mock
95
+ Kernel.const_set(data_name, Class.new do
96
+ def self.new(opts = {})
97
+ mock(self.name.gsub(/^.*:/, ''), opts)
98
+ end
99
+ def self.method_missing(*)
100
+ self.new
101
+ end
102
+ def self.respond_to?(*)
103
+ true
104
+ end
105
+ end)
106
+ else
107
+ require "data/#{source}/#{data_lib_name}"
108
+ Kernel.const_get(data_name)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,122 @@
1
+ module Peacekeeper
2
+ class Model
3
+ kernel = ::Kernel.dup
4
+ kernel.class_eval do
5
+ [:to_s, :inspect, :=~, :!~, :===, :<=>, :eql?, :hash].each do |m|
6
+ undef_method m
7
+ end
8
+ end
9
+ include kernel
10
+
11
+ class<<self
12
+ include ModelDelegation
13
+
14
+ def new(*)
15
+ raise RuntimeError, "ModelObject cannot be instantiated directly. Please subclass first!" if self == Model
16
+ super
17
+ end
18
+
19
+ def subclasses; (@subclasses ||= []); end
20
+
21
+ def config; (@config ||= {}); end
22
+
23
+ def config=(new_config)
24
+ @config = new_config
25
+
26
+ subclasses.each do |sub|
27
+ sub.config = @config
28
+ end
29
+ @config
30
+ end
31
+
32
+ def data_source; (@data_source ||= nil); end
33
+
34
+ def data_source=(source)
35
+ @data_source = source
36
+ @loader = Loader.new(config.merge(source: source))
37
+ @loader.load_source
38
+
39
+ if source == :mock
40
+ data_class # Trigger mock data_class creation
41
+ end
42
+
43
+ subclasses.each do |sub|
44
+ sub.data_source = @data_source
45
+ end
46
+
47
+ @data_source
48
+ end
49
+
50
+ def data_class
51
+ return nil if self == Model
52
+ @data_class ||= (data_source.nil? ? nil : DataLoader.data_class(data_name: data_name, data_lib_name: data_lib_name, source: data_source))
53
+ end
54
+ alias :delegate :data_class
55
+
56
+ def inherited(sub)
57
+ unless sub.name.nil?
58
+ subclasses << sub
59
+ sub.setup(self)
60
+ end
61
+ end
62
+
63
+ def has_wrapper_for?(val)
64
+ !!wrapper_for(val)
65
+ end
66
+
67
+ def wrapper_for(val)
68
+ subclasses.find do |subclass|
69
+ val == subclass.delegate
70
+ end
71
+ end
72
+
73
+ def setup(parent)
74
+ self.config = parent.config
75
+ self.data_source = parent.data_source
76
+ end
77
+
78
+ def data_name
79
+ self.name.sub(/Model$/, '')
80
+ end
81
+
82
+ def data_lib_name
83
+ name = data_name
84
+ name.gsub!(/::/, '/')
85
+ name.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
86
+ name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
87
+ name.tr!("-", "_")
88
+ name.downcase!
89
+ name
90
+ end
91
+
92
+ end
93
+
94
+ include ModelDelegation
95
+
96
+ attr_reader :data
97
+ alias :delegate :data
98
+
99
+ def initialize(*args)
100
+ if !args.nil? && args.length == 1 && args.first.kind_of?(data_class)
101
+ @data = args.first
102
+ else
103
+ @data = data_class.new(*args)
104
+ end
105
+
106
+ unless self.class.instance_methods(false).include?(:to_json)
107
+ class<<self
108
+ undef_method :to_json if respond_to?(:to_json)
109
+ undef_method :as_json if respond_to?(:as_json)
110
+ end
111
+ end
112
+ end
113
+
114
+ def ==(obj)
115
+ obj.equal?(self) || @data == obj.data
116
+ end
117
+
118
+ def data_class
119
+ self.class.data_class
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,68 @@
1
+ module Peacekeeper
2
+ module ModelDelegation
3
+ class Pass
4
+ def self.===(o)
5
+ false
6
+ end
7
+ end
8
+
9
+ def method_missing(mid, *args, &block)
10
+ if !delegate.nil? && delegate.respond_to?(mid)
11
+ mblock = begin
12
+ delegate.method(mid).to_proc
13
+ rescue NameError
14
+ proc do |*args, &block|
15
+ delegate.send(mid, *args, &block)
16
+ end
17
+ end
18
+ define_wrapped_singleton_method(mid, mblock)
19
+ __send__(mid, *args, &block)
20
+ else
21
+ super
22
+ end
23
+ end
24
+
25
+ def respond_to_missing?(mid, include_private)
26
+ ret = delegate.respond_to?(mid, include_private)
27
+ if ret && include_private && !delegate.respond_to?(mid, false)
28
+ # Don't delegate private methods
29
+ return false
30
+ end
31
+ ret
32
+ end
33
+
34
+ def wrap(val)
35
+ if (self.kind_of?(Class) && val.kind_of?(delegate))
36
+ new(val)
37
+ elsif (self.kind_of?(Peacekeeper::Model) && val.kind_of?(delegate.class))
38
+ self.class.new(val)
39
+ elsif (Peacekeeper::Model.has_wrapper_for?(val.class))
40
+ Peacekeeper::Model.wrapper_for(val.class).new(val)
41
+ elsif (val.kind_of?(Hash))
42
+ Hash[*val.flat_map { |k, v| [k, wrap(v)] }]
43
+ elsif (val.kind_of?(Enumerable) || val.methods.include?(:each))
44
+ val.map { |i| wrap(i) }
45
+ else
46
+ val
47
+ end
48
+ end
49
+
50
+ def define_wrapped_singleton_method(mid, mblock)
51
+ define_singleton_method(mid) do |*args, &block|
52
+ wrap(mblock.call(*args, &block))
53
+ end
54
+ end
55
+
56
+ def def_data_method(mid, &mblock)
57
+ define_method(mid) do |*args|
58
+ wrap(delegate.instance_exec(*args, &mblock))
59
+ end
60
+ end
61
+
62
+ def def_singleton_data_method(mid, &mblock)
63
+ define_singleton_method(mid) do |*args|
64
+ wrap(delegate.instance_exec(*args, &mblock))
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,3 +1,3 @@
1
1
  module Peacekeeper
2
- VERSION = "0.0.0"
2
+ VERSION = '0.4.0'
3
3
  end
@@ -0,0 +1,2 @@
1
+ class AfterAr < ActiveRecord::Base
2
+ end
@@ -0,0 +1 @@
1
+ class AfterSettingAr; end
@@ -0,0 +1,2 @@
1
+ class BeforeAr < ActiveRecord::Base
2
+ end
@@ -0,0 +1 @@
1
+ class BeforeSettingAr; end
@@ -0,0 +1,3 @@
1
+ class MySubtestAr < ActiveRecord::Base
2
+ belongs_to :my_test_ar
3
+ end
@@ -0,0 +1,5 @@
1
+ class MyTestAr < ActiveRecord::Base
2
+ has_one :other, class_name: "MyTestAr", foreign_key: :other_id
3
+ has_many :my_subtest_ars
4
+ end
5
+
@@ -0,0 +1,2 @@
1
+ class After < Sequel::Model
2
+ end
@@ -0,0 +1 @@
1
+ class AfterSetting; end
@@ -0,0 +1,2 @@
1
+ class Before < Sequel::Model
2
+ end
@@ -0,0 +1 @@
1
+ class BeforeSetting; end
@@ -0,0 +1,3 @@
1
+ class MySubtest < Sequel::Model
2
+ many_to_one :my_test
3
+ end
@@ -0,0 +1,5 @@
1
+ class MyTest < Sequel::Model
2
+ one_to_one :other, class: MyTest, key: :other_id
3
+ one_to_many :my_subtests
4
+ end
5
+
@@ -0,0 +1,137 @@
1
+ require_relative '../test_helper'
2
+
3
+ describe Peacekeeper::Loader do
4
+ describe 'manages a data source selection' do
5
+ # Implementation deatail...
6
+ Peacekeeper::Model.instance_variable_set(:@subclasses, [])
7
+
8
+ it 'is nil by default' do
9
+ Peacekeeper::Model.data_source.should.be.nil
10
+ end
11
+
12
+ describe 'set to Sequel' do
13
+ before do
14
+ Peacekeeper::Model.data_source = nil
15
+ end
16
+
17
+ it 'requires the Sequel library' do
18
+ loader = mock("Loader")
19
+ loader.should.receive(:load_source)
20
+ Peacekeeper::Loader.should.receive(:new).and_return(loader)
21
+ Peacekeeper::Model.data_source = :sequel
22
+ end
23
+
24
+ it 'loads the Data object for subclasses created before' do
25
+ loader = mock("Loader")
26
+ loader.should.receive(:load_source).times(3)
27
+ Peacekeeper::Loader.should.receive(:new).times(3).and_return(loader)
28
+
29
+ class BeforeModel < Peacekeeper::Model;
30
+ end
31
+ Peacekeeper::Model.data_source = :sequel
32
+ lambda do
33
+ BeforeModel.data_class.should.equal Before
34
+ end
35
+ end
36
+
37
+ it 'loads the Data object for subclasses created after' do
38
+ loader = mock("Loader")
39
+ loader.should.receive(:load_source).times(3)
40
+ Peacekeeper::Loader.should.receive(:new).times(3).and_return(loader)
41
+
42
+ Peacekeeper::Model.data_source = :sequel
43
+ class AfterModel < Peacekeeper::Model;
44
+ end
45
+ lambda do
46
+ AfterModel.data_class.should.equal After
47
+ end
48
+ end
49
+
50
+ it 'propogates the ORM setting to subclasses' do
51
+ class BeforeSettingModel < Peacekeeper::Model;
52
+ end
53
+ Peacekeeper::Model.data_source = :sequel
54
+ class AfterSettingModel < Peacekeeper::Model;
55
+ end
56
+
57
+ BeforeSettingModel.data_source.should.equal :sequel
58
+ AfterSettingModel.data_source.should.equal :sequel
59
+ end
60
+
61
+ it 'should only connect to the Database once' do
62
+ class BeforeModel < Peacekeeper::Model;
63
+ end
64
+ class BeforeSettingModel < Peacekeeper::Model;
65
+ end
66
+ Peacekeeper::Model.data_source = :sequel
67
+ class AfterModel < Peacekeeper::Model;
68
+ end
69
+ class AfterSettingModel < Peacekeeper::Model;
70
+ end
71
+ Sequel::DATABASES.length.should.equal 1
72
+ end
73
+ end
74
+
75
+ describe 'set to Active Record' do
76
+ before do
77
+ Peacekeeper::Model.data_source = nil
78
+ end
79
+
80
+ it 'requires the Active Record library' do
81
+ loader = mock("Loader")
82
+ loader.should.receive(:load_source).times(5)
83
+ Peacekeeper::Loader.should.receive(:new).times(5).and_return(loader)
84
+ Peacekeeper::Model.data_source = :active_record
85
+ end
86
+
87
+ it 'should connect to the Database' do
88
+ Peacekeeper::Model.data_source = :active_record
89
+ ActiveRecord::Base.connection() # Force AR to ~actually~ connect to the DB
90
+ ActiveRecord::Base.connected?.should.equal true
91
+ end
92
+ end
93
+
94
+ describe 'set to mock' do
95
+ Peacekeeper::Model.data_source = :nil
96
+ Peacekeeper::Model.config[:mock_library] = 'facon'
97
+
98
+ it 'requires the facon library' do
99
+ loader = mock("Loader")
100
+ loader.should.receive(:load_source).times(5)
101
+ Peacekeeper::Loader.should.receive(:new).times(5).and_return(loader)
102
+
103
+ Peacekeeper::Model.data_source = :mock
104
+ end
105
+
106
+ it 'creates a data class ready for mocking' do
107
+ class MyMockModel < Peacekeeper::Model; end
108
+ defined?(MyMock).should.equal 'constant'
109
+ MyMock.should.receive(:delegate_call)
110
+ MyMockModel.delegate_call
111
+ end
112
+
113
+ it 'returns empty mocks for data class calls by default' do
114
+ class MyMockableModel < Peacekeeper::Model; end
115
+
116
+ foo = MyMockableModel.get_foo
117
+
118
+ foo.name.should.equal 'MyMockable'
119
+ foo.should.be.kind_of Facon::Mock
120
+ end
121
+
122
+ it 'returns a mock with properties set when #new is called with options' do
123
+ class MyInstantiableMockModel < Peacekeeper::Model; end
124
+
125
+ user = MyInstantiableMockModel.new(name: "Joe", position: :employee, vacation_days: 14)
126
+
127
+ user.name.should.equal "Joe"
128
+ user.position.should.equal :employee
129
+ user.vacation_days.should.equal 14
130
+ end
131
+
132
+ # Implementation deatail...
133
+ Peacekeeper::Model.instance_variable_set(:@subclasses, [])
134
+ end
135
+ end
136
+
137
+ end
@@ -0,0 +1,224 @@
1
+ require_relative '../test_helper'
2
+
3
+ describe Peacekeeper::Model do
4
+ it 'cannot be instantiated directly' do
5
+ -> { Peacekeeper::Model.new }.should.raise(RuntimeError)
6
+ end
7
+
8
+ class SubclassTestModel < Peacekeeper::Model; end
9
+
10
+ it 'derives a model name based on the subclass\'s name' do
11
+ SubclassTestModel.data_name.should.equal 'SubclassTest'
12
+ end
13
+
14
+ it 'derives a data library name based on the subclass\'s name' do
15
+ SubclassTestModel.data_lib_name.should.equal 'subclass_test'
16
+ end
17
+
18
+ describe 'manages a database config' do
19
+ #it 'is empty by default' do
20
+ # Peacekeeper::Model.config.should.be.empty
21
+ #end
22
+
23
+ it 'is inherited by subclasses' do
24
+ path = 'tmp/test_db.sqlite'
25
+ class SubclassBeforeModel < Peacekeeper::Model;
26
+ end
27
+ Peacekeeper::Model.config = {path: path}
28
+ class SubclassAfterModel < Peacekeeper::Model;
29
+ end
30
+
31
+ SubclassBeforeModel.config[:path].should.equal path
32
+ SubclassAfterModel.config[:path].should.equal path
33
+ end
34
+ end
35
+
36
+ describe 'used to create a model subclass with Sequel' do
37
+ # Repeat config here in case these tests are run alone
38
+ Peacekeeper::Model.config[:protocol] = 'jdbc:sqlite::memory:'
39
+ Peacekeeper::Model.data_source = :sequel
40
+
41
+ class MyTestModel < Peacekeeper::Model
42
+ def test
43
+ :ok
44
+ end
45
+ end
46
+
47
+ class MySubtestModel < Peacekeeper::Model; end
48
+
49
+ # Setup the DB and populate with some test data
50
+ DB = Sequel::Model.db
51
+ DB.create_table :my_tests do
52
+ primary_key :id
53
+ foreign_key :other_id, :my_tests
54
+ String :name
55
+ end
56
+ DB[:my_tests].insert(id: 1, name: 'A Test')
57
+ DB[:my_tests].insert(id: 2, other_id: 1, name: 'Other')
58
+ DB[:my_tests].filter(id: 1).update(other_id: 2)
59
+
60
+ DB.create_table :my_subtests do
61
+ primary_key :id
62
+ foreign_key :my_test_id, :my_tests
63
+ String :name
64
+ end
65
+ DB[:my_subtests].insert(id: 1, my_test_id: 1, name: 'First')
66
+ DB[:my_subtests].insert(id: 2, my_test_id: 1, name: 'Second')
67
+ MySubtestModel.new # Instantiate to force loading of data class
68
+
69
+ it 'delegates data class methods to the data class' do
70
+ (MyTestModel.respond_to?(:table_name)).should.be.true
71
+ MyTestModel.table_name.should.equal :my_tests
72
+ end
73
+
74
+ describe 'when instantiated' do
75
+ my_test_model = MyTestModel.new
76
+
77
+ it 'creates a data instance' do
78
+ my_test_model.data.class.should.equal MyTest
79
+ end
80
+
81
+ it 'delegates data methods to the data object' do
82
+ my_test_model.columns.should.equal [:id, :other_id, :name]
83
+ end
84
+
85
+ it 'still has access to methods defined on the model' do
86
+ my_test_model.test.should.be.equal :ok
87
+ end
88
+
89
+ it 'wraps delegated methods that return data class instances' do
90
+ a_test = MyTestModel.filter(name: 'Other').first
91
+ a_test.other.should.be.kind_of MyTestModel
92
+ end
93
+ end
94
+
95
+ it 'wraps a data object return value in a model object' do
96
+ res = MyTestModel.first
97
+ res.should.be.kind_of MyTestModel
98
+ end
99
+
100
+ it 'wraps a collection of data object return values in model objects' do
101
+ res = MyTestModel.all
102
+ res.should.be.kind_of Array
103
+ res.each { |i| i.should.be.kind_of MyTestModel }
104
+ end
105
+
106
+ it 'wraps return values from other model objects' do
107
+ test = MyTestModel.first
108
+ res = test.my_subtests
109
+ res.should.be.kind_of Array
110
+ res.each { |i| i.should.be.kind_of MySubtestModel }
111
+ end
112
+
113
+ it 'maps a hash return value to a hash' do
114
+ res = MyTestModel.new.associations
115
+ res.should.be.kind_of Hash
116
+ end
117
+
118
+ it 'delegates class methods with an argument' do
119
+ my_test_model = MyTestModel.create name: 'Another Test'
120
+ my_test_model.should.be.kind_of MyTestModel
121
+ MyTestModel.filter(name: 'Another Test').first.should.equal my_test_model
122
+ end
123
+ it 'should prevent calling :to_json inherited from Object' do
124
+ class Object
125
+ def to_json
126
+ raise "don't call me"
127
+ end
128
+ end
129
+ class MyTestModel < Peacekeeper::Model
130
+ end
131
+ class MyTest
132
+ def to_json
133
+ :ok
134
+ end
135
+ end
136
+ MyTestModel.new.to_json.should.equal :ok
137
+ -> { Object.new.to_json }.should.raise(RuntimeError)
138
+ end
139
+
140
+ it 'should allow redefinition of :to_json in the model' do
141
+ class Object
142
+ def to_json
143
+ raise "don't call me"
144
+ end
145
+ end
146
+ class MyTestModel < Peacekeeper::Model
147
+ def to_json
148
+ :ok
149
+ end
150
+ end
151
+ class MyTest
152
+ def to_json
153
+ :ko
154
+ end
155
+ end
156
+ MyTestModel.new.to_json.should.equal :ok
157
+ end
158
+
159
+ it 'can define methods that operate directly on the data class' do
160
+ class MyTestModel
161
+ def_data_method :others_first_subtest do
162
+ other.my_subtests.first
163
+ end
164
+ end
165
+ res = MyTestModel.filter(id: 2).first.others_first_subtest
166
+ res.should.be.kind_of MySubtestModel
167
+ res.name.should.equal 'First'
168
+ end
169
+
170
+ it 'can define methods that operate directly on the data class and takes arguments' do
171
+ class MyTestModel
172
+ def_data_method :others_nth_subtest do |n|
173
+ other.my_subtests[n]
174
+ end
175
+ end
176
+ res = MyTestModel.filter(id: 2).first.others_nth_subtest(1)
177
+ res.should.be.kind_of MySubtestModel
178
+ res.name.should.equal 'Second'
179
+ end
180
+
181
+ it 'can define class methods that operate directly on the data class' do
182
+ class MyTestModel
183
+ def_singleton_data_method :first_subtest do
184
+ first.my_subtests.first
185
+ end
186
+ end
187
+ res = MyTestModel.first_subtest
188
+ res.should.be.kind_of MySubtestModel
189
+ res.name.should.equal 'First'
190
+ end
191
+ end
192
+
193
+ # These are too close to testing the implementation with the implementation.
194
+ # We should probably either test URL construction indirectly or using mocks.
195
+ # TODO: Implement a test for config['options'] as well.
196
+ #describe 'database connection uri' do
197
+ # username = Peacekeeper::Model.config['username'] = 'username'
198
+ # password = Peacekeeper::Model.config['password'] = 'password'
199
+ # host = Peacekeeper::Model.config['host'] = 'localhost'
200
+ # database = Peacekeeper::Model.config['database'] = 'database'
201
+
202
+ # describe "when specified adapter is 'jdbc:mysql'" do
203
+ # it 'generate uri for jdbc:mysql' do
204
+ # adapter = Peacekeeper::Model.config['adapter'] = 'jdbc:mysql'
205
+ # Peacekeeper::Model.sequel_db_uri.should.eql "#{adapter}://#{host}/#{database}?user=#{username}&password=#{password}"
206
+ # end
207
+ # end
208
+
209
+ # describe "when specified adapter is not 'jdbc:mysql'" do
210
+ # it 'generate non-jdbc uri' do
211
+ # adapter = Peacekeeper::Model.config['adapter'] = 'mysql2'
212
+ # Peacekeeper::Model.sequel_db_uri.should.eql "#{adapter}://#{username}:#{password}@#{host}/#{database}"
213
+ # end
214
+ # end
215
+
216
+ # describe "when no adapter is specified" do
217
+ # it 'generate sqlite uri' do
218
+ # Peacekeeper::Model.config = {}
219
+ # Peacekeeper::Model.sequel_db_uri.should.eql 'sqlite:/'
220
+ # end
221
+ # end
222
+ #end
223
+ end
224
+
@@ -0,0 +1,115 @@
1
+ require_relative '../test_helper'
2
+
3
+ describe Peacekeeper::Model do
4
+ describe 'used to create a model subclass with Active Record' do
5
+ class MyTestArModel < Peacekeeper::Model
6
+ def test
7
+ :ok
8
+ end
9
+ end
10
+
11
+ class MySubtestArModel < Peacekeeper::Model; end
12
+
13
+ # Setup the DB and populate with some test data
14
+ Peacekeeper::Model.config['database'] = ':memory:'
15
+ Peacekeeper::Model.config[:protocol] = 'jdbc:sqlite:'
16
+ Peacekeeper::Model.data_source = :active_record
17
+
18
+ ActiveRecord::Base.connection.execute("CREATE TABLE my_test_ars (id INTEGER PRIMARY KEY, other_id INTEGER, name STRING);")
19
+ ActiveRecord::Base.connection.execute("CREATE TABLE my_subtest_ars (id INTEGER PRIMARY KEY, my_test_ar_id INTEGER, name STRING);")
20
+
21
+ ActiveRecord::Base.connection.execute("INSERT INTO my_test_ars (id,other_id,name) VALUES (1, 2, 'A Test');")
22
+ ActiveRecord::Base.connection.execute("INSERT INTO my_test_ars (id,other_id,name) VALUES (2, 1, 'Other');")
23
+ ActiveRecord::Base.connection.execute("INSERT INTO my_subtest_ars (id,my_test_ar_id,name) VALUES (1, 1, 'First');")
24
+ ActiveRecord::Base.connection.execute("INSERT INTO my_subtest_ars (id,my_test_ar_id,name) VALUES (2, 1, 'Second');")
25
+
26
+ it 'delegates data class methods to the data class' do
27
+ (MyTestArModel.respond_to?(:table_name)).should.be.true
28
+ MyTestArModel.table_name.should.equal "my_test_ars"
29
+ end
30
+
31
+ describe 'when instantiated' do
32
+ my_test_model = MyTestArModel.new
33
+
34
+ it 'creates a data instance' do
35
+ my_test_model.data.class.should.equal MyTestAr
36
+ end
37
+
38
+ it 'delegates data methods to the data object' do
39
+ my_test_model.attribute_names.should.equal ['id', 'other_id', 'name']
40
+ end
41
+
42
+ it 'still has access to methods defined on the model' do
43
+ my_test_model.test.should.be.equal :ok
44
+ end
45
+
46
+ it 'wraps delegated methods that return data class instances' do
47
+ a_test = MyTestArModel.where(name: 'Other').first
48
+ a_test.other.should.be.kind_of MyTestArModel
49
+ end
50
+ end
51
+
52
+ it 'wraps a data object return value in a model object' do
53
+ res = MyTestArModel.first
54
+ res.should.be.kind_of MyTestArModel
55
+ end
56
+
57
+ it 'wraps a collection of data object return values in model objects' do
58
+ res = MyTestArModel.all
59
+ res.should.be.kind_of Array
60
+ res.each { |i| i.should.be.kind_of MyTestArModel }
61
+ end
62
+
63
+ it 'wraps return values from other model objects' do
64
+ test = MyTestArModel.first
65
+ res = test.my_subtest_ars
66
+ res.should.be.kind_of Array
67
+ res.each { |i| i.should.be.kind_of MySubtestArModel }
68
+ end
69
+
70
+ it 'maps a hash return value to a hash' do
71
+ res = MyTestArModel.new.attributes
72
+ res.should.be.kind_of Hash
73
+ end
74
+
75
+ it 'delegates class methods with an argument' do
76
+ my_test_model = MyTestArModel.create name: 'Another Test'
77
+ my_test_model.should.be.kind_of MyTestArModel
78
+ MyTestArModel.where(name: 'Another Test').first.should.equal my_test_model
79
+ end
80
+
81
+ it 'can define methods that operate directly on the data class' do
82
+ class MyTestArModel
83
+ def_data_method :others_first_subtest do
84
+ other.my_subtest_ars.first
85
+ end
86
+ end
87
+ res = MyTestArModel.where(id: 2).first.others_first_subtest
88
+ res.should.be.kind_of MySubtestArModel
89
+ res.name.should.equal 'First'
90
+ end
91
+
92
+ it 'can define methods that operate directly on the data class and takes arguments' do
93
+ class MyTestArModel
94
+ def_data_method :others_nth_subtest do |n|
95
+ other.my_subtest_ars[n]
96
+ end
97
+ end
98
+ res = MyTestArModel.where(id: 2).first.others_nth_subtest(1)
99
+ res.should.be.kind_of MySubtestArModel
100
+ res.name.should.equal 'Second'
101
+ end
102
+
103
+ it 'can define class methods that operate directly on the data class' do
104
+ class MyTestArModel
105
+ def_singleton_data_method :first_subtest do
106
+ first.my_subtest_ars.first
107
+ end
108
+ end
109
+ res = MyTestArModel.first_subtest
110
+ res.should.be.kind_of MySubtestArModel
111
+ res.name.should.equal 'First'
112
+ end
113
+ end
114
+ end
115
+
@@ -0,0 +1,32 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ $LOAD_PATH.unshift File.expand_path('../fixtures', __FILE__)
3
+
4
+ require 'ruby-debug'
5
+ begin
6
+ require 'pry'
7
+ rescue LoadError
8
+ $stderr.puts 'Install the pry gem for better debugging.'
9
+ end
10
+ require 'facon'
11
+ require 'peacekeeper'
12
+
13
+ # Until JRuby fixes http://jira.codehaus.org/browse/JRUBY-6550 ...
14
+ class Should
15
+ def satisfy(*args, &block)
16
+ if args.size == 1 && String === args.first
17
+ description = args.shift
18
+ else
19
+ description = ""
20
+ end
21
+
22
+ # ToDo Jruby bug not yet resolved see http://jira.codehaus.org/browse/JRUBY-6550 Victor Christensen at 2:06 PM on 4/20/12
23
+ #r = yield(@object, *args)
24
+ r = yield(@object)
25
+ if Bacon::Counter[:depth] > 0
26
+ Bacon::Counter[:requirements] += 1
27
+ raise Bacon::Error.new(:failed, description) unless @negated ^ r
28
+ else
29
+ @negated ? !r : !!r
30
+ end
31
+ end
32
+ end
metadata CHANGED
@@ -1,52 +1,209 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: peacekeeper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
5
- prerelease:
4
+ version: 0.4.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Josh Ballanco
9
- autorequire:
8
+ autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-03-24 00:00:00.000000000 Z
13
- dependencies: []
14
- description: Using Peacekeeper, you can develop models separately from the ORM used
15
- to persist data.
11
+ date: 2013-10-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sequel
15
+ version_requirements: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 3.38.0
20
+ requirement: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: 3.38.0
25
+ prerelease: false
26
+ type: :runtime
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 3.2.6
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: 3.2.6
39
+ prerelease: false
40
+ type: :runtime
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord-jdbc-adapter
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 1.2.2
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ~>
51
+ - !ruby/object:Gem::Version
52
+ version: 1.2.2
53
+ prerelease: false
54
+ type: :runtime
55
+ - !ruby/object:Gem::Dependency
56
+ name: nasreddin
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '0.3'
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ version: '0.3'
67
+ prerelease: false
68
+ type: :runtime
69
+ - !ruby/object:Gem::Dependency
70
+ name: jdbc-sqlite3
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 3.7.2
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ~>
79
+ - !ruby/object:Gem::Version
80
+ version: 3.7.2
81
+ prerelease: false
82
+ type: :development
83
+ - !ruby/object:Gem::Dependency
84
+ name: bacon
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: 1.1.0
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ~>
93
+ - !ruby/object:Gem::Version
94
+ version: 1.1.0
95
+ prerelease: false
96
+ type: :development
97
+ - !ruby/object:Gem::Dependency
98
+ name: facon
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: 0.5.0
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ~>
107
+ - !ruby/object:Gem::Version
108
+ version: 0.5.0
109
+ prerelease: false
110
+ type: :development
111
+ - !ruby/object:Gem::Dependency
112
+ name: kramdown
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 0.13.7
118
+ requirement: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ~>
121
+ - !ruby/object:Gem::Version
122
+ version: 0.13.7
123
+ prerelease: false
124
+ type: :development
125
+ - !ruby/object:Gem::Dependency
126
+ name: yard
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: 0.8.2
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ~>
135
+ - !ruby/object:Gem::Version
136
+ version: 0.8.2
137
+ prerelease: false
138
+ type: :development
139
+ description: Using Peacekeeper, you can develop models separately from the ORM used to persist data.
16
140
  email:
17
141
  - jballanc@gmail.com
18
142
  executables: []
19
143
  extensions: []
20
144
  extra_rdoc_files: []
21
145
  files:
22
- - .gitignore
23
- - Gemfile
24
- - Rakefile
25
146
  - lib/peacekeeper.rb
147
+ - lib/peacekeeper/loader.rb
148
+ - lib/peacekeeper/model.rb
149
+ - lib/peacekeeper/model_delegation.rb
26
150
  - lib/peacekeeper/version.rb
27
- - peacekeeper.gemspec
151
+ - test/test_helper.rb
152
+ - test/fixtures/data/active_record/after_ar.rb
153
+ - test/fixtures/data/active_record/after_setting_ar.rb
154
+ - test/fixtures/data/active_record/before_ar.rb
155
+ - test/fixtures/data/active_record/before_setting_ar.rb
156
+ - test/fixtures/data/active_record/my_subtest_ar.rb
157
+ - test/fixtures/data/active_record/my_test_ar.rb
158
+ - test/fixtures/data/sequel/after.rb
159
+ - test/fixtures/data/sequel/after_setting.rb
160
+ - test/fixtures/data/sequel/before.rb
161
+ - test/fixtures/data/sequel/before_setting.rb
162
+ - test/fixtures/data/sequel/my_subtest.rb
163
+ - test/fixtures/data/sequel/my_test.rb
164
+ - test/peacekeeper/test_loader.rb
165
+ - test/peacekeeper/test_model.rb
166
+ - test/peacekeeper/test_orm_model.rb
167
+ - Rakefile
168
+ - README.md
28
169
  homepage: ''
29
170
  licenses: []
30
- post_install_message:
171
+ metadata: {}
172
+ post_install_message:
31
173
  rdoc_options: []
32
174
  require_paths:
33
175
  - lib
34
176
  required_ruby_version: !ruby/object:Gem::Requirement
35
- none: false
36
177
  requirements:
37
- - - ! '>='
178
+ - - '>='
38
179
  - !ruby/object:Gem::Version
39
180
  version: '0'
40
181
  required_rubygems_version: !ruby/object:Gem::Requirement
41
- none: false
42
182
  requirements:
43
- - - ! '>='
183
+ - - '>='
44
184
  - !ruby/object:Gem::Version
45
185
  version: '0'
46
186
  requirements: []
47
187
  rubyforge_project: peacekeeper
48
- rubygems_version: 1.8.15
49
- signing_key:
50
- specification_version: 3
188
+ rubygems_version: 2.1.10
189
+ signing_key:
190
+ specification_version: 4
51
191
  summary: Peacekeeper handles delegation to a variety of ORMs
52
- test_files: []
192
+ test_files:
193
+ - test/test_helper.rb
194
+ - test/fixtures/data/active_record/after_ar.rb
195
+ - test/fixtures/data/active_record/after_setting_ar.rb
196
+ - test/fixtures/data/active_record/before_ar.rb
197
+ - test/fixtures/data/active_record/before_setting_ar.rb
198
+ - test/fixtures/data/active_record/my_subtest_ar.rb
199
+ - test/fixtures/data/active_record/my_test_ar.rb
200
+ - test/fixtures/data/sequel/after.rb
201
+ - test/fixtures/data/sequel/after_setting.rb
202
+ - test/fixtures/data/sequel/before.rb
203
+ - test/fixtures/data/sequel/before_setting.rb
204
+ - test/fixtures/data/sequel/my_subtest.rb
205
+ - test/fixtures/data/sequel/my_test.rb
206
+ - test/peacekeeper/test_loader.rb
207
+ - test/peacekeeper/test_model.rb
208
+ - test/peacekeeper/test_orm_model.rb
209
+ has_rdoc:
data/.gitignore DELETED
@@ -1,4 +0,0 @@
1
- *.gem
2
- .bundle
3
- Gemfile.lock
4
- pkg/*
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source "http://rubygems.org"
2
-
3
- # Specify your gem's dependencies in peacekeeper.gemspec
4
- gemspec
data/peacekeeper.gemspec DELETED
@@ -1,24 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "peacekeeper/version"
4
-
5
- Gem::Specification.new do |s|
6
- s.name = "peacekeeper"
7
- s.version = Peacekeeper::VERSION
8
- s.authors = ["Josh Ballanco"]
9
- s.email = ["jballanc@gmail.com"]
10
- s.homepage = ""
11
- s.summary = %q{Peacekeeper handles delegation to a variety of ORMs}
12
- s.description = %q{Using Peacekeeper, you can develop models separately from the ORM used to persist data.}
13
-
14
- s.rubyforge_project = "peacekeeper"
15
-
16
- s.files = `git ls-files`.split("\n")
17
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
- s.require_paths = ["lib"]
20
-
21
- # specify any dependencies here; for example:
22
- # s.add_development_dependency "rspec"
23
- # s.add_runtime_dependency "rest-client"
24
- end