disposable 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 392f9c38f87256a9f23d4b15c5f1400ced554331
4
- data.tar.gz: 415113cee0b7f66c9cd6be8504ebb1c2cc5602d6
3
+ metadata.gz: a86781957ac37581fc914a2f8dc9ca83b67d47e1
4
+ data.tar.gz: 0a994524733877ec59edb02ce050272b447e9409
5
5
  SHA512:
6
- metadata.gz: debc2221e53dd47bb8029053b490f16dc5f52e6bbfd1ac268dba5e5b7702643061dac9fb1b868b34ca13a4825d5db82d564f4e5bb84a332ae0f41e3446e3204e
7
- data.tar.gz: cf1861a9c4c3cddcfb8d5a80821774b388ef8471940db2cc0c46b2609500985d8af5fa4d4f8456e9222c6b28a1b88d6192eb849fc66822b8b6108c11af83cef2
6
+ metadata.gz: 94b995707bba8d65c9361ae3c0b50cfbd23d2c72f2f0212791800cfe8e771a815b7cef80c4d305e9481a3e284959131cc304b3fc2a0fc97076e7d28435de3898
7
+ data.tar.gz: 6f7fcd2584e0b0d06b2d4615d9d7280d0791591837750c2d515fdb0445de3677e69d397de557d3fc05e181e8877046bb3fa8ae884e7d75c55f207af1f55c537a
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 1.9.3
5
+ - 1.8.7
6
+ gemfile:
7
+ - gemfiles/Gemfile.rails-3.0
8
+ - gemfiles/Gemfile.rails-2.3
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in disposable.gemspec
4
4
  gemspec
5
+
6
+ # gem 'representable', path: '../representable'
data/README.md CHANGED
@@ -1,6 +1,89 @@
1
1
  # Disposable
2
2
 
3
3
 
4
+
5
+ ## Twin
6
+
7
+ Twins implement light-weight domain objects that contain business logic - no persistance logic. They have read- and write access to a persistant object (or a composition of those) and expose a sub-set of accessors to that persistant "brother", making it a "data mapper-like phantom".
8
+
9
+ Being designed to wrap persistant objects, a typical "brother" class could be an ActiveRecord one.
10
+
11
+ ```ruby
12
+ class Song < ActiveRecord::Base
13
+ belongs_to :album
14
+ end
15
+ ```
16
+
17
+ You don't wanna work with that persistant brother directly anymore. A twin will wrap it and indirect domain from persistance.
18
+
19
+ The twin itself has a _ridiculous_ simple API.
20
+
21
+ ```ruby
22
+ class Twin::Song < Disposable::Twin
23
+ model ::Song # the persistant ActiveRecord brother class
24
+
25
+ property :title
26
+ property :album, twin: Album # a nested twin.
27
+ ```
28
+
29
+
30
+ ### Creation
31
+
32
+ You can create fresh, blank-slate twins yourself. They will create a new persistant object automatically.
33
+
34
+ ```ruby
35
+ song = Twin::Song.new(title: "Justified Black Eye")
36
+
37
+ #=> #<Twin::Song title: "Justified Black Eye">
38
+ ```
39
+
40
+ This doesn't involve any database operations at all.
41
+
42
+
43
+ ### Creation Using Finders
44
+
45
+ (to be implemented)
46
+ Every `Twin` subclass exposes all finders from the brother class. However, instances from the persistance layer are automatically twin'ed.
47
+
48
+ ```ruby
49
+ song = Twin::Song.find(1)
50
+
51
+ #=> #<Twin::Song title: "Already Won" album: #<Twin::Album>>
52
+ ```
53
+
54
+
55
+ ### Read And Write Access
56
+
57
+ All attributes declared with `property` are exposed on the twin.
58
+
59
+ ```ruby
60
+ song.title #=> "Already Won"
61
+
62
+ song.album.name #=> "The Songs of Tony Sly: A Tribute"
63
+ ```
64
+
65
+ Note that writing to the twin **does not** change any state on the persistant brother.
66
+
67
+ ```ruby
68
+ song.title = "Still Winning" # no change on Song, no write to DB.
69
+ ```
70
+
71
+
72
+ ### Saving
73
+
74
+ Calling `#save` will sync all properties to the brother object and advice the persistant brother to save. This works recursively, meaning that nested twins will do the same with their pendant.
75
+
76
+ ```ruby
77
+ song.save # write to DB.
78
+ ```
79
+
80
+ * Twins don't know anything about the underlying persistance layer.
81
+ * Lazy-loading
82
+
83
+
84
+
85
+ ## To be documented properly
86
+
4
87
  Facade existing (model) class
5
88
  where to decorate old instances from collections?
6
89
  option.invoice_template => .items
@@ -18,6 +101,16 @@ class Invoice
18
101
  end
19
102
 
20
103
 
104
+ Why facades?
105
+ you don't wanna add code to existing shit
106
+ transparently change the API
107
+ you don't have to worry about what's happening in the underlying pile of shit (GstCalculations module), you only correct the API on top of it
108
+ no inheritance, composition whereever possible (interfaces between layers)
109
+ optional, still use old assets
110
+ don't/step-wise change existing "running" code
111
+
112
+ you basically don't change existing code but build extracted components on top of your legacy app
113
+
21
114
  * facades
22
115
  * overriding public methods in facade
23
116
  * temporary Refinements
@@ -25,11 +118,49 @@ class Invoice
25
118
  * steps of refactoring
26
119
 
27
120
 
121
+ "partial refactoring"
28
122
  "explicit refactoring"
29
123
 
30
124
 
125
+ * make it simple to split existing model into smaller (reducing validations etc)
126
+ * mark dead code
127
+ * by explicit use of `facade` you can track "dirt"
128
+
129
+ * rename options => DSL
31
130
 
32
131
  TODO: Write a gem description
132
+ * generator for Facades
133
+ loading of facades?
134
+ location of facades? i wanna have Facade::Client, not facades/ClientFacade.
135
+ FACADE keeps all configuration in one place (like a new has_many), also you can track which methods you actually need in your data model. this wouldn't be possible that easy with inheritance.
136
+
137
+ Facadable#facade
138
+ Facade#facaded
139
+
140
+
141
+ idea: collect # REFAC lines
142
+
143
+
144
+ running with Rails 2.3->4.0, 1.8.7 ..as that makes sense
145
+
146
+
147
+ ## Refinement
148
+
149
+ injected into instance _after_ construction
150
+
151
+ ## Build
152
+
153
+
154
+
155
+ FacadeClass
156
+ extend Build
157
+
158
+ module ClassMethods
159
+ def initializer
160
+ def another_class_method # FIXME: does that work?
161
+
162
+ make anonymous class, allow overriding initializer and (works?) add class methods.
163
+
33
164
 
34
165
  ## Installation
35
166
 
data/Rakefile CHANGED
@@ -1 +1,9 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task :default => [:test]
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'test'
7
+ test.test_files = FileList['test/**/*_test.rb']
8
+ test.verbose = true
9
+ end
data/STUFF CHANGED
@@ -1,4 +1,11 @@
1
1
  USE CASE:
2
2
 
3
3
  <% client.invoices.outstanding.each do |invoice| %>
4
- <%= link_to invoice.invoice_number, {:controller => "party", :action => "invoice_show", :id => invoice.id, :entity_instance_id => client.party, :entity_type_id => EntityType::PARTY}, :onclick => "event.cancelBubble=true;" %> <%= number_to_currency(invoice.balance_out)
4
+ <%= link_to invoice.invoice_number, {:controller => "party", :action => "invoice_show", :id => invoice.id, :entity_instance_id => client.party, :entity_type_id => EntityType::PARTY}, :onclick => "event.cancelBubble=true;" %> <%= number_to_currency(invoice.balance_out)
5
+
6
+
7
+ Invoice::Option::Rate <-- uses option everywhere
8
+ make that declaratively hook-able:
9
+ belongs_to Invoice::Option
10
+
11
+ Invoice::Option#rate => Rate.new(self)
data/database.sqlite3 ADDED
Binary file
data/disposable.gemspec CHANGED
@@ -18,6 +18,12 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_dependency "uber"
22
+ spec.add_dependency "representable"
23
+
21
24
  spec.add_development_dependency "bundler", "~> 1.3"
22
25
  spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "minitest"
27
+ spec.add_development_dependency "activerecord"
28
+ spec.add_development_dependency "sqlite3"
23
29
  end
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in reform.gemspec
4
+ gemspec :path => '../'
5
+
6
+ gem 'rails', '2.3.18'
@@ -0,0 +1,41 @@
1
+ PATH
2
+ remote: /home/nick/projects/disposable
3
+ specs:
4
+ disposable (0.0.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ actionmailer (2.3.18)
10
+ actionpack (= 2.3.18)
11
+ actionpack (2.3.18)
12
+ activesupport (= 2.3.18)
13
+ rack (~> 1.1.0)
14
+ activerecord (2.3.18)
15
+ activesupport (= 2.3.18)
16
+ activeresource (2.3.18)
17
+ activesupport (= 2.3.18)
18
+ activesupport (2.3.18)
19
+ minitest (5.0.8)
20
+ rack (1.1.6)
21
+ rails (2.3.18)
22
+ actionmailer (= 2.3.18)
23
+ actionpack (= 2.3.18)
24
+ activerecord (= 2.3.18)
25
+ activeresource (= 2.3.18)
26
+ activesupport (= 2.3.18)
27
+ rake (>= 0.8.3)
28
+ rake (10.1.0)
29
+ sqlite3 (1.3.8)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ activerecord
36
+ bundler (~> 1.3)
37
+ disposable!
38
+ minitest
39
+ rails (= 2.3.18)
40
+ rake
41
+ sqlite3
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in reform.gemspec
4
+ gemspec :path => '../'
5
+
6
+ gem 'railties', '~> 3.0.11'
7
+ gem 'activerecord', '~> 3.0.11'
@@ -0,0 +1,65 @@
1
+ PATH
2
+ remote: /home/nick/projects/disposable
3
+ specs:
4
+ disposable (0.0.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ abstract (1.0.0)
10
+ actionpack (3.0.20)
11
+ activemodel (= 3.0.20)
12
+ activesupport (= 3.0.20)
13
+ builder (~> 2.1.2)
14
+ erubis (~> 2.6.6)
15
+ i18n (~> 0.5.0)
16
+ rack (~> 1.2.5)
17
+ rack-mount (~> 0.6.14)
18
+ rack-test (~> 0.5.7)
19
+ tzinfo (~> 0.3.23)
20
+ activemodel (3.0.20)
21
+ activesupport (= 3.0.20)
22
+ builder (~> 2.1.2)
23
+ i18n (~> 0.5.0)
24
+ activerecord (3.0.20)
25
+ activemodel (= 3.0.20)
26
+ activesupport (= 3.0.20)
27
+ arel (~> 2.0.10)
28
+ tzinfo (~> 0.3.23)
29
+ activesupport (3.0.20)
30
+ arel (2.0.10)
31
+ builder (2.1.2)
32
+ erubis (2.6.6)
33
+ abstract (>= 1.0.0)
34
+ i18n (0.5.0)
35
+ json (1.8.0)
36
+ minitest (5.0.8)
37
+ rack (1.2.8)
38
+ rack-mount (0.6.14)
39
+ rack (>= 1.0.0)
40
+ rack-test (0.5.7)
41
+ rack (>= 1.0)
42
+ railties (3.0.20)
43
+ actionpack (= 3.0.20)
44
+ activesupport (= 3.0.20)
45
+ rake (>= 0.8.7)
46
+ rdoc (~> 3.4)
47
+ thor (~> 0.14.4)
48
+ rake (10.1.0)
49
+ rdoc (3.12.2)
50
+ json (~> 1.4)
51
+ sqlite3 (1.3.8)
52
+ thor (0.14.6)
53
+ tzinfo (0.3.38)
54
+
55
+ PLATFORMS
56
+ ruby
57
+
58
+ DEPENDENCIES
59
+ activerecord (~> 3.0.11)
60
+ bundler (~> 1.3)
61
+ disposable!
62
+ minitest
63
+ railties (~> 3.0.11)
64
+ rake
65
+ sqlite3
data/lib/disposable.rb CHANGED
@@ -1,7 +1,12 @@
1
1
  require "disposable/version"
2
2
 
3
3
  module Disposable
4
- # Your code goes here...
4
+ autoload :Twin, 'disposable/twin'
5
+ autoload :Composition, 'disposable/composition'
5
6
  end
6
7
 
7
- require "disposable/facade"
8
+ require "disposable/facade"
9
+
10
+ if defined?(ActiveRecord)
11
+ require 'disposable/facade/active_record'
12
+ end
@@ -0,0 +1,48 @@
1
+ module Disposable
2
+ # Composition delegates accessors to models as per configuration.
3
+ #
4
+ # class Album
5
+ # include Disposable::Composition
6
+
7
+ # map( {cd: [:id, :name], band: [:title]} )
8
+ # end
9
+
10
+ # album = Album.new(cd: CD.find(1), band: Band.new)
11
+ # album.id #=> 1
12
+ # album.title = "Ten Foot Pole"
13
+ module Composition
14
+ def self.included(base)
15
+ base.extend(ClassMethods)
16
+ end
17
+
18
+ module ClassMethods
19
+ def map(options)
20
+ @attr2obj = {} # {song: ["title", "track"], artist: ["name"]}
21
+
22
+ options.each do |mdl, meths|
23
+ create_accessors(mdl, meths)
24
+ attr_reader mdl
25
+
26
+ meths.each { |m| @attr2obj[m.to_s] = mdl }
27
+ end
28
+ end
29
+
30
+ def create_accessors(model, methods)
31
+ accessors = methods.collect { |m| [m, "#{m}="] }.flatten
32
+ delegate *accessors << {:to => :"#{model}"}
33
+ end
34
+ end
35
+
36
+
37
+ private
38
+ def initialize(models)
39
+ models.each do |name, obj|
40
+ instance_variable_set(:"@#{name}", obj)
41
+ end
42
+
43
+ @_models = models.values
44
+ end
45
+
46
+ attr_reader:_models
47
+ end
48
+ end
@@ -1,25 +1,94 @@
1
1
  require "delegate"
2
+ require "uber/inheritable_attr"
2
3
 
3
4
  module Disposable
4
5
  class Facade < SimpleDelegator
6
+ extend Uber::InheritableAttr
7
+ inheritable_attr :facade_options
8
+ self.facade_options = [nil, {}]
9
+
10
+
5
11
  module Facadable
6
- def facade
12
+ # used in facaded.
13
+ def facade!(facade_class=nil)
14
+ facade_class ||= self.class.facade_class
15
+ facade_class.facade!(self)
16
+ end
17
+
18
+ def facade(facade_class=nil)
19
+ facade_class ||= self.class.facade_class
20
+ facade_class.facade(self)
21
+ end
22
+ end
23
+
24
+
25
+ class << self
26
+ def facades(klass, options={})
27
+ facade_options = [klass, options] # TODO: test.
28
+
29
+ self.facade_options = facade_options
30
+
31
+ facade_class = self
32
+ klass.instance_eval do
33
+ include Facadable
34
+ @_facade_class = facade_class
35
+
36
+ def facade_class
37
+ @_facade_class
38
+ end
39
+ end # TODO: use hooks.
40
+ end
41
+
42
+ # TODO: move both into Facader instance.
43
+ def facade(facaded)
44
+ if facade_options.last[:if]
45
+ return facaded unless facade_options.last[:if].call(facaded)
46
+ end
47
+
7
48
  # TODO: check if already facaded.
8
- self.class.facade_class.new(self)
49
+ facade!(facaded)
9
50
  end
51
+
52
+ def facade!(facaded)
53
+ new(facaded)
54
+ end
55
+ end
56
+
57
+ # Forward #id to facaded. this is only a concern in 1.8.
58
+ def id
59
+ __getobj__.id
10
60
  end
11
61
 
12
- def self.facades(klass)
13
- facade_class = self
14
62
 
15
- klass.instance_eval do
16
- include Facadable
17
- @_facade_class = facade_class
63
+ alias_method :facaded, :__getobj__
64
+
65
+
66
+ # Extend your facade and call Song.build, includes ClassMethods (extend constructor).
67
+ module Subclass
68
+ def build(*args)
69
+ facade_class = self
70
+ Class.new(facade_options.first).class_eval do
71
+ include facade_class::InstanceMethods # TODO: check if exists.
72
+ extend facade_class::ClassMethods # TODO: check if exists.
73
+
74
+ self
75
+ end.new(*args)
76
+ end
77
+
78
+ alias_method :subclass, :build
79
+ end
80
+
18
81
 
19
- def facade_class
20
- @_facade_class
82
+ module Refine
83
+ def initialize(facaded) # DISCUSS: should we override ::facade here?
84
+ super.tap do |res|
85
+ refine(facaded)
21
86
  end
22
- end # TODO: use hooks.
87
+ end
88
+
89
+ def refine(facaded)
90
+ facaded.extend(self.class::Refinements)
91
+ end
23
92
  end
24
93
  end
25
94
  end
@@ -0,0 +1,11 @@
1
+ class Disposable::Facade
2
+ module ActiveRecord
3
+ def is_a?(klass)
4
+ # DISCUSS: should we use facade_options here for the class?
5
+ #return self.class.facade_options.first == klass if self.class.facade_options.first
6
+ # DISCUSS: make ::facades obligatory?
7
+
8
+ klass == __getobj__.class or super
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,135 @@
1
+ require 'uber/inheritable_attr'
2
+ require 'representable/decorator'
3
+ require 'representable/hash'
4
+
5
+ module Disposable
6
+ class Twin
7
+ class Decorator < Representable::Decorator
8
+ include Representable::Hash
9
+ include AllowSymbols
10
+
11
+ # DISCUSS: same in reform, is that a bug in represntable?
12
+ def self.clone # called in inheritable_attr :representer_class.
13
+ Class.new(self) # By subclassing, representable_attrs.clone is called.
14
+ end
15
+
16
+ def self.build_config
17
+ super.extend(ConfigExtensions)
18
+ end
19
+
20
+ def self.twin_names
21
+ representable_attrs.twin_names
22
+ end
23
+
24
+ def twins(&block)
25
+ clone_config!.
26
+ find_all { |attr| attr[:form] }.
27
+ each(&block)
28
+ end
29
+
30
+ module ConfigExtensions
31
+ def twin_names
32
+ find_all { |attr| attr[:twin] }.
33
+ collect { |attr| attr.name }
34
+ end
35
+ end
36
+
37
+
38
+ class Save < self
39
+
40
+ end
41
+ end
42
+
43
+
44
+
45
+
46
+ extend Uber::InheritableAttr
47
+ inheritable_attr :representer_class
48
+ self.representer_class = Class.new(Decorator)
49
+
50
+ inheritable_attr :_model
51
+
52
+ def self.model(name)
53
+ self._model = name
54
+ end
55
+
56
+ def self.property(name, *args, &block)
57
+ attr_accessor name
58
+
59
+ representer_class.property(name, *args, &block)
60
+ end
61
+
62
+ def self.from(model) # TODO: private.
63
+ new(model)
64
+ end
65
+
66
+ def self.new(model=nil)
67
+ model, options = nil, model if model.is_a?(Hash) # sorry but i wanna have the same API as ActiveRecord here.
68
+ super(model || _model.new, *[options].compact) # TODO: make this nicer.
69
+ end
70
+
71
+ def self.find(id)
72
+ new(_model.find(id))
73
+ end
74
+
75
+ def self.save_representer
76
+ # TODO: do that only at compile-time!
77
+ save = Class.new(representer_class) # inherit configuration
78
+ save.representable_attrs.
79
+ find_all { |attr| attr[:twin] }.
80
+ each { |attr| attr.merge!(
81
+ :representable => true) }
82
+ save
83
+ end
84
+
85
+ def self.new_representer
86
+ representer = Class.new(representer_class) # inherit configuration
87
+ representer.representable_attrs.
88
+ find_all { |attr| attr[:twin] }.
89
+ each { |attr| attr.merge!(
90
+ :pass_options => true,
91
+ :prepare => lambda { |object, args| args.binding[:twin].new(object) }) }
92
+ representer
93
+ end
94
+
95
+
96
+ def to_hash(*) # DISCUSS: do we want that here?
97
+ model
98
+ end
99
+
100
+ # it's important to stress that #save is the only entry point where we hit the database after initialize.
101
+ def save # use that in Reform::AR.
102
+ twin_names = self.class.representer_class.twin_names
103
+
104
+ raw_attrs = self.class.representer_class.new(self).to_hash
105
+ save_attrs = raw_attrs.select { |k| twin_names.include?(k) }
106
+ save_attrs.values.map(&:save)
107
+
108
+
109
+ sync_attrs = self.class.save_representer.new(self).to_hash
110
+ # this is ORM-specific:
111
+ model.update_attributes(sync_attrs) # this also does `album: #<Album>`
112
+
113
+ # FIXME: sync again, here, or just id?
114
+ self.id = model.id
115
+ end
116
+
117
+ # below is the code for a representable-style twin:
118
+
119
+ # TODO: improve speed when setting up a twin.
120
+ def initialize(model, options={})
121
+ @model = model
122
+
123
+ # DISCUSS: does the case exist where we get model AND options? if yes, test. if no, we can save the mapping and just use options.
124
+ from_hash(self.class.new_representer.new(model).to_hash.
125
+ merge(options))
126
+ end
127
+
128
+ private
129
+ def from_hash(options={})
130
+ self.class.representer_class.new(self).from_hash(options)
131
+ end
132
+
133
+ attr_reader :model # TODO: test
134
+ end
135
+ end
@@ -1,3 +1,3 @@
1
1
  module Disposable
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,80 @@
1
+ require 'test_helper'
2
+
3
+ require 'active_record'
4
+ class Invoice < ActiveRecord::Base
5
+ has_many :invoice_items
6
+ end
7
+
8
+ class InvoiceItem < ActiveRecord::Base
9
+ belongs_to :invoice
10
+ end
11
+
12
+ ActiveRecord::Base.establish_connection(
13
+ :adapter => "sqlite3",
14
+ :database => "#{Dir.pwd}/database.sqlite3"
15
+ )
16
+
17
+ # ActiveRecord::Schema.define do
18
+ # create_table :invoices do |table|
19
+ # table.timestamps
20
+ # end
21
+ # end
22
+
23
+ # ActiveRecord::Schema.define do
24
+ # create_table :invoice_items do |table|
25
+ # table.column :invoice_id, :string
26
+ # table.timestamps
27
+ # end
28
+ # end
29
+
30
+ # TODO: test auto-loading of Rails assets.
31
+ require 'disposable/facade/active_record'
32
+
33
+ class ActiveRecordAssociationsTest < MiniTest::Spec
34
+ class Item < Disposable::Facade
35
+ facades InvoiceItem
36
+
37
+ include Disposable::Facade::ActiveRecord
38
+ end
39
+
40
+ let (:invoice) { Invoice.new }
41
+ it "allows adding facades to associations" do
42
+ # tests #is_a?
43
+ InvoiceItem.new.facade.class.must_equal Item
44
+ InvoiceItem.new.facade.is_a?(InvoiceItem).must_equal true
45
+
46
+ invoice.invoice_items << InvoiceItem.new.facade
47
+ end
48
+
49
+
50
+ class InvoiceFacade < Disposable::Facade
51
+ facades ::Invoice
52
+
53
+ include Disposable::Facade::ActiveRecord
54
+ #has_many :items, :class_name => ::InvoiceItem, :foreign_key_name => :invoice_item_id
55
+
56
+ module InstanceMethods # IncludeMethods, Included
57
+ extend ActiveSupport::Concern
58
+ included do
59
+ has_many :items, :class_name => ::InvoiceItem, :foreign_key => :invoice_item_id
60
+
61
+ def self.name
62
+ "anon"
63
+ end
64
+
65
+ end
66
+ end
67
+ module ClassMethods # ExtendMethods, Extended
68
+ end
69
+
70
+ extend Disposable::Facade::Subclass
71
+ end
72
+
73
+ it "what" do
74
+ invoice = InvoiceFacade.subclass
75
+ invoice.items << item = InvoiceItem.new
76
+ # TODO: test items << Facade::Item.new
77
+
78
+ invoice.items.must_equal([item])
79
+ end
80
+ end
@@ -0,0 +1,128 @@
1
+ require 'test_helper'
2
+
3
+ class FacadeTest < MiniTest::Spec
4
+ class Track < ::Track
5
+ end
6
+
7
+ class Song < Disposable::Facade
8
+ facades Track
9
+ end
10
+
11
+ class Hit < Disposable::Facade
12
+ end
13
+
14
+ let (:track) { Track.new }
15
+
16
+ it { track.class.must_equal Track }
17
+ it { track.facade.class.must_equal Song }
18
+
19
+ it "allows passing facade name" do # FIXME: what if track doesn't have Facadable?
20
+ track.facade(Hit).class.must_equal Hit
21
+ end
22
+ # DISCUSS: should this be Hit.facade(track) ?
23
+
24
+ describe "::facade" do
25
+ it { Hit.facade(track).class.must_equal Hit }
26
+ end
27
+
28
+ it "facades only once" do
29
+ end
30
+
31
+ describe "#id" do
32
+ let (:track) do Track.new.instance_eval {
33
+ def id; 1; end
34
+ self }
35
+ end
36
+
37
+ # DISCUSS: this actually tests Facadable.
38
+ it "calls original" do
39
+ track.facade.id.must_equal 1
40
+ end
41
+ end
42
+
43
+ it "responds to #facaded" do
44
+ Song.new(facaded = Object.new).facaded.must_equal facaded
45
+ end
46
+ end
47
+
48
+ class FacadesWithOptionsTest < MiniTest::Spec
49
+ class Track < ::Track
50
+ end
51
+
52
+ class Song < Disposable::Facade
53
+ facades Track, :if => lambda { |t| t.title }
54
+ end
55
+
56
+ it { Track.new(:title => "Trudging").facade.class.must_equal Song }
57
+ it { Track.new.facade.class.must_equal Track }
58
+
59
+ describe "#facade!" do
60
+ it "ignores :if" do
61
+ Track.new.facade!.class.must_equal Song
62
+ end
63
+ end
64
+ end
65
+
66
+ class SubclassTest < MiniTest::Spec
67
+ class Track
68
+ attr_reader :title
69
+
70
+ def initialize(options)
71
+ @title = options[:title] # we only save this key.
72
+ raise "rename didn't work" if options[:name]
73
+ end
74
+ end
75
+
76
+ #require 'disposable/facade/active_record'
77
+ class Song < Disposable::Facade
78
+ facades Track
79
+ # has_one :album
80
+ # rename_options
81
+
82
+ extend Subclass # do ... end instead of ClassMethods.
83
+ #include Disposable::Facade::ActiveRecord
84
+
85
+ module InstanceMethods
86
+ # DISCUSS: this could be initializer do .. end
87
+ def initialize(options)
88
+ options[:title ] = options.delete(:name)
89
+ super
90
+ end
91
+ end
92
+ module ClassMethods
93
+
94
+ end
95
+ end
96
+
97
+ #it { Track.facade(Song).new(:name => "Bombs Away").title.must_equal "Bombs Away" }
98
+ it { Song.subclass(:name => "Bombs Away").title.must_equal "Bombs Away" }
99
+ # it "what" do
100
+ # Song.build(:name => "Bombs Away").facade(Song).is_a?(Track).must_equal true
101
+ # end
102
+ end
103
+
104
+
105
+ class RefinementsTest < MiniTest::Spec
106
+ class Track
107
+ def length
108
+ duration.round(2)
109
+ end
110
+
111
+ def duration
112
+ 4.321
113
+ end
114
+ end
115
+
116
+ class Song < Disposable::Facade
117
+ facades Track
118
+ include Disposable::Facade::Refine
119
+
120
+ module Refinements
121
+ def duration
122
+ 5.309
123
+ end
124
+ end
125
+ end
126
+
127
+ it { Track.new.facade.length.must_equal 5.31 }
128
+ end
@@ -0,0 +1,10 @@
1
+ require 'disposable'
2
+ require 'minitest/autorun'
3
+
4
+ class Track
5
+ def initialize(options={})
6
+ @title = options[:title]
7
+ end
8
+
9
+ attr_reader :title
10
+ end
@@ -0,0 +1,104 @@
1
+ require 'test_helper'
2
+
3
+ require 'active_record'
4
+ class Album < ActiveRecord::Base
5
+ has_many :songs
6
+ end
7
+
8
+ class Song < ActiveRecord::Base
9
+ belongs_to :album
10
+ end
11
+
12
+ ActiveRecord::Base.establish_connection(
13
+ :adapter => "sqlite3",
14
+ :database => "#{Dir.pwd}/database.sqlite3"
15
+ )
16
+
17
+ # ActiveRecord::Schema.define do
18
+ # create_table :songs do |table|
19
+ # table.column :title, :string
20
+ # table.column :album_id, :integer
21
+ # table.timestamps
22
+ # end
23
+
24
+ # create_table :albums do |table|
25
+ # table.column :name, :string
26
+ # table.timestamps
27
+ # end
28
+ # end
29
+
30
+
31
+ class TwinActiveRecordTest < MiniTest::Spec
32
+ module Twin
33
+ class Album < Disposable::Twin
34
+ property :id
35
+ property :name
36
+
37
+ model ::Album
38
+ end
39
+
40
+ class Song < Disposable::Twin
41
+ property :id
42
+ property :title
43
+ property :album, :twin => Album#, representable: true #, setter: lambda { |v, args| self.album=(Album.from(v)) }
44
+
45
+ model ::Song
46
+ end
47
+ end
48
+
49
+
50
+
51
+ describe "::find" do
52
+ let (:song_model) { ::Song.create(:title => "Savage") }
53
+ subject { Twin::Song.find(song_model.id) }
54
+
55
+ it { subject.id.must_equal song_model.id }
56
+ it { subject.title.must_equal "Savage" }
57
+ it { subject.album.must_equal nil }
58
+ end
59
+
60
+
61
+
62
+ describe "::save, nested not set" do
63
+ let (:twin) { Twin::Song.new(:title => "1.80 Down") }
64
+ before { twin.save }
65
+ subject { ::Song.find(twin.id) }
66
+
67
+ it { subject.attributes.slice("id", "title").
68
+ must_equal({"id" => subject.id, "title" => "1.80 Down"}) }
69
+
70
+ it { subject.album.must_equal nil }
71
+ end
72
+
73
+
74
+ describe "::save, nested present" do
75
+ let (:song) { ::Song.new(:title => "Broken", :album => album) }
76
+ let (:album) { ::Album.new(:name => "The Process Of Belief") }
77
+
78
+ let(:twin) { Twin::Song.from(song) }
79
+
80
+ before { twin.save } # propagate album.save
81
+
82
+ subject { ::Song.find(twin.id) }
83
+
84
+ it { subject.attributes.slice("id", "title").
85
+ must_equal({"id" => subject.id, "title" => "Broken"}) }
86
+
87
+ it { subject.album.must_equal album }
88
+ end
89
+
90
+
91
+ describe "::save, nested new" do
92
+ let(:twin) { Twin::Song.new(:title => "How It Goes", :album => Twin::Album.new(:name => "Billy Talent")) }
93
+
94
+ before { twin.save } # propagate album.save
95
+
96
+ subject { ::Song.find(twin.id) }
97
+
98
+ it {
99
+ subject.attributes.slice("id", "title").
100
+ must_equal({"id" => subject.id, "title" => "How It Goes"}) }
101
+
102
+ it { subject.album.attributes.slice("name").must_equal("name" => "Billy Talent") }
103
+ end
104
+ end
@@ -0,0 +1,50 @@
1
+ require 'test_helper'
2
+
3
+ class CompositionTest < MiniTest::Spec
4
+ module Model
5
+ Band = Struct.new(:id, :title,)
6
+ Album = Struct.new(:id, :name)
7
+ end
8
+
9
+ module Twin
10
+ class Album #< Disposable::Twin
11
+ include Disposable::Composition
12
+ extend Disposable::Composition::ClassMethods # FIXME.
13
+
14
+ # property :id # DISCUSS: needed for #save.
15
+ # property :name, :on => :album
16
+ # property :title, :on => :band # as: :band_name
17
+
18
+ # model Model::Album
19
+
20
+ map( {:album => [:id, :name], :band => [:title]} )
21
+ end
22
+ end
23
+
24
+ let (:band) { Model::Band.new(1, "Frenzal Rhomb") }
25
+ let (:album) { Model::Album.new(2, "Dick Sandwhich") }
26
+ subject { Twin::Album.new(:album => album, :band => band) }
27
+
28
+ # it { subject.id.must_equal 2 }
29
+ it { subject.name.must_equal "Dick Sandwhich" }
30
+ it { subject.title.must_equal "Frenzal Rhomb" }
31
+
32
+ # it { subject.save }
33
+
34
+ it "raises when non-mapped property" do
35
+ assert_raises NoMethodError do
36
+ subject.raise_an_exception
37
+ end
38
+ end
39
+
40
+ describe "readers to models" do
41
+ it { subject.album.object_id.must_equal album.object_id }
42
+ it { subject.band.object_id.must_equal band.object_id }
43
+ end
44
+
45
+
46
+ describe "#_models" do
47
+ it { subject.send(:_models).must_equal([album, band]) }
48
+ it { Twin::Album.new(:album => album).send(:_models).must_equal([album]) }
49
+ end
50
+ end
@@ -0,0 +1,79 @@
1
+ require 'test_helper'
2
+
3
+
4
+ class TwinTest < MiniTest::Spec
5
+ module Model
6
+ Song = Struct.new(:id, :title, :album)
7
+ Album = Struct.new(:id, :name)
8
+ end
9
+
10
+
11
+ module Twin
12
+ class Album < Disposable::Twin
13
+ property :id # DISCUSS: needed for #save.
14
+ property :name
15
+
16
+ model Model::Album
17
+ end
18
+
19
+ class Song < Disposable::Twin
20
+ property :id # DISCUSS: needed for #save.
21
+ property :title
22
+ property :album, :twin => Album #, setter: lambda { |v, args| self.album=(Album.from(v)) }
23
+
24
+ model Model::Song
25
+ end
26
+ end
27
+
28
+
29
+ describe "::new" do # TODO: this creates a new model!
30
+ subject { Twin::Song.new }
31
+
32
+ it { subject.title.must_equal nil }
33
+ it { subject.album.must_equal nil }
34
+ end
35
+
36
+
37
+ describe "::new with arguments" do
38
+ let (:album) { Twin::Album.new(:name => "30 Years") }
39
+ subject { Twin::Song.new("title" => "Broken", "album" => album) }
40
+
41
+ it { subject.title.must_equal "Broken" }
42
+ it { subject.album.must_equal album }
43
+ it { subject.album.name.must_equal "30 Years" }
44
+ end
45
+
46
+
47
+ describe "::new with :symbols" do
48
+ subject { Twin::Song.new(:title => "Broken") }
49
+
50
+ it { subject.title.must_equal "Broken" }
51
+ it { subject.album.must_equal nil }
52
+ end
53
+
54
+
55
+ # DISCUSS: make ::from private.
56
+ describe "::from" do
57
+ let (:song) { Model::Song.new(1, "Broken", album) }
58
+ let (:album) { Model::Album.new(2, "The Process Of Belief") }
59
+
60
+ subject {Twin::Song.from(song) }
61
+
62
+ it { subject.title.must_equal "Broken" }
63
+ it { subject.album.must_be_kind_of Twin::Album }
64
+ it { subject.album.name.must_equal album.name }
65
+ end
66
+ end
67
+
68
+
69
+ class TwinDecoratorTest < MiniTest::Spec
70
+ subject { TwinTest::Twin::Song.representer_class }
71
+
72
+ it { subject.twin_names.must_equal ["album"] }
73
+ end
74
+
75
+ # from is as close to from_hash as possible
76
+ # there should be #to in a perfect API, nothing else.
77
+
78
+
79
+ # should #new create empty associated models?
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: disposable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Sutterer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-11-20 00:00:00.000000000 Z
11
+ date: 2014-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: uber
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: representable
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: bundler
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +66,48 @@ dependencies:
38
66
  - - '>='
39
67
  - !ruby/object:Gem::Version
40
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activerecord
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
41
111
  description: Domain-Oriented Refactoring Framework.
42
112
  email:
43
113
  - apotonick@gmail.com
@@ -46,15 +116,30 @@ extensions: []
46
116
  extra_rdoc_files: []
47
117
  files:
48
118
  - .gitignore
119
+ - .travis.yml
49
120
  - Gemfile
50
121
  - LICENSE.txt
51
122
  - README.md
52
123
  - Rakefile
53
124
  - STUFF
125
+ - database.sqlite3
54
126
  - disposable.gemspec
127
+ - gemfiles/Gemfile.rails-2.3
128
+ - gemfiles/Gemfile.rails-2.3.lock
129
+ - gemfiles/Gemfile.rails-3.0
130
+ - gemfiles/Gemfile.rails-3.0.lock
55
131
  - lib/disposable.rb
132
+ - lib/disposable/composition.rb
56
133
  - lib/disposable/facade.rb
134
+ - lib/disposable/facade/active_record.rb
135
+ - lib/disposable/twin.rb
57
136
  - lib/disposable/version.rb
137
+ - test/active_record_test.rb
138
+ - test/facade_test.rb
139
+ - test/test_helper.rb
140
+ - test/twin/active_record_test.rb
141
+ - test/twin/composition_test.rb
142
+ - test/twin/twin_test.rb
58
143
  homepage: ''
59
144
  licenses:
60
145
  - MIT
@@ -75,8 +160,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
160
  version: '0'
76
161
  requirements: []
77
162
  rubyforge_project:
78
- rubygems_version: 2.0.3
163
+ rubygems_version: 2.2.2
79
164
  signing_key:
80
165
  specification_version: 4
81
166
  summary: Domain-Oriented Refactoring Framework.
82
- test_files: []
167
+ test_files:
168
+ - test/active_record_test.rb
169
+ - test/facade_test.rb
170
+ - test/test_helper.rb
171
+ - test/twin/active_record_test.rb
172
+ - test/twin/composition_test.rb
173
+ - test/twin/twin_test.rb