metacosm 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.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +4 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +20 -0
- data/README.md +171 -0
- data/Rakefile +31 -0
- data/bin/metacosm +14 -0
- data/features/.gitkeep +0 -0
- data/features/metacosm.feature +1 -0
- data/features/step_definitions/.gitkeep +0 -0
- data/features/step_definitions/metacosm_steps.rb +1 -0
- data/gemspec.yml +17 -0
- data/lib/metacosm.rb +159 -0
- data/lib/metacosm/version.rb +4 -0
- data/metacosm.gemspec +60 -0
- data/schema.png +0 -0
- data/spec/metacosm_spec.rb +215 -0
- data/spec/spec_helper.rb +120 -0
- data/spec/support/fizz_buzz.rb +128 -0
- data/spec/support/village.rb +155 -0
- metadata +182 -0
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
data/.gitignore
ADDED
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
data/Gemfile
ADDED
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
|
+
[](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
|
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
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|