schizo 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/.travis.yml +4 -0
- data/CHANGELOG +5 -0
- data/Gemfile +4 -0
- data/README.rdoc +175 -0
- data/Rakefile +27 -0
- data/db/database.yml +2 -0
- data/db/migrate/001_create_users.rb +8 -0
- data/db/migrate/002_create_posts.rb +9 -0
- data/lib/schizo/data.rb +32 -0
- data/lib/schizo/facade/base.rb +44 -0
- data/lib/schizo/facade/class_builder.rb +53 -0
- data/lib/schizo/facade/object_builder.rb +49 -0
- data/lib/schizo/facade.rb +9 -0
- data/lib/schizo/role.rb +44 -0
- data/lib/schizo/version.rb +3 -0
- data/lib/schizo.rb +8 -0
- data/schizo.gemspec +29 -0
- data/spec/active_record/attributes_spec.rb +44 -0
- data/spec/active_record/has_many_spec.rb +51 -0
- data/spec/facade/class_builder_spec.rb +52 -0
- data/spec/facade/class_spec.rb +56 -0
- data/spec/facade/object_builder_spec.rb +51 -0
- data/spec/facade/object_spec.rb +72 -0
- data/spec/nested_spec.rb +105 -0
- data/spec/spec_helper.rb +17 -0
- metadata +151 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG
ADDED
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
== Schizo {<img src="https://secure.travis-ci.org/cjbottaro/schizo.png" />}[http://travis-ci.org/cjbottaro/schizo]
|
2
|
+
|
3
|
+
Schizo is a libary that aids in using DCI (data, context and interaction) in Ruby/Rails projects. It aims
|
4
|
+
to overcome some of the shortcomings of using plain <tt>Object#extend</tt>, namely the issue that extending
|
5
|
+
a role can permenantly alter a class.
|
6
|
+
|
7
|
+
== Quickstart
|
8
|
+
|
9
|
+
Dive in...
|
10
|
+
|
11
|
+
class User < ActiveRecord::Base
|
12
|
+
include Schizo::Data
|
13
|
+
end
|
14
|
+
|
15
|
+
module Poster
|
16
|
+
extend Schizo::Role
|
17
|
+
|
18
|
+
extended do
|
19
|
+
has_many :posts
|
20
|
+
end
|
21
|
+
|
22
|
+
def post_count_in_english
|
23
|
+
"#{name} has #{posts.count} post(s)"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
user = User.find(1)
|
28
|
+
user.as(Poster) do |poster|
|
29
|
+
poster.respond_to?(:posts) # => true
|
30
|
+
user.respond_to?(:posts) # => false
|
31
|
+
|
32
|
+
poster.respond_to?(:post_count_in_english) # => true
|
33
|
+
user.respond_to?(:post_count_in_english) # => false
|
34
|
+
|
35
|
+
poster.kind_of?(User) # => true
|
36
|
+
poster.instance_of?(User) # => true
|
37
|
+
|
38
|
+
poster.class.name # => "User"
|
39
|
+
end
|
40
|
+
|
41
|
+
== DCI
|
42
|
+
|
43
|
+
{\http://en.wikipedia.org/wiki/Data,_context_and_interaction}[http://en.wikipedia.org/wiki/Data,_context_and_interaction]
|
44
|
+
|
45
|
+
{\http://mikepackdev.com/blog_posts/24-the-right-way-to-code-dci-in-ruby}[http://mikepackdev.com/blog_posts/24-the-right-way-to-code-dci-in-ruby]
|
46
|
+
|
47
|
+
{\http://saturnflyer.com/blog/jim/2011/10/04/oop-dci-and-ruby-what-your-system-is-vs-what-your-system-does/}[http://saturnflyer.com/blog/jim/2011/10/04/oop-dci-and-ruby-what-your-system-is-vs-what-your-system-does/]
|
48
|
+
|
49
|
+
{\http://victorsavkin.com/post/13966712168/dci-in-ruby}[http://victorsavkin.com/post/13966712168/dci-in-ruby]
|
50
|
+
|
51
|
+
{\http://andrzejonsoftware.blogspot.com/2011/02/dci-and-rails.html}[http://andrzejonsoftware.blogspot.com/2011/02/dci-and-rails.html]
|
52
|
+
|
53
|
+
== The Problem
|
54
|
+
|
55
|
+
So what's wrong with just using <tt>Object#extend</tt>? Nothing, until you want to alter an instance's class
|
56
|
+
as a side effect of adorning it with a role... which happens often when using ActiveRecord.
|
57
|
+
|
58
|
+
Consider the following use of DCI and ActiveRecord with plain old <tt>Object#extend</tt>:
|
59
|
+
|
60
|
+
class User < ActiveRecord::Base
|
61
|
+
end
|
62
|
+
|
63
|
+
module Poster
|
64
|
+
def self.extended(object)
|
65
|
+
object.class.class_eval do
|
66
|
+
has_many :posts
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def post_count_in_english
|
71
|
+
"#{name} has #{posts.count} post(s)"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
user1 = User.find(1)
|
76
|
+
user1.extend(Poster)
|
77
|
+
user1.respond_to?(:posts) # Ok
|
78
|
+
|
79
|
+
user2 = User.find(2)
|
80
|
+
user2.respond_to?(:posts) # Oops, extending user1 ended up changing *all* users!
|
81
|
+
|
82
|
+
That goes against the core concept in DCI that your data should only be injected with behavior for a specific
|
83
|
+
context.
|
84
|
+
|
85
|
+
== The Magic
|
86
|
+
|
87
|
+
So how does Schizo work? It creates <i>facade classes</i> and <i>facade objects</i> that stand in for the
|
88
|
+
classes and objects you really want. The facades try to quack as best they can like the real objects/classes.
|
89
|
+
|
90
|
+
This is easier to explain in an example (continuing from the Quickstart example):
|
91
|
+
|
92
|
+
user = User.find(1)
|
93
|
+
user.as(Poster) do |poster|
|
94
|
+
poster.kind_of?(User) # => true
|
95
|
+
poster.instance_of?(User) # => true
|
96
|
+
|
97
|
+
poster.class.name # => "User"
|
98
|
+
poster.class # => Schizo::Facades::User::Poster
|
99
|
+
end
|
100
|
+
|
101
|
+
<tt>Schizo::Facades::User::Poster</tt> inherits from +User+, that's why <tt>poster.kind_of?(User)</tt> works natrually.
|
102
|
+
<tt>poster.instance_of?(User)</tt> works because of the facade consciously trying to quack like +User+.
|
103
|
+
|
104
|
+
== Facades and Objects
|
105
|
+
|
106
|
+
So knowing you're working with a facade instead of the original object, some of the gotchas become obvious.
|
107
|
+
|
108
|
+
class Foo
|
109
|
+
include Schizo::Data
|
110
|
+
attr_reader :bar
|
111
|
+
def initialize
|
112
|
+
@bar = "low"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
module Baz
|
117
|
+
extend Schizo::Role
|
118
|
+
def set_bar(value)
|
119
|
+
@bar = value
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
foo = Foo.new
|
124
|
+
baz = foo.as(Baz)
|
125
|
+
baz.set_bar("high")
|
126
|
+
baz.bar # => "high"
|
127
|
+
foo.bar # => "low"
|
128
|
+
|
129
|
+
Makes perfect sense, right? But what about this...
|
130
|
+
|
131
|
+
foo = Foo.new
|
132
|
+
foo.as(Baz) do |baz|
|
133
|
+
baz.set_bar("high")
|
134
|
+
end
|
135
|
+
foo.bar # => "high"
|
136
|
+
|
137
|
+
What?! Nah, it's really simple. At the end of the code block, +baz.actualize+ is called. All +#actualize+ does
|
138
|
+
is copy over the instances variables from the facade to the real object.
|
139
|
+
|
140
|
+
You can get the exact same affect by doing:
|
141
|
+
|
142
|
+
foo = Foo.new
|
143
|
+
baz = foo.as(Baz)
|
144
|
+
baz.set_bar("high")
|
145
|
+
baz.actualize
|
146
|
+
foo.bar # => "high"
|
147
|
+
|
148
|
+
Hmm, maybe +#actualize+ should be renamed +#converge+... what do you think?
|
149
|
+
|
150
|
+
== Multiple Roles and Nesting
|
151
|
+
|
152
|
+
You can adorn a data object with more than one role...
|
153
|
+
|
154
|
+
poster = User.new.as(Poster)
|
155
|
+
commenter = poster.as(Commenter) # Has all the methods of a Commenter AND Poster
|
156
|
+
|
157
|
+
Alternatively...
|
158
|
+
|
159
|
+
User.new.as(Poster) do |poster|
|
160
|
+
poster.as(Commenter) do |commenter|
|
161
|
+
# Has all the methods of a Commenter AND Poster
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
== Documentation
|
166
|
+
|
167
|
+
{\http://doc.stochasticbytes.com/schizo/index.html}[http://doc.stochasticbytes.com/schizo/index.html]
|
168
|
+
|
169
|
+
== Contact
|
170
|
+
|
171
|
+
{@cjbottaro}[http://twitter.com/cjbottaro]
|
172
|
+
|
173
|
+
== Liscense
|
174
|
+
|
175
|
+
MIT or something
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'yaml'
|
5
|
+
require 'logger'
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
|
8
|
+
ENV["RAILS_ENV"] ||= "test"
|
9
|
+
|
10
|
+
task :default => :spec
|
11
|
+
|
12
|
+
desc "Run specs (default)"
|
13
|
+
RSpec::Core::RakeTask.new(:spec => :migrate) do |t|
|
14
|
+
t.rspec_opts = "--color -f d"
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Migrate the database through scripts in db/migrate. Target specific version with VERSION=x"
|
18
|
+
task :migrate => :environment do
|
19
|
+
ActiveRecord::Migrator.migrate('db/migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
|
20
|
+
end
|
21
|
+
|
22
|
+
task :environment do
|
23
|
+
FileUtils.mkdir_p("tmp")
|
24
|
+
ActiveRecord::Base.establish_connection(YAML::load(File.open('db/database.yml')))
|
25
|
+
ActiveRecord::Base.logger = Logger.new(File.open('tmp/database.log', 'a'))
|
26
|
+
end
|
27
|
+
|
data/db/database.yml
ADDED
data/lib/schizo/data.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Schizo
|
2
|
+
|
3
|
+
# Include Data into a class to give it the ability to be adorned with a Role.
|
4
|
+
module Data
|
5
|
+
|
6
|
+
# Adorn a Data object with a Role.
|
7
|
+
#
|
8
|
+
# If a block is given, then a facade for _self_ is yielded to the block and the return value
|
9
|
+
# is _self_ with the facade's instance variables copied over to it.
|
10
|
+
# user.name = "callie"
|
11
|
+
# user.as(Poster) do |poster|
|
12
|
+
# poster.name = "coco"
|
13
|
+
# end
|
14
|
+
# user.name # => "coco"
|
15
|
+
#
|
16
|
+
# Without a block, a facade for _self_ is returned.
|
17
|
+
# user.name = "callie"
|
18
|
+
# poster = user.as(Poster)
|
19
|
+
# poster.name = "coco"
|
20
|
+
# user.name # => "callie"
|
21
|
+
def as(role, &block)
|
22
|
+
facade = Facade::ObjectBuilder.new(self, role).product
|
23
|
+
if block_given?
|
24
|
+
block.call(facade)
|
25
|
+
facade.actualize
|
26
|
+
else
|
27
|
+
facade
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Schizo
|
2
|
+
# Rdoc won't let me :nodoc: this module. Nothing to see here.
|
3
|
+
module Facade
|
4
|
+
|
5
|
+
# This module is included in each Data object facade's class.
|
6
|
+
module Base
|
7
|
+
|
8
|
+
def self.included(mod) #:nodoc:
|
9
|
+
mod.extend(ClassMethods)
|
10
|
+
mod.class_eval <<-STR
|
11
|
+
attr_reader :#{DCI_INSTANCE_VARIABLE.to_s[1..-1]}
|
12
|
+
STR
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods #:nodoc:
|
16
|
+
|
17
|
+
def name
|
18
|
+
superclass.name || "AnonClass#{object_id}"
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(object, role) #:nodoc:
|
24
|
+
instance_variable_set(DCI_INSTANCE_VARIABLE, Struct.new(:object, :role).new(object, role))
|
25
|
+
end
|
26
|
+
|
27
|
+
def instance_of?(klass) #:nodoc:
|
28
|
+
# This is to get it working with nested facades. We need to traverse
|
29
|
+
# superclasses until we get to the first real (non facade) superclass.
|
30
|
+
my_superclass = self.class.superclass
|
31
|
+
while my_superclass.ancestors.include?(Base) and my_superclass
|
32
|
+
my_superclass = my_superclass.superclass
|
33
|
+
end
|
34
|
+
my_superclass == klass
|
35
|
+
end
|
36
|
+
|
37
|
+
# Copy a facade's instance variables to the object it's facading for.
|
38
|
+
def actualize
|
39
|
+
dci.object.tap{ |object| Schizo::Facade.copy_instance_variables(self, object) }
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Schizo
|
2
|
+
module Facade #:nodoc:
|
3
|
+
class ClassBuilder #:nodoc:
|
4
|
+
|
5
|
+
attr_reader :base, :role
|
6
|
+
|
7
|
+
def initialize(base, role)
|
8
|
+
@base, @role = base, role
|
9
|
+
end
|
10
|
+
|
11
|
+
def product
|
12
|
+
@product ||= role_class
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def module_name
|
18
|
+
base.name || "AnonClass#{base.object_id}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def role_name
|
22
|
+
role.name || "AnonRole#{role.object_id}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def container_module
|
26
|
+
@container_module ||= begin
|
27
|
+
if Schizo::Facades.const_defined?(module_name, false)
|
28
|
+
Schizo::Facades.const_get(module_name)
|
29
|
+
else
|
30
|
+
Schizo::Facades.const_set(module_name, Module.new)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def role_class
|
36
|
+
@role_class ||= begin
|
37
|
+
if container_module.const_defined?(role_name, false)
|
38
|
+
container_module.const_get(role_name)
|
39
|
+
else
|
40
|
+
container_module.const_set(role_name, build)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def build
|
46
|
+
Class.new(base){ include Base }.tap do |klass|
|
47
|
+
klass.class_eval(&role.extended_block) if role.extended_block
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Schizo
|
2
|
+
module Facade #:nodoc:
|
3
|
+
|
4
|
+
DCI_INSTANCE_VARIABLE = :@dci
|
5
|
+
|
6
|
+
def self.copy_instance_variables(from, to)
|
7
|
+
from.instance_variables.each do |ivar_name|
|
8
|
+
ivar = from.instance_variable_get(ivar_name)
|
9
|
+
to.instance_variable_set(ivar_name, ivar) unless ivar_name == DCI_INSTANCE_VARIABLE
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ObjectBuilder #:nodoc:
|
14
|
+
attr_reader :object, :role
|
15
|
+
|
16
|
+
def initialize(object, role)
|
17
|
+
@object, @role = object, role
|
18
|
+
end
|
19
|
+
|
20
|
+
def product
|
21
|
+
@product ||= build
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def facade_class
|
27
|
+
@facade_class ||= ClassBuilder.new(object.class, role).product
|
28
|
+
end
|
29
|
+
|
30
|
+
def build
|
31
|
+
facade_class.new(object, role).tap do |facade|
|
32
|
+
|
33
|
+
# This is to get nesting to work. Because each facade keeps a reference to the object
|
34
|
+
# it's facading for, we can traverse nested facades. Each time we find one, we extend
|
35
|
+
# its role into facade we're building.
|
36
|
+
previous_facade = object
|
37
|
+
while previous_facade.respond_to?(:dci)
|
38
|
+
facade.extend(previous_facade.dci.role)
|
39
|
+
previous_facade = previous_facade.dci.object
|
40
|
+
end
|
41
|
+
|
42
|
+
facade.extend(role)
|
43
|
+
Schizo::Facade.copy_instance_variables(object, facade)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/schizo/role.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Schizo
|
2
|
+
|
3
|
+
# Extend a module with Role to make that module a _role_.
|
4
|
+
# module RoleA
|
5
|
+
# extend Schizo::Role
|
6
|
+
#
|
7
|
+
# def method_a
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# def method_b
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
module Role
|
14
|
+
|
15
|
+
# call-seq:
|
16
|
+
# extended{ ... }
|
17
|
+
#
|
18
|
+
# Call this method with a block and that block will be evaled by the Data object's class when
|
19
|
+
# the Data object is adorned with this role.
|
20
|
+
# module Poster
|
21
|
+
# extend Schizo::Role
|
22
|
+
#
|
23
|
+
# extended do
|
24
|
+
# has_many :posts
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# User.new.as(Poster).do |poster|
|
29
|
+
# poster.posts.create :title => "My first post!"
|
30
|
+
# end
|
31
|
+
def extended(object = nil, &block)
|
32
|
+
if block_given?
|
33
|
+
@extended_block = block
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def extended_block #:nodoc:
|
40
|
+
@extended_block
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
data/lib/schizo.rb
ADDED
data/schizo.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "schizo/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "schizo"
|
7
|
+
s.version = Schizo::VERSION
|
8
|
+
s.authors = ["Christopher J. Bottaro"]
|
9
|
+
s.email = ["cjbottaro@alumni.cs.utexas.edu"]
|
10
|
+
s.homepage = "https://github.com/cjbottaro/schizo"
|
11
|
+
s.summary = %q{DCI (data, context and interaction) for Ruby / Rails / ActiveRecord}
|
12
|
+
s.description = %q{DCI (data, context and interaction) for Ruby / Rails / ActiveRecord}
|
13
|
+
|
14
|
+
s.rubyforge_project = "schizo"
|
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_development_dependency "activerecord"
|
24
|
+
s.add_development_dependency "sqlite3"
|
25
|
+
s.add_development_dependency "rdoc"
|
26
|
+
s.add_development_dependency "pry"
|
27
|
+
s.add_development_dependency "rake"
|
28
|
+
# s.add_runtime_dependency "rest-client"
|
29
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "an ActiveRecord instance" do
|
4
|
+
context "adorned with a role" do
|
5
|
+
|
6
|
+
let(:role) do
|
7
|
+
Module.new do
|
8
|
+
extend Schizo::Role
|
9
|
+
def set_name(name)
|
10
|
+
self.name = name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:user){ User.new :name => "christopher" }
|
16
|
+
|
17
|
+
let(:adorned_user){ user.as(role) }
|
18
|
+
|
19
|
+
it "setting attributes should affect the original instance because they aren't duped" do
|
20
|
+
adorned_user.set_name("callie")
|
21
|
+
adorned_user.should be_name_changed
|
22
|
+
adorned_user.name.should == "callie"
|
23
|
+
user.name.should == "callie"
|
24
|
+
user.should be_name_changed
|
25
|
+
end
|
26
|
+
|
27
|
+
context "saving" do
|
28
|
+
|
29
|
+
before(:all){ adorned_user.save }
|
30
|
+
|
31
|
+
it "does not affect the original instance's status as a new record" do
|
32
|
+
adorned_user.should_not be_new_record
|
33
|
+
user.should be_new_record
|
34
|
+
end
|
35
|
+
|
36
|
+
it "unless #actualize is called" do
|
37
|
+
adorned_user.actualize
|
38
|
+
user.should_not be_new_record
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "an ActiveRecord instance" do
|
4
|
+
context "extended with a role that has_many :posts" do
|
5
|
+
|
6
|
+
let(:user){ User.create! :name => "chris" }
|
7
|
+
|
8
|
+
let(:role) do
|
9
|
+
Module.new do
|
10
|
+
extend Schizo::Role
|
11
|
+
extended do
|
12
|
+
has_many :posts
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:adorned_user){ user.as(role) }
|
18
|
+
|
19
|
+
it "responds to #posts" do
|
20
|
+
adorned_user.should respond_to(:posts)
|
21
|
+
user.should_not respond_to(:posts)
|
22
|
+
end
|
23
|
+
|
24
|
+
context "#posts" do
|
25
|
+
|
26
|
+
it "works with #<<" do
|
27
|
+
post = Post.new :title => "first"
|
28
|
+
post.should be_a_new_record
|
29
|
+
adorned_user.posts << post
|
30
|
+
post.should_not be_a_new_record
|
31
|
+
adorned_user.posts.count.should == 1
|
32
|
+
end
|
33
|
+
|
34
|
+
it "works with #build" do
|
35
|
+
post = adorned_user.posts.build :title => "second"
|
36
|
+
post.should be_a_new_record
|
37
|
+
adorned_user.save!
|
38
|
+
post.should_not be_a_new_record
|
39
|
+
adorned_user.posts.find(post.id).should == post
|
40
|
+
end
|
41
|
+
|
42
|
+
it "works with #create" do
|
43
|
+
post = adorned_user.posts.create :title => "third"
|
44
|
+
post.should_not be_a_new_record
|
45
|
+
adorned_user.posts.find(post.id).should == post
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "schizo/facade"
|
3
|
+
|
4
|
+
module Schizo
|
5
|
+
module Facade
|
6
|
+
|
7
|
+
describe ClassBuilder do
|
8
|
+
|
9
|
+
let(:base) do
|
10
|
+
Class.new do
|
11
|
+
def self.name; "Foo"; end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:role) do
|
16
|
+
Module.new do
|
17
|
+
extend Role
|
18
|
+
extended do
|
19
|
+
@test_var = 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:builder) do
|
25
|
+
ClassBuilder.new(base, role)
|
26
|
+
end
|
27
|
+
|
28
|
+
context "#initialize" do
|
29
|
+
|
30
|
+
it "sets base and role" do
|
31
|
+
builder.base.should == base
|
32
|
+
builder.role.should == role
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
context "#product" do
|
38
|
+
|
39
|
+
it "returns a facade class" do
|
40
|
+
builder.product.tap do |facade|
|
41
|
+
facade.should be_a(Class)
|
42
|
+
facade.ancestors.should include(base)
|
43
|
+
facade.ancestors.should include(Base)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Schizo
|
4
|
+
module Facade
|
5
|
+
|
6
|
+
describe "a facade class" do
|
7
|
+
|
8
|
+
let(:base) do
|
9
|
+
Class.new do
|
10
|
+
def self.name; "Foo"; end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:role) do
|
15
|
+
Module.new do
|
16
|
+
extend Role
|
17
|
+
extended do
|
18
|
+
@test_var = 1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:builder) do
|
24
|
+
ClassBuilder.new(base, role)
|
25
|
+
end
|
26
|
+
|
27
|
+
let(:facade) do
|
28
|
+
builder.product
|
29
|
+
end
|
30
|
+
|
31
|
+
it "has the same #name as its superclass" do
|
32
|
+
facade.name.should == "Foo"
|
33
|
+
base.name.should == "Foo"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "#initialize has arity == 2" do
|
37
|
+
base.instance_method(:initialize).arity.should < 2
|
38
|
+
facade.instance_method(:initialize).arity.should == 2
|
39
|
+
end
|
40
|
+
|
41
|
+
it "has class evaled the extended block of the role" do
|
42
|
+
facade.should be_instance_variable_defined(:@test_var)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "is defined in Schizo::Facades" do
|
46
|
+
facade.should == Schizo::Facades.const_get(base.name).const_get("AnonRole#{role.object_id}")
|
47
|
+
end
|
48
|
+
|
49
|
+
it "is a singleton" do
|
50
|
+
facade.object_id.should == ClassBuilder.new(base, role).product.object_id
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Schizo
|
4
|
+
module Facade
|
5
|
+
|
6
|
+
describe(ObjectBuilder) do
|
7
|
+
|
8
|
+
let(:base) do
|
9
|
+
Class.new do
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:role) do
|
14
|
+
Module.new do
|
15
|
+
extend Role
|
16
|
+
def bar(v)
|
17
|
+
@bar = v
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:object) do
|
23
|
+
base.new
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:builder) do
|
27
|
+
ObjectBuilder.new(object, role)
|
28
|
+
end
|
29
|
+
|
30
|
+
context "#initialize" do
|
31
|
+
|
32
|
+
it "sets object and role" do
|
33
|
+
builder.object.should == object
|
34
|
+
builder.role.should == role
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
context "#product" do
|
40
|
+
|
41
|
+
it "returns a facade object" do
|
42
|
+
builder.product.should be_a(base)
|
43
|
+
builder.product.should be_instance_of(base)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Schizo
|
4
|
+
module Facade
|
5
|
+
|
6
|
+
describe "a facade object" do
|
7
|
+
|
8
|
+
let(:base) do
|
9
|
+
Class.new do
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:role) do
|
14
|
+
Module.new do
|
15
|
+
extend Role
|
16
|
+
def bar(v)
|
17
|
+
@bar = v
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:object) do
|
23
|
+
base.new
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:builder) do
|
27
|
+
ObjectBuilder.new(object, role)
|
28
|
+
end
|
29
|
+
|
30
|
+
let(:facade) do
|
31
|
+
builder.product
|
32
|
+
end
|
33
|
+
|
34
|
+
it "is not the same as the original object" do
|
35
|
+
facade.should_not == object
|
36
|
+
end
|
37
|
+
|
38
|
+
it "responds to methods defined in its role" do
|
39
|
+
facade.should respond_to(:bar)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "calling methods should not affect original object" do
|
43
|
+
facade.bar("test")
|
44
|
+
facade.instance_variable_get(:@bar).should == "test"
|
45
|
+
object.instance_variable_get(:@bar).should be_nil
|
46
|
+
end
|
47
|
+
|
48
|
+
context "#actualize" do
|
49
|
+
|
50
|
+
before(:all) do
|
51
|
+
facade.bar("blah")
|
52
|
+
facade.actualize
|
53
|
+
end
|
54
|
+
|
55
|
+
it "returns the original object" do
|
56
|
+
facade.actualize.should == object
|
57
|
+
end
|
58
|
+
|
59
|
+
it "sets instance variables in the original object" do
|
60
|
+
object.instance_variable_get(:@bar).should == "blah"
|
61
|
+
end
|
62
|
+
|
63
|
+
it "does not set any internal dci instance variables" do
|
64
|
+
object.instance_variable_defined?(Schizo::Facade::DCI_INSTANCE_VARIABLE).should be_false
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
data/spec/nested_spec.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "a nested facade for ClassA with RoleA, RoleB and RoleC" do
|
4
|
+
|
5
|
+
let(:class_a) do
|
6
|
+
Class.new do
|
7
|
+
include Schizo::Data
|
8
|
+
attr_reader :foo, :bar, :baz
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:role_a) do
|
13
|
+
Module.new do
|
14
|
+
extend Schizo::Role
|
15
|
+
def set_foo; @foo = "foo"; end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:role_b) do
|
20
|
+
Module.new do
|
21
|
+
extend Schizo::Role
|
22
|
+
def set_bar; @bar = "bar"; end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:role_c) do
|
27
|
+
Module.new do
|
28
|
+
extend Schizo::Role
|
29
|
+
def set_baz; @baz = "baz"; end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
let(:object) do
|
34
|
+
class_a.new
|
35
|
+
end
|
36
|
+
|
37
|
+
let(:facade_a) do
|
38
|
+
object.as(role_a)
|
39
|
+
end
|
40
|
+
|
41
|
+
let(:facade_b) do
|
42
|
+
facade_a.as(role_b)
|
43
|
+
end
|
44
|
+
|
45
|
+
let(:facade_c) do
|
46
|
+
facade_b.as(role_c)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "#instance_of?(ClassA) is true" do
|
50
|
+
facade_c.should be_instance_of(class_a)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "#kind_of?(ClassA) is true" do
|
54
|
+
facade_c.should be_kind_of(class_a)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should have methods from RoleA, RoleB and RoleC" do
|
58
|
+
facade_a.should respond_to(:set_foo)
|
59
|
+
facade_a.should_not respond_to(:set_bar)
|
60
|
+
facade_a.should_not respond_to(:set_baz)
|
61
|
+
|
62
|
+
facade_b.should respond_to(:set_foo)
|
63
|
+
facade_b.should respond_to(:set_bar)
|
64
|
+
facade_a.should_not respond_to(:set_baz)
|
65
|
+
|
66
|
+
facade_c.should respond_to(:set_foo)
|
67
|
+
facade_c.should respond_to(:set_bar)
|
68
|
+
facade_c.should respond_to(:set_baz)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "#actualize should walk up the chain" do
|
72
|
+
facade_c.set_foo
|
73
|
+
facade_c.set_bar
|
74
|
+
facade_c.set_baz
|
75
|
+
|
76
|
+
facade_c.foo.should == "foo"
|
77
|
+
facade_c.bar.should == "bar"
|
78
|
+
facade_c.baz.should == "baz"
|
79
|
+
|
80
|
+
facade_b.foo.should be_nil
|
81
|
+
facade_b.bar.should be_nil
|
82
|
+
facade_b.baz.should be_nil
|
83
|
+
|
84
|
+
facade_a.foo.should be_nil
|
85
|
+
facade_a.bar.should be_nil
|
86
|
+
facade_a.baz.should be_nil
|
87
|
+
|
88
|
+
facade_c.actualize
|
89
|
+
|
90
|
+
facade_b.foo.should == "foo"
|
91
|
+
facade_b.bar.should == "bar"
|
92
|
+
facade_b.baz.should == "baz"
|
93
|
+
|
94
|
+
facade_a.foo.should be_nil
|
95
|
+
facade_a.bar.should be_nil
|
96
|
+
facade_a.baz.should be_nil
|
97
|
+
|
98
|
+
facade_b.actualize
|
99
|
+
|
100
|
+
facade_a.foo.should == "foo"
|
101
|
+
facade_a.bar.should == "bar"
|
102
|
+
facade_a.baz.should == "baz"
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "schizo"
|
2
|
+
require "active_record"
|
3
|
+
require "pry"
|
4
|
+
|
5
|
+
ActiveRecord::Base.establish_connection(YAML::load(File.open('db/database.yml')))
|
6
|
+
|
7
|
+
class User < ActiveRecord::Base
|
8
|
+
include Schizo::Data
|
9
|
+
end
|
10
|
+
|
11
|
+
class Post < ActiveRecord::Base
|
12
|
+
include Schizo::Data
|
13
|
+
end
|
14
|
+
|
15
|
+
RSpec.configure do |config|
|
16
|
+
#config.mock_with :rr
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: schizo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Christopher J. Bottaro
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70202402544160 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70202402544160
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activerecord
|
27
|
+
requirement: &70202402543320 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70202402543320
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: sqlite3
|
38
|
+
requirement: &70202402542560 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70202402542560
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rdoc
|
49
|
+
requirement: &70202402541760 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70202402541760
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: pry
|
60
|
+
requirement: &70202402706420 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70202402706420
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: &70202402705740 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70202402705740
|
80
|
+
description: DCI (data, context and interaction) for Ruby / Rails / ActiveRecord
|
81
|
+
email:
|
82
|
+
- cjbottaro@alumni.cs.utexas.edu
|
83
|
+
executables: []
|
84
|
+
extensions: []
|
85
|
+
extra_rdoc_files: []
|
86
|
+
files:
|
87
|
+
- .gitignore
|
88
|
+
- .travis.yml
|
89
|
+
- CHANGELOG
|
90
|
+
- Gemfile
|
91
|
+
- README.rdoc
|
92
|
+
- Rakefile
|
93
|
+
- db/database.yml
|
94
|
+
- db/migrate/001_create_users.rb
|
95
|
+
- db/migrate/002_create_posts.rb
|
96
|
+
- lib/schizo.rb
|
97
|
+
- lib/schizo/data.rb
|
98
|
+
- lib/schizo/facade.rb
|
99
|
+
- lib/schizo/facade/base.rb
|
100
|
+
- lib/schizo/facade/class_builder.rb
|
101
|
+
- lib/schizo/facade/object_builder.rb
|
102
|
+
- lib/schizo/role.rb
|
103
|
+
- lib/schizo/version.rb
|
104
|
+
- schizo.gemspec
|
105
|
+
- spec/active_record/attributes_spec.rb
|
106
|
+
- spec/active_record/has_many_spec.rb
|
107
|
+
- spec/facade/class_builder_spec.rb
|
108
|
+
- spec/facade/class_spec.rb
|
109
|
+
- spec/facade/object_builder_spec.rb
|
110
|
+
- spec/facade/object_spec.rb
|
111
|
+
- spec/nested_spec.rb
|
112
|
+
- spec/spec_helper.rb
|
113
|
+
homepage: https://github.com/cjbottaro/schizo
|
114
|
+
licenses: []
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
segments:
|
126
|
+
- 0
|
127
|
+
hash: -910995716218782308
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
segments:
|
135
|
+
- 0
|
136
|
+
hash: -910995716218782308
|
137
|
+
requirements: []
|
138
|
+
rubyforge_project: schizo
|
139
|
+
rubygems_version: 1.8.11
|
140
|
+
signing_key:
|
141
|
+
specification_version: 3
|
142
|
+
summary: DCI (data, context and interaction) for Ruby / Rails / ActiveRecord
|
143
|
+
test_files:
|
144
|
+
- spec/active_record/attributes_spec.rb
|
145
|
+
- spec/active_record/has_many_spec.rb
|
146
|
+
- spec/facade/class_builder_spec.rb
|
147
|
+
- spec/facade/class_spec.rb
|
148
|
+
- spec/facade/object_builder_spec.rb
|
149
|
+
- spec/facade/object_spec.rb
|
150
|
+
- spec/nested_spec.rb
|
151
|
+
- spec/spec_helper.rb
|