disposable 0.0.1 → 0.0.2

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