metacosm 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8d542356d48068642523496b69d59aa7b8bfc6c6
4
+ data.tar.gz: 42389cba5819b48ccb74a2927200f664ee5a3ad6
5
+ SHA512:
6
+ metadata.gz: 6482d50736208180e023f36cb5a4d053b64d8d2ea5c8d2d77bb14c4ae7fe9725f7fc6bbecda4d42fa18bb703b1cc183e860794e3baff566ff4444bb7e1d0e83a
7
+ data.tar.gz: 23a94920bac7b7e87f11ebefd0cf090f381f76600a6932c69edce9ea92b62a90fa4015768e6ac20cb363b56938facb7c93343cf43d7c1e05ea01d599fde5b4df
data/.document ADDED
@@ -0,0 +1,3 @@
1
+ -
2
+ ChangeLog.md
3
+ LICENSE.txt
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ /.bundle
2
+ /.yardoc/
3
+ /Gemfile.lock
4
+ /doc/
5
+ /pkg/
6
+ /vendor/cache/*.gem
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown --title "metacosm Documentation" --protected
data/ChangeLog.md ADDED
@@ -0,0 +1,4 @@
1
+ ### 0.1.0 / 2016-02-16
2
+
3
+ * Initial release:
4
+
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source 'https://rubygems.org'
2
+
3
+ ruby '2.3.0'
4
+
5
+ gemspec
6
+
7
+ gem 'frappuccino'
8
+ gem 'activesupport'
9
+ gem 'passive_record'
10
+
11
+ group :test do
12
+ gem 'rspec-its'
13
+ end
14
+
15
+ group :development do
16
+ gem 'kramdown'
17
+ gem 'pry'
18
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016 Joseph Weissman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # metacosm
2
+
3
+ * [Homepage](https://rubygems.org/gems/metacosm)
4
+ * [Documentation](http://rubydoc.info/gems/metacosm/frames)
5
+ * [Email](mailto:jweissman1986 at gmail.com)
6
+
7
+ [![Code Climate GPA](https://codeclimate.com/github/deepcerulean/metacosm/badges/gpa.svg)](https://codeclimate.com/github/deepcerulean/metacosm)
8
+
9
+ ## Description
10
+
11
+ Metacosm is an awesome microframework for building reactive systems.
12
+
13
+ The idea is to enable quick prototyping of command-query separated or event-sourced systems.
14
+
15
+ One core concept is that we use commands to update "write-only" models,
16
+ which trigger events that update "read-only" view models that are used by queries.
17
+
18
+ Models only transform their state in response to commands, so their state can be reconstructed by replaying the stream of commands.
19
+
20
+ ## Features
21
+
22
+ - One interesting feature here is a sort of mock in-memory AR component called `Registrable` that is used for internal tests (note: this has been extracted to [PassiveRecord](http://github.com/deepcerulean/passive_record))
23
+
24
+ ## Examples
25
+
26
+ A Fizzbuzz implementation contrived enough to show off many of the features of the framework.
27
+
28
+ ````ruby
29
+ require 'metacosm'
30
+ include Metacosm
31
+
32
+ class Counter < Model
33
+ def initialize
34
+ @counter = 0
35
+ super
36
+ end
37
+
38
+ def fizz!
39
+ emit fizz
40
+ end
41
+
42
+ def buzz!
43
+ emit buzz
44
+ end
45
+
46
+ def increment!(inc)
47
+ @counter += inc
48
+ emit(counter_incremented)
49
+ end
50
+
51
+ protected
52
+ def fizz
53
+ FizzEvent.create(value: @counter, counter_id: @id)
54
+ end
55
+
56
+ def buzz
57
+ BuzzEvent.create(value: @counter, counter_id: @id)
58
+ end
59
+
60
+ def counter_incremented
61
+ CounterIncrementedEvent.create(
62
+ value: @counter,
63
+ counter_id: @id
64
+ )
65
+ end
66
+ end
67
+
68
+ class CounterView < View
69
+ attr_accessor :value, :counter_id
70
+ def update_value(new_value)
71
+ @value = new_value
72
+ self
73
+ end
74
+ end
75
+
76
+ class IncrementCounterCommand < Struct.new(:increment, :counter_id)
77
+ end
78
+
79
+ class IncrementCounterCommandHandler
80
+ def handle(command)
81
+ counter = Counter.find_by(command.counter_id)
82
+ counter.increment!(command.increment)
83
+ end
84
+ end
85
+
86
+ class CounterIncrementedEvent < Event
87
+ attr_accessor :value, :counter_id
88
+ end
89
+
90
+ class CounterIncrementedEventListener < EventListener
91
+ def receive(value:,counter_id:)
92
+ update_counter_view(counter_id, value)
93
+
94
+ fizz_buzz!(counter_id, value)
95
+ puts(value) unless fizz?(value) || buzz?(value)
96
+ end
97
+
98
+ def update_counter_view(counter_id, value)
99
+ counter_view = CounterView.where(counter_id: counter_id).first_or_create
100
+ counter_view.value = value
101
+ end
102
+
103
+ private
104
+ def fizz_buzz!(counter_id, n)
105
+ fire(FizzCommand.new(counter_id, n)) if fizz?(n)
106
+ fire(BuzzCommand.new(counter_id, n)) if buzz?(n)
107
+ end
108
+
109
+ def fizz?(n); n % 3 == 0 end
110
+ def buzz?(n); n % 5 == 0 end
111
+ end
112
+
113
+ class FizzCommand < Struct.new(:counter_id, :value); end
114
+ class FizzCommandHandler
115
+ def handle(command)
116
+ counter = Counter.find_by(command.counter_id)
117
+ counter.fizz!
118
+ end
119
+ end
120
+
121
+ class BuzzCommand < Struct.new(:counter_id, :value); end
122
+ class BuzzCommandHandler
123
+ def handle(command)
124
+ counter = Counter.find_by(command.counter_id)
125
+ counter.buzz!
126
+ end
127
+ end
128
+
129
+ class FizzEvent < Event
130
+ attr_accessor :value, :counter_id
131
+ end
132
+
133
+ class FizzEventListener < EventListener
134
+ def receive(event)
135
+ puts "fizz"
136
+ end
137
+ end
138
+
139
+ class BuzzEvent < Event
140
+ attr_accessor :value, :counter_id
141
+ end
142
+
143
+ class BuzzEventListener < EventListener
144
+ def receive(event)
145
+ puts "buzz"
146
+ end
147
+ end
148
+
149
+ class CounterValueQuery
150
+ def execute(counter_id:)
151
+ counter = CounterView.find_by(counter_id: counter_id)
152
+ counter.value
153
+ end
154
+ end
155
+ ````
156
+
157
+ ## Requirements
158
+
159
+ ## Install
160
+
161
+ $ gem install metacosm
162
+
163
+ ## Synopsis
164
+
165
+ $ metacosm
166
+
167
+ ## Copyright
168
+
169
+ Copyright (c) 2016 Joseph Weissman
170
+
171
+ See {file:LICENSE.txt} for details.
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ begin
6
+ require 'bundler/setup'
7
+ rescue LoadError => e
8
+ abort e.message
9
+ end
10
+
11
+ require 'rake'
12
+
13
+
14
+ require 'rubygems/tasks'
15
+ Gem::Tasks.new
16
+
17
+ require 'rspec/core/rake_task'
18
+ RSpec::Core::RakeTask.new
19
+
20
+ task :test => :spec
21
+ task :default => :spec
22
+
23
+ require 'yard'
24
+ YARD::Rake::YardocTask.new
25
+ task :doc => :yard
26
+
27
+ require 'cucumber/rake/task'
28
+
29
+ Cucumber::Rake::Task.new do |t|
30
+ t.cucumber_opts = %w[--format pretty]
31
+ end
data/bin/metacosm ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ root = File.expand_path(File.join(File.dirname(__FILE__),'..'))
4
+ if File.directory?(File.join(root,'.git'))
5
+ Dir.chdir(root) do
6
+ begin
7
+ require 'bundler/setup'
8
+ rescue LoadError => e
9
+ warn e.message
10
+ warn "Run `gem install bundler` to install Bundler"
11
+ exit -1
12
+ end
13
+ end
14
+ end
data/features/.gitkeep ADDED
File without changes
@@ -0,0 +1 @@
1
+ Feature: Blah blah blah
File without changes
@@ -0,0 +1 @@
1
+ @wip
data/gemspec.yml ADDED
@@ -0,0 +1,17 @@
1
+ name: metacosm
2
+ summary: "reactive simulation framework"
3
+ description: "pure Ruby functional-reactive simulation metaframework"
4
+ license: MIT
5
+ authors: Joseph Weissman
6
+ email: jweissman1986@gmail.com
7
+ homepage: https://rubygems.org/gems/metacosm
8
+ dependencies:
9
+ passive_record: ~> 0.1.8
10
+ development_dependencies:
11
+ bundler: ~> 1.10
12
+ codeclimate-test-reporter: ~> 0.1
13
+ cucumber: ~> 0.10.2
14
+ rake: ~> 10.0
15
+ rspec: ~> 3.0
16
+ rubygems-tasks: ~> 0.2
17
+ yard: ~> 0.8
data/lib/metacosm.rb ADDED
@@ -0,0 +1,159 @@
1
+ require 'passive_record'
2
+ require 'frappuccino'
3
+ require 'metacosm/version'
4
+
5
+ module Metacosm
6
+ class Model
7
+ include PassiveRecord
8
+ after_create :register_observer, :emit_creation_event
9
+
10
+ def update(attrs={})
11
+ attrs.each do |k,v|
12
+ send("#{k}=",v)
13
+ end
14
+
15
+ emit(updation_event(attrs)) if updated_event_class
16
+ end
17
+
18
+ protected
19
+ def register_observer
20
+ Simulation.current.watch(self)
21
+ end
22
+
23
+ def emit_creation_event
24
+ emit(creation_event) if created_event_class
25
+ end
26
+
27
+ def attributes_with_external_id
28
+ attrs = to_h
29
+ if attrs.key?(:id)
30
+ new_id_key = self.class.name.split('::').last.underscore + "_id"
31
+ attrs[new_id_key.to_sym] = attrs.delete(:id)
32
+ end
33
+ attrs
34
+ end
35
+
36
+ # trim down extenralized attrs for evt
37
+ def attributes_for_event(klass, additional_attrs={})
38
+ # assume evts attrs are attr_accessible?
39
+ keys_to_keep = klass.instance_methods.find_all do |method|
40
+ method != :== &&
41
+ method != :! &&
42
+ klass.instance_methods.include?(:"#{method}=")
43
+ end
44
+
45
+ attributes_with_external_id.
46
+ delete_if {|k,v| !keys_to_keep.include?(k) }.
47
+ merge(additional_attrs)
48
+ end
49
+
50
+ def assemble_event(klass, addl_attrs={})
51
+ klass.create(attributes_for_event(klass).merge(addl_attrs))
52
+ end
53
+
54
+ def creation_event
55
+ assemble_event(created_event_class)
56
+ end
57
+
58
+ def created_event_class
59
+ created_event_name = self.class.name + "CreatedEvent"
60
+ Object.const_get(created_event_name) rescue nil
61
+ end
62
+
63
+ def updation_event(changed_attrs={})
64
+ assemble_event(updated_event_class, changed_attrs)
65
+ end
66
+
67
+ def updated_event_class
68
+ updated_event_name = self.class.name + "UpdatedEvent"
69
+ Object.const_get(updated_event_name) rescue nil
70
+ end
71
+
72
+ def blacklisted_attribute_names
73
+ [ :@observer_peers ]
74
+ end
75
+ end
76
+
77
+ class View
78
+ include PassiveRecord
79
+ end
80
+
81
+ class Command
82
+ include PassiveRecord
83
+
84
+ def attrs
85
+ to_h.keep_if { |k,_| k != :id }
86
+ end
87
+
88
+ def ==(other)
89
+ attrs == other.attrs
90
+ end
91
+ end
92
+
93
+ class Event
94
+ include PassiveRecord
95
+
96
+ def attrs
97
+ to_h.keep_if { |k,_| k != :id }
98
+ end
99
+
100
+ def ==(other)
101
+ attrs == other.attrs
102
+ end
103
+ end
104
+
105
+ class EventListener < Struct.new(:simulation)
106
+ def fire(command)
107
+ self.simulation.apply(command)
108
+ end
109
+ end
110
+
111
+ class Simulation
112
+ def watch(model)
113
+ Frappuccino::Stream.new(model).on_value(&method(:receive))
114
+ end
115
+
116
+ def apply(command)
117
+ handler_for(command).handle(command.attrs)
118
+ end
119
+
120
+ def receive(event, record: true)
121
+ events.push(event) if record
122
+
123
+ listener = listener_for(event)
124
+ if event.attrs.any?
125
+ listener.receive(event.attrs)
126
+ else
127
+ listener.receive
128
+ end
129
+ end
130
+
131
+ def events
132
+ @events ||= []
133
+ end
134
+
135
+ def self.current
136
+ @current ||= new
137
+ end
138
+
139
+ def clear!
140
+ @events = []
141
+ end
142
+
143
+ protected
144
+ def handler_for(command)
145
+ @handlers ||= {}
146
+ @handlers[command] ||= Object.const_get(command.class.name.split('::').last + "Handler").new
147
+ end
148
+
149
+ def listener_for(event)
150
+ @listeners ||= {}
151
+ @listeners[event] ||= construct_listener_for(event)
152
+ end
153
+
154
+ def construct_listener_for(event)
155
+ listener = Object.const_get(event.class.name.split('::').last + "Listener").new(self)
156
+ listener
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,4 @@
1
+ module Metacosm
2
+ # metacosm version
3
+ VERSION = "0.1.1"
4
+ end
data/metacosm.gemspec ADDED
@@ -0,0 +1,60 @@
1
+ # encoding: utf-8
2
+
3
+ require 'yaml'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gemspec = YAML.load_file('gemspec.yml')
7
+
8
+ gem.name = gemspec.fetch('name')
9
+ gem.version = gemspec.fetch('version') do
10
+ lib_dir = File.join(File.dirname(__FILE__),'lib')
11
+ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
12
+
13
+ require 'metacosm/version'
14
+ Metacosm::VERSION
15
+ end
16
+
17
+ gem.summary = gemspec['summary']
18
+ gem.description = gemspec['description']
19
+ gem.licenses = Array(gemspec['license'])
20
+ gem.authors = Array(gemspec['authors'])
21
+ gem.email = gemspec['email']
22
+ gem.homepage = gemspec['homepage']
23
+
24
+ glob = lambda { |patterns| gem.files & Dir[*patterns] }
25
+
26
+ gem.files = `git ls-files`.split($/)
27
+ gem.files = glob[gemspec['files']] if gemspec['files']
28
+
29
+ gem.executables = gemspec.fetch('executables') do
30
+ glob['bin/*'].map { |path| File.basename(path) }
31
+ end
32
+ gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.'
33
+
34
+ gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb']
35
+ gem.test_files = glob[gemspec['test_files'] || '{test/{**/}*_test.rb']
36
+ gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}']
37
+
38
+ gem.require_paths = Array(gemspec.fetch('require_paths') {
39
+ %w[ext lib].select { |dir| File.directory?(dir) }
40
+ })
41
+
42
+ gem.requirements = Array(gemspec['requirements'])
43
+ gem.required_ruby_version = gemspec['required_ruby_version']
44
+ gem.required_rubygems_version = gemspec['required_rubygems_version']
45
+ gem.post_install_message = gemspec['post_install_message']
46
+
47
+ split = lambda { |string| string.split(/,\s*/) }
48
+
49
+ if gemspec['dependencies']
50
+ gemspec['dependencies'].each do |name,versions|
51
+ gem.add_dependency(name,split[versions])
52
+ end
53
+ end
54
+
55
+ if gemspec['development_dependencies']
56
+ gemspec['development_dependencies'].each do |name,versions|
57
+ gem.add_development_dependency(name,split[versions])
58
+ end
59
+ end
60
+ end
data/schema.png ADDED
File without changes
@@ -0,0 +1,215 @@
1
+ require 'spec_helper'
2
+
3
+ describe "a simple simulation (fizzbuzz)" do
4
+ subject(:simulation) { Simulation.current }
5
+ let!(:model) { Counter.create }
6
+ let(:last_event) { simulation.events.last }
7
+
8
+ describe "#apply" do
9
+ let(:increment_counter) do
10
+ IncrementCounterCommand.create(
11
+ increment: 1, counter_id: model.id
12
+ )
13
+ end
14
+
15
+ context "one command once" do
16
+ before { simulation.apply(increment_counter) }
17
+
18
+ describe "the last event" do
19
+ subject { last_event }
20
+ it { is_expected.to be_a CounterIncrementedEvent }
21
+ its(:counter_id) { is_expected.to eql(model.id) }
22
+ its(:value) { is_expected.to eq(1) }
23
+ end
24
+
25
+ describe "querying for the counter value" do
26
+ let(:counter_value_query) do
27
+ CounterValueQuery.new
28
+ end
29
+
30
+ subject do
31
+ counter_value_query.execute(counter_id: model.id)
32
+ end
33
+
34
+ it { is_expected.to eq(1) }
35
+ end
36
+ end
37
+
38
+ context "one command ten times" do
39
+ it 'is expected to play fizz buzz' do
40
+ expect {
41
+ 10.times { simulation.apply(increment_counter) }
42
+ }.to output(%w[ 1 2 fizz 4 buzz fizz 7 8 fizz buzz ].join("\n") + "\n").to_stdout
43
+ end
44
+ end
45
+
46
+ context "one command repeatedly" do
47
+ let(:n) { 10 } # ops
48
+
49
+ context 'with a single command source' do
50
+ before do
51
+ n.times { simulation.apply(increment_counter) }
52
+ end
53
+
54
+ describe "the last event" do
55
+ subject { last_event }
56
+ it { is_expected.to be_a BuzzEvent }
57
+ end
58
+
59
+ describe "querying for the counter value" do
60
+ let(:counter_value_query) do
61
+ CounterValueQuery.new
62
+ end
63
+
64
+ subject do
65
+ counter_value_query.execute(counter_id: model.id)
66
+ end
67
+
68
+ it { is_expected.to eq(n) } #"The counter is at 1") }
69
+ end
70
+ end
71
+
72
+ context 'with concurrent command sources' do
73
+ let(:m) { 2 } # fibers
74
+ let(:threads) {
75
+ ts = []
76
+ m.times do
77
+ ts.push(Thread.new do
78
+ (n/m).times { simulation.apply(increment_counter) }
79
+ end)
80
+ end
81
+ ts
82
+ }
83
+
84
+ before do
85
+ threads.map(&:join)
86
+ end
87
+
88
+ describe "the last event" do
89
+ subject { last_event }
90
+
91
+ it { is_expected.to be_a BuzzEvent }
92
+ end
93
+
94
+ describe "querying for the counter value" do
95
+ let(:counter_value_query) do
96
+ CounterValueQuery.new
97
+ end
98
+
99
+ subject do
100
+ counter_value_query.execute(counter_id: model.id)
101
+ end
102
+
103
+ it { is_expected.to eq(n) }
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ describe "a more complex simulation (village)" do
111
+ subject(:simulation) { Simulation.current }
112
+ let!(:world) { World.create(id: world_id) }
113
+ let(:world_id) { 'world_id' }
114
+
115
+ describe "#apply" do
116
+ context 'create and populate villages' do
117
+ let(:person_id) { 'person_id' }
118
+ let(:village_id) { 'village_id' }
119
+ let(:village_name) { 'Oakville Ridge' }
120
+
121
+ let(:people_per_village) { 10 }
122
+
123
+ let(:create_village_command) do
124
+ CreateVillageCommand.create(
125
+ world_id: world_id,
126
+ village_id: village_id,
127
+ village_name: village_name
128
+ )
129
+ end
130
+
131
+ let(:rename_village_command) do
132
+ RenameVillageCommand.create(
133
+ village_id: village_id,
134
+ new_village_name: "Newcity"
135
+ )
136
+ end
137
+
138
+ let(:village_created_event) do
139
+ VillageCreatedEvent.create(
140
+ world_id: world_id,
141
+ village_id: village_id,
142
+ name: village_name
143
+ )
144
+ end
145
+
146
+ let(:populate_world_command) do
147
+ PopulateWorldCommand.create(
148
+ world_id: world_id,
149
+ name_dictionary: %w[ Alice ],
150
+ per_village: people_per_village
151
+ )
152
+ end
153
+ #
154
+ let(:create_person_command) do
155
+ CreatePersonCommand.create(
156
+ world_id: world_id,
157
+ village_id: village_id,
158
+ person_id: person_id,
159
+ person_name: "Alice"
160
+ )
161
+ end
162
+
163
+ let(:person_created_event) do
164
+ PersonCreatedEvent.create(village_id: village_id, person_id: person_id, name: "Alice")
165
+ end
166
+
167
+ let(:village_names_query) do
168
+ VillageNamesQuery.new(world_id)
169
+ end
170
+
171
+ let(:people_names_query) do
172
+ PeopleNamesQuery.new(world_id)
173
+ end
174
+
175
+ describe "handling a create village command" do
176
+ it 'should result in a village creation event' do
177
+ given_no_activity.
178
+ when(create_village_command).expect_events([village_created_event])
179
+ end
180
+ end
181
+
182
+ describe 'recieving a village created event' do
183
+ it 'should create a village view we can lookup' do
184
+ given_events([village_created_event]).
185
+ expect_query(village_names_query, to_find: ["Oakville Ridge"])
186
+ end
187
+ end
188
+
189
+ it 'should create a village and a person' do
190
+ given_no_activity.
191
+ when(create_village_command, create_person_command).
192
+ expect_events([village_created_event, person_created_event]).
193
+ expect_query(village_names_query, to_find: ["Oakville Ridge"]).
194
+ expect_query(people_names_query, to_find: ["Alice"])
195
+ end
196
+
197
+ it 'should populate the world' do
198
+ expected_names = Array.new(people_per_village) { "Alice" }
199
+
200
+ given_no_activity.
201
+ when(create_village_command, populate_world_command).
202
+ expect_query(village_names_query, to_find: ["Oakville Ridge"]).
203
+ expect_query(people_names_query, to_find: expected_names)
204
+ end
205
+
206
+ it 'should rename a village' do
207
+ given_no_activity.
208
+ when(
209
+ create_village_command,
210
+ rename_village_command
211
+ ).expect_query(village_names_query, to_find: ["Newcity"])
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,120 @@
1
+ require 'rspec'
2
+ require 'rspec/its'
3
+
4
+ require 'pry'
5
+ require 'ostruct'
6
+ require 'metacosm'
7
+
8
+ include Metacosm
9
+
10
+ require 'support/fizz_buzz'
11
+ require 'support/village'
12
+
13
+ class GivenWhenThen < Struct.new(:given_events,:when_command,:then_event_class)
14
+ include RSpec::Matchers
15
+
16
+ def when(*commands)
17
+ @when_commands ||= []
18
+ commands.each do |command|
19
+ @when_commands.push command
20
+ end
21
+ self
22
+ end
23
+
24
+ def expect_events(evts)
25
+ @then_events = evts
26
+ verify!
27
+ self
28
+ end
29
+
30
+ def expect_query(query, to_find:)
31
+ @query = query
32
+ @expected_query_results = to_find
33
+ verify!
34
+ self
35
+ end
36
+
37
+ protected
38
+
39
+ def verify!
40
+ clean_slate!
41
+ receive_events!
42
+ fire_commands!
43
+
44
+ validate_events!
45
+ validate_query!
46
+
47
+ self
48
+ end
49
+
50
+ private
51
+
52
+ def clean_slate!
53
+ PassiveRecord.drop_all
54
+ Simulation.current.clear!
55
+ self
56
+ end
57
+
58
+ def receive_events!
59
+ unless self.given_events.nil?
60
+ self.given_events.each do |evt|
61
+ sim.receive(evt, record: false)
62
+ end
63
+ end
64
+ self
65
+ end
66
+
67
+ def fire_commands!
68
+ unless @when_commands.nil?
69
+ @when_commands.each do |cmd|
70
+ sim.apply(cmd)
71
+ end
72
+ end
73
+ self
74
+ end
75
+
76
+ def validate_events!
77
+ if @then_event_class
78
+ expect(@then_event_class).to eq(sim.events.last.class)
79
+ end
80
+
81
+ if @then_events
82
+ expect(@then_events).to match_array(sim.events)
83
+ end
84
+
85
+ if @then_event_attrs
86
+ @then_event_attrs.each do |k,v|
87
+ expect(sim.events.last.send(k)).to eq(v)
88
+ end
89
+ end
90
+
91
+ self
92
+ end
93
+
94
+ def validate_query!
95
+ if @query
96
+ expect(@query.execute).to eq(@expected_query_results)
97
+ end
98
+ self
99
+ end
100
+
101
+ def sim
102
+ @sim ||= Simulation.current
103
+ end
104
+ end
105
+
106
+ module Metacosm
107
+ module SpecHelpers
108
+ def given_no_activity
109
+ GivenWhenThen.new
110
+ end
111
+
112
+ def given_events(events)
113
+ GivenWhenThen.new(events)
114
+ end
115
+ end
116
+ end
117
+
118
+ RSpec.configure do |c|
119
+ c.include Metacosm::SpecHelpers
120
+ end
@@ -0,0 +1,128 @@
1
+ class Counter < Model
2
+ def initialize
3
+ @counter = 0
4
+ super
5
+ end
6
+
7
+ def fizz!
8
+ emit fizz
9
+ end
10
+
11
+ def buzz!
12
+ emit buzz
13
+ end
14
+
15
+ def increment!(inc)
16
+ @counter += inc
17
+ emit(counter_incremented)
18
+ end
19
+
20
+ protected
21
+ def fizz
22
+ FizzEvent.create
23
+ end
24
+
25
+ def buzz
26
+ BuzzEvent.create
27
+ end
28
+
29
+ def counter_incremented
30
+ CounterIncrementedEvent.create(
31
+ value: @counter,
32
+ counter_id: @id
33
+ )
34
+ end
35
+ end
36
+
37
+ class CounterView < View
38
+ attr_accessor :value, :counter_id
39
+ def update_value(new_value)
40
+ @value = new_value
41
+ self
42
+ end
43
+ end
44
+
45
+ class IncrementCounterCommand < Command
46
+ attr_accessor :increment, :counter_id
47
+ end
48
+
49
+ class IncrementCounterCommandHandler
50
+ def handle(increment:,counter_id:)
51
+ counter = Counter.find(counter_id)
52
+ counter.increment!(increment)
53
+ end
54
+ end
55
+
56
+ class CounterIncrementedEvent < Event
57
+ attr_accessor :value, :counter_id
58
+ end
59
+
60
+ class CounterIncrementedEventListener < EventListener
61
+ def receive(value:,counter_id:)
62
+ update_counter_view(counter_id, value)
63
+
64
+ fizz_buzz!(counter_id, value)
65
+ puts(value) unless fizz?(value) || buzz?(value)
66
+ end
67
+
68
+ def update_counter_view(counter_id, value)
69
+ counter_view = CounterView.where(counter_id: counter_id).first_or_create
70
+ counter_view.value = value
71
+ end
72
+
73
+ private
74
+ def fizz_buzz!(counter_id, n)
75
+ fire(FizzCommand.create(counter_id: counter_id)) if fizz?(n)
76
+ fire(BuzzCommand.create(counter_id: counter_id)) if buzz?(n)
77
+ end
78
+
79
+ def fizz?(n); n % 3 == 0 end
80
+ def buzz?(n); n % 5 == 0 end
81
+ end
82
+
83
+ class FizzCommand < Command
84
+ attr_accessor :counter_id
85
+ end
86
+
87
+ class FizzCommandHandler
88
+ def handle(counter_id:)
89
+ counter = Counter.find(counter_id)
90
+ counter.fizz!
91
+ end
92
+ end
93
+
94
+ class BuzzCommand < Command
95
+ attr_accessor :counter_id
96
+ end
97
+
98
+ class BuzzCommandHandler
99
+ def handle(counter_id:)
100
+ counter = Counter.find(counter_id)
101
+ counter.buzz!
102
+ end
103
+ end
104
+
105
+ class FizzEvent < Event
106
+ end
107
+
108
+ class FizzEventListener < EventListener
109
+ def receive
110
+ puts "fizz"
111
+ end
112
+ end
113
+
114
+ class BuzzEvent < Event
115
+ end
116
+
117
+ class BuzzEventListener < EventListener
118
+ def receive
119
+ puts "buzz"
120
+ end
121
+ end
122
+
123
+ class CounterValueQuery
124
+ def execute(counter_id:)
125
+ counter = CounterView.find_by(counter_id: counter_id)
126
+ counter.value
127
+ end
128
+ end
@@ -0,0 +1,155 @@
1
+ class Person < Model
2
+ belongs_to :village
3
+ attr_accessor :name
4
+ end
5
+
6
+ class PersonView < View
7
+ belongs_to :village_view
8
+ attr_accessor :person_name, :person_id, :village_id
9
+ end
10
+
11
+ class Village < Model
12
+ belongs_to :world
13
+ has_many :people
14
+ attr_accessor :name
15
+ end
16
+
17
+ class VillageView < View
18
+ belongs_to :world_view
19
+ has_many :person_views
20
+ attr_accessor :name, :village_id, :world_id
21
+ end
22
+
23
+ class World < Model
24
+ has_many :villages
25
+ has_many :people, :through => :villages
26
+ end
27
+
28
+ class WorldView < View
29
+ attr_accessor :world_id
30
+ has_many :village_views
31
+ has_many :person_views, :through => :village_views
32
+ end
33
+
34
+ class CreateVillageCommand < Command
35
+ attr_accessor :world_id, :village_id, :village_name
36
+ end
37
+
38
+ class CreateVillageCommandHandler
39
+ def handle(world_id:,village_id:,village_name:)
40
+ world = World.where(id: world_id).first_or_create
41
+ world.create_village(name: village_name, id: village_id)
42
+ self
43
+ end
44
+ end
45
+
46
+ class VillageCreatedEvent < Event
47
+ attr_accessor :village_id, :name, :world_id
48
+ end
49
+
50
+ class VillageCreatedEventListener < EventListener
51
+ def receive(world_id:, village_id:, name:)
52
+ world = WorldView.where(world_id: world_id).first_or_create
53
+ world.create_village_view(
54
+ world_id: world_id,
55
+ village_id: village_id,
56
+ name: name
57
+ )
58
+ end
59
+ end
60
+
61
+ class RenameVillageCommand < Command
62
+ attr_accessor :village_id, :new_village_name
63
+ end
64
+
65
+ class RenameVillageCommandHandler
66
+ def handle(village_id:, new_village_name:)
67
+ village = Village.find(village_id)
68
+ village.update name: new_village_name
69
+ end
70
+ end
71
+
72
+ class VillageUpdatedEvent < Event
73
+ attr_accessor :village_id, :name
74
+ end
75
+
76
+ class VillageUpdatedEventListener < EventListener
77
+ def receive(village_id:, name:)
78
+ village_view = VillageView.find_by(village_id: village_id)
79
+ village_view.name = name
80
+ end
81
+ end
82
+
83
+
84
+ class CreatePersonCommand < Command
85
+ attr_accessor :world_id, :village_id, :person_id, :person_name
86
+ end
87
+
88
+ class CreatePersonCommandHandler
89
+ def handle(world_id:, village_id:, person_id:, person_name:)
90
+ world = World.where(id: world_id).first_or_create
91
+ world.create_person(id: person_id, village_id: village_id, name: person_name)
92
+ end
93
+ end
94
+
95
+ class PersonCreatedEvent < Event
96
+ attr_accessor :name, :person_id, :village_id
97
+ end
98
+
99
+ class PersonCreatedEventListener < EventListener
100
+ def receive(name:, person_id:, village_id:)
101
+ village_view = VillageView.where(village_id: village_id).first
102
+ village_view.create_person_view(
103
+ person_name: name,
104
+ person_id: person_id,
105
+ village_id: village_id
106
+ )
107
+ end
108
+ end
109
+
110
+ class PopulateWorldCommand < Command
111
+ attr_accessor :world_id, :name_dictionary, :per_village
112
+ end
113
+
114
+ class PopulateWorldCommandHandler
115
+ def handle(world_id:, name_dictionary:, per_village:)
116
+ world_id = world_id
117
+ dictionary = name_dictionary
118
+
119
+ world = World.where(id: world_id).first_or_create
120
+
121
+ world.villages.each do |village|
122
+ name = dictionary.sample
123
+ per_village.times do
124
+ village.create_person(name: name)
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ class CreateWorldCommand < Command
131
+ attr_accessor :world_id
132
+ end
133
+
134
+ class CreateWorldCommandHandler
135
+ def handle(cmd)
136
+ world_id = cmd.world_id
137
+ World.create(id: world_id)
138
+ end
139
+ end
140
+
141
+ ## queries
142
+
143
+ class VillageNamesQuery < Struct.new(:world_id)
144
+ def execute
145
+ world = WorldView.where(world_id: world_id).first_or_create
146
+ world.village_views.map(&:name)
147
+ end
148
+ end
149
+
150
+ class PeopleNamesQuery < Struct.new(:world_id)
151
+ def execute
152
+ world_view = WorldView.where(world_id: world_id).first_or_create
153
+ world_view.person_views.flat_map(&:person_name)
154
+ end
155
+ end
metadata ADDED
@@ -0,0 +1,182 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: metacosm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Joseph Weissman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: passive_record
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.8
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.8
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: codeclimate-test-reporter
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: cucumber
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.10.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.10.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubygems-tasks
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.2'
111
+ - !ruby/object:Gem::Dependency
112
+ name: yard
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.8'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.8'
125
+ description: pure Ruby functional-reactive simulation metaframework
126
+ email: jweissman1986@gmail.com
127
+ executables:
128
+ - metacosm
129
+ extensions: []
130
+ extra_rdoc_files:
131
+ - ChangeLog.md
132
+ - LICENSE.txt
133
+ - README.md
134
+ files:
135
+ - ".document"
136
+ - ".gitignore"
137
+ - ".rspec"
138
+ - ".yardopts"
139
+ - ChangeLog.md
140
+ - Gemfile
141
+ - LICENSE.txt
142
+ - README.md
143
+ - Rakefile
144
+ - bin/metacosm
145
+ - features/.gitkeep
146
+ - features/metacosm.feature
147
+ - features/step_definitions/.gitkeep
148
+ - features/step_definitions/metacosm_steps.rb
149
+ - gemspec.yml
150
+ - lib/metacosm.rb
151
+ - lib/metacosm/version.rb
152
+ - metacosm.gemspec
153
+ - schema.png
154
+ - spec/metacosm_spec.rb
155
+ - spec/spec_helper.rb
156
+ - spec/support/fizz_buzz.rb
157
+ - spec/support/village.rb
158
+ homepage: https://rubygems.org/gems/metacosm
159
+ licenses:
160
+ - MIT
161
+ metadata: {}
162
+ post_install_message:
163
+ rdoc_options: []
164
+ require_paths:
165
+ - lib
166
+ required_ruby_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ required_rubygems_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ requirements: []
177
+ rubyforge_project:
178
+ rubygems_version: 2.5.1
179
+ signing_key:
180
+ specification_version: 4
181
+ summary: reactive simulation framework
182
+ test_files: []