fourth_dimensional 0.1.0 → 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 +4 -4
- data/lib/fourth_dimensional.rb +73 -0
- data/lib/fourth_dimensional/aggregate_root.rb +100 -0
- data/lib/fourth_dimensional/command.rb +63 -0
- data/lib/fourth_dimensional/command_handler.rb +79 -0
- data/lib/fourth_dimensional/configuration.rb +28 -0
- data/lib/fourth_dimensional/event.rb +28 -3
- data/lib/fourth_dimensional/event_loaders.rb +5 -0
- data/lib/fourth_dimensional/event_loaders/active_record.rb +85 -0
- data/lib/fourth_dimensional/eventable.rb +56 -0
- data/lib/fourth_dimensional/railtie.rb +4 -0
- data/lib/fourth_dimensional/record_projector.rb +60 -0
- data/lib/fourth_dimensional/repository.rb +87 -0
- data/lib/fourth_dimensional/version.rb +1 -1
- data/lib/generators/fourth_dimensional/install_generator.rb +11 -0
- data/lib/generators/fourth_dimensional/migration_generator.rb +29 -0
- data/lib/generators/fourth_dimensional/templates/initializer.rb +7 -0
- metadata +91 -12
- data/.gitignore +0 -11
- data/.rspec +0 -3
- data/.travis.yml +0 -15
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -39
- data/README.md +0 -123
- data/Rakefile +0 -16
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/fourth_dimensional.gemspec +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e42f145173551ac50193f92d2dea18e824e9c08807040cb53bdab953ff57061
|
4
|
+
data.tar.gz: a8813fea8c342ca410cb5935dd4e4deece9fb247fa110b3b03f2f9044ad266bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 32b5cec8272c1d40448a60ffaad530713f02d5373e2a9f181ebacb39a92cf378300ba51a80acfc138fca0fa5f5443612177a2a2fe0f3bb6b7caeaf7fcde3d510
|
7
|
+
data.tar.gz: 39c610fad07ed26675a66c30f5c3324837f70fea7301a5d78bf3b4df1a5aaf68fd4307760538085b7ea32d0e78470bdcbf3e7c0e3be1adafc110a3afb55bfdb5
|
data/lib/fourth_dimensional.rb
CHANGED
@@ -1,7 +1,80 @@
|
|
1
1
|
require "fourth_dimensional/version"
|
2
|
+
require "fourth_dimensional/railtie" if defined?(Rails)
|
2
3
|
|
3
4
|
module FourthDimensional
|
4
5
|
class Error < StandardError; end
|
5
6
|
|
7
|
+
autoload :AggregateRoot, 'fourth_dimensional/aggregate_root'
|
8
|
+
autoload :Command, 'fourth_dimensional/command'
|
9
|
+
autoload :CommandHandler, 'fourth_dimensional/command_handler'
|
10
|
+
autoload :Configuration, 'fourth_dimensional/configuration'
|
6
11
|
autoload :Event, 'fourth_dimensional/event'
|
12
|
+
autoload :Eventable, 'fourth_dimensional/eventable'
|
13
|
+
autoload :EventLoaders, 'fourth_dimensional/event_loaders'
|
14
|
+
autoload :RecordProjector, 'fourth_dimensional/record_projector'
|
15
|
+
autoload :Repository, 'fourth_dimensional/repository'
|
16
|
+
|
17
|
+
# The singleton instance of Configuration
|
18
|
+
def self.config
|
19
|
+
@configuration ||= Configuration.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# Yields the Configuration instance
|
23
|
+
#
|
24
|
+
# FourthDimensional.configure do |config|
|
25
|
+
# config.command_handlers = [
|
26
|
+
# CommentCommandHandler,
|
27
|
+
# PostCommandHandler
|
28
|
+
# ]
|
29
|
+
# end
|
30
|
+
def self.configure
|
31
|
+
yield config
|
32
|
+
end
|
33
|
+
|
34
|
+
# Iniitlaizes a Repository with the required dependencies.
|
35
|
+
#
|
36
|
+
# FourthDimensional.repository # => FourthDimensional::Repository
|
37
|
+
def self.build_repository
|
38
|
+
Repository.new(event_loader: config.event_loader)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Runs a single or array of commands through all command handlers, saves
|
42
|
+
# commands and applied events, and invokes event handlers.
|
43
|
+
#
|
44
|
+
# Fourthdimensional.execute_command(command)
|
45
|
+
# FourthDimensional.execute_commands(command1, command2)
|
46
|
+
# FourthDimensional.execute_commands([command1, command2])
|
47
|
+
def self.execute_commands(*commands)
|
48
|
+
repository = build_repository
|
49
|
+
call_command_handlers(repository, commands)
|
50
|
+
saved_events = save_commands_and_events(repository)
|
51
|
+
call_event_handlers(saved_events)
|
52
|
+
end
|
53
|
+
|
54
|
+
class << self
|
55
|
+
alias_method :execute_command, :execute_commands
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def self.call_command_handlers(repository, commands)
|
61
|
+
config.command_handlers.each do |command_handler|
|
62
|
+
commands.flatten.each do |command|
|
63
|
+
command_handler.new(repository: repository).call(command)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.save_commands_and_events(repository)
|
69
|
+
config.event_loader.save_commands_and_events(
|
70
|
+
commands: repository.called_commands,
|
71
|
+
events: repository.applied_events
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.call_event_handlers(events)
|
76
|
+
config.event_handlers.each do |event_handler|
|
77
|
+
event_handler.call(events)
|
78
|
+
end
|
79
|
+
end
|
7
80
|
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module FourthDimensional
|
2
|
+
# == FourthDimensional::AggregateRoot
|
3
|
+
#
|
4
|
+
# An aggregate root is an object whose entire state is built by applying
|
5
|
+
# events in sequential order.
|
6
|
+
#
|
7
|
+
# Often you will see an aggregate root providing a method to create the event
|
8
|
+
# followed by a event binding to apply the changes to the current object.
|
9
|
+
#
|
10
|
+
# class Post < FourthDimensional::AggregateRoot
|
11
|
+
# attr_reader :state, :title
|
12
|
+
#
|
13
|
+
# def initialize(*args)
|
14
|
+
# super
|
15
|
+
#
|
16
|
+
# @state = :draft
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def add(title:)
|
20
|
+
# apply PostAdded, data: { title: title }
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def delete
|
24
|
+
# apply PostDeleted
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# on PostAdded do |event|
|
28
|
+
# @state = :added
|
29
|
+
# @title = event.data.fetch('title')
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# on PostDeleted do |event|
|
33
|
+
# @state = :deleted
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# aggregate = Post.new(id: SecureRandom.uuid)
|
38
|
+
# aggregate.state # => :draft
|
39
|
+
# aggregate.title # => nil
|
40
|
+
#
|
41
|
+
# aggregate.add(title: 'post-title')
|
42
|
+
# aggregate.state # => :added
|
43
|
+
# aggregate.title # => 'post-title'
|
44
|
+
#
|
45
|
+
# aggregate.delete
|
46
|
+
# aggregate.state # => :deleted
|
47
|
+
class AggregateRoot
|
48
|
+
include Eventable
|
49
|
+
|
50
|
+
UnknownEventError = Class.new(Error)
|
51
|
+
|
52
|
+
# aggregate id
|
53
|
+
attr_reader :id
|
54
|
+
|
55
|
+
# array of events applied
|
56
|
+
attr_reader :applied_events
|
57
|
+
|
58
|
+
# current version
|
59
|
+
attr_reader :version
|
60
|
+
|
61
|
+
# Initializes an aggregate with an id
|
62
|
+
def initialize(id:)
|
63
|
+
@id = id
|
64
|
+
|
65
|
+
@applied_events = []
|
66
|
+
@version = 1
|
67
|
+
end
|
68
|
+
|
69
|
+
# Applies an event to the aggregate when a callback is bound. **+args+ are
|
70
|
+
# merged with the +id+ of the aggregate.
|
71
|
+
#
|
72
|
+
# Callbacks are invoked within the instance of the aggregate root.
|
73
|
+
def apply(event_class, **args)
|
74
|
+
event = event_class.new(args.merge(aggregate_id: id))
|
75
|
+
apply_existing_event(event)
|
76
|
+
applied_events << event
|
77
|
+
end
|
78
|
+
|
79
|
+
# Calls the event binding without persisting the event being applied. Used
|
80
|
+
# when loading an aggregate from an existing store.
|
81
|
+
#
|
82
|
+
# post.apply_existing_event(title_updated_event)
|
83
|
+
def apply_existing_event(event)
|
84
|
+
callback = self.class.event_bindings[event.class]
|
85
|
+
|
86
|
+
if callback.nil?
|
87
|
+
raise UnknownEventError.new("#{self.class.name} doesn't have a binding for '#{event.class}'")
|
88
|
+
end
|
89
|
+
|
90
|
+
instance_exec(event, &callback)
|
91
|
+
@version = next_version
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def next_version
|
97
|
+
version + 1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "active_model"
|
2
|
+
|
3
|
+
module FourthDimensional
|
4
|
+
# == FourthDimensional::Command
|
5
|
+
#
|
6
|
+
# Commands are the input to create events. They provide an early validation
|
7
|
+
# step to ensuring the data format is correct before validating the current
|
8
|
+
# state of the system.
|
9
|
+
#
|
10
|
+
# class AddPost < FourthDimensional::Command
|
11
|
+
# attributes :title, :body, :published
|
12
|
+
# validates_presence_of :title, :body
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# AddPost.new # => raises ArgumentError.new("missing keywords: aggregate_id, :title, :body, :published)
|
16
|
+
# command = AddPost.new(aggregate_id: '1-2-3', title: 'post-title', body: 'post-body', published: false)
|
17
|
+
# command.valid? # => true
|
18
|
+
# command.aggregate_id # => '1-2-3'
|
19
|
+
# command.title # => 'post-title'
|
20
|
+
# command.body # => 'post-body'
|
21
|
+
# command.published # => false
|
22
|
+
#
|
23
|
+
# command.to_h # => {'title' => 'post-title', 'body' => 'post-body', 'published' => false}
|
24
|
+
class Command
|
25
|
+
include ActiveModel::Validations
|
26
|
+
|
27
|
+
# The aggregate the command is acting on
|
28
|
+
attr_reader :aggregate_id
|
29
|
+
|
30
|
+
def initialize(aggregate_id:)
|
31
|
+
@aggregate_id = aggregate_id
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_h
|
35
|
+
{}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Defines an initializer with required keyword arguments, readonly only
|
39
|
+
# attributes and +to_h+ to access all defined attributes
|
40
|
+
#
|
41
|
+
# class AddPost < FourthDimensional::Command
|
42
|
+
# attributes :title, :body, :published
|
43
|
+
# end
|
44
|
+
def self.attributes(*attributes)
|
45
|
+
attr_reader *attributes
|
46
|
+
|
47
|
+
method_arguments = attributes.map { |arg| "#{arg}:" }.join(', ')
|
48
|
+
method_assignments = attributes.map { |arg| "@#{arg} = #{arg}" }.join(';')
|
49
|
+
method_attrs_to_hash = attributes.map { |arg| "'#{arg}' => #{arg}" }.join(',')
|
50
|
+
|
51
|
+
class_eval <<~CODE
|
52
|
+
def initialize(aggregate_id:, #{method_arguments})
|
53
|
+
@aggregate_id = aggregate_id
|
54
|
+
#{method_assignments}
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_h
|
58
|
+
{#{method_attrs_to_hash}}
|
59
|
+
end
|
60
|
+
CODE
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module FourthDimensional
|
2
|
+
# == FourthDimensional::CommandHandler
|
3
|
+
#
|
4
|
+
# Command handlers have bindings that wrap a Command to load an aggregate and
|
5
|
+
# apply events.
|
6
|
+
#
|
7
|
+
# class PostCommandHandler < FourthDimensional::CommandHandler
|
8
|
+
# on AddPost do |command|
|
9
|
+
# with_aggregate(PostAggregate, command) do |post|
|
10
|
+
# post.add(title: command.title, body: command.body)
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# # manually load and save aggregate
|
15
|
+
# on UpdateTitle do |command|
|
16
|
+
# post = repository.load_aggregate(PostAggregate, command.aggregate_id)
|
17
|
+
# post.update_title(title: command.title)
|
18
|
+
# save(command, post)
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# on PublishPost do |command|
|
22
|
+
# with_aggregate(PostAggregate, command) do |post|
|
23
|
+
# post.publish
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
class CommandHandler
|
28
|
+
include Eventable
|
29
|
+
|
30
|
+
attr_reader :repository
|
31
|
+
|
32
|
+
class CommandAndEvents
|
33
|
+
attr_reader :command, :events
|
34
|
+
|
35
|
+
def initialize(command:, events:)
|
36
|
+
@command = command
|
37
|
+
@events = events
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(other)
|
41
|
+
self.class == other.class && command == other.command && events == other.events
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(repository:)
|
46
|
+
@repository = repository
|
47
|
+
end
|
48
|
+
|
49
|
+
# Invokes a callback for an command.
|
50
|
+
def call(command)
|
51
|
+
callback = self.class.event_bindings[command.class]
|
52
|
+
return if callback.nil?
|
53
|
+
instance_exec(command, &callback)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Yields the aggregate and saves the applied events
|
57
|
+
def with_aggregate(aggregate_class, command, &block)
|
58
|
+
aggregate = repository.load_aggregate(aggregate_class, command.aggregate_id)
|
59
|
+
yield aggregate
|
60
|
+
save(command, aggregate)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Saves the command and aggregate's applied events
|
64
|
+
#
|
65
|
+
# class PostCommandHandler < FourthDimensional::CommandHandler
|
66
|
+
# on AddPost do |command|
|
67
|
+
# post = repository.load_aggregate(PostAggregate, command.aggregate_id)
|
68
|
+
# post.add(title: command.title)
|
69
|
+
# save(command, post)
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
def save(command, aggregate)
|
73
|
+
repository.save_command_and_events(CommandAndEvents.new(
|
74
|
+
command: command,
|
75
|
+
events: aggregate.applied_events
|
76
|
+
))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module FourthDimensional
|
2
|
+
# == FourthDimensional::Configuration
|
3
|
+
#
|
4
|
+
# FourthDimensional.configure do |config|
|
5
|
+
# config.command_handlers = [...]
|
6
|
+
# config.event_handlers = [...]
|
7
|
+
# config.event_loader = FourthDimensional::ActiveRecord::EventLoader.new
|
8
|
+
# end
|
9
|
+
class Configuration
|
10
|
+
# An array of command handlers
|
11
|
+
attr_accessor :command_handlers
|
12
|
+
|
13
|
+
# An array of event handlers
|
14
|
+
attr_accessor :event_handlers
|
15
|
+
|
16
|
+
# The event loader
|
17
|
+
attr_accessor :event_loader
|
18
|
+
|
19
|
+
# The table prefix
|
20
|
+
attr_accessor :table_prefix
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@command_handlers = []
|
24
|
+
@event_handlers = []
|
25
|
+
@table_prefix = 'fourd_'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,3 +1,6 @@
|
|
1
|
+
require "active_support/core_ext/hash/keys"
|
2
|
+
require "active_support/inflector"
|
3
|
+
|
1
4
|
module FourthDimensional
|
2
5
|
# == FourthDimensional::Event
|
3
6
|
#
|
@@ -29,6 +32,9 @@ module FourthDimensional
|
|
29
32
|
# hash of data with stringified keys
|
30
33
|
attr_reader :data, :metadata
|
31
34
|
|
35
|
+
# persisted event attributes
|
36
|
+
attr_reader :id, :version, :created_at, :updated_at
|
37
|
+
|
32
38
|
# Initializes an event with the required +aggregate_id+ and optional +data+
|
33
39
|
# and +metadata+.
|
34
40
|
#
|
@@ -41,14 +47,33 @@ module FourthDimensional
|
|
41
47
|
# strings to accommodate deserializing the values from json.
|
42
48
|
#
|
43
49
|
# event = MyEvent.new(aggregate_id: '1-2-3',
|
50
|
+
# version: 1,
|
44
51
|
# data: { one: 1 },
|
45
52
|
# metadata: { two: 2 })
|
53
|
+
# event.version # => 1
|
46
54
|
# event.data # => { 'one' => 1 }
|
47
55
|
# event.metadata # => { 'two' => 2 }
|
48
|
-
def initialize(aggregate_id:,
|
56
|
+
def initialize(aggregate_id:,
|
57
|
+
id: nil,
|
58
|
+
version: nil,
|
59
|
+
created_at: nil,
|
60
|
+
updated_at: nil,
|
61
|
+
data: nil,
|
62
|
+
metadata: nil)
|
49
63
|
@aggregate_id = aggregate_id
|
50
|
-
@
|
51
|
-
@
|
64
|
+
@id = id
|
65
|
+
@version = version
|
66
|
+
@created_at = created_at
|
67
|
+
@updated_at = updated_at
|
68
|
+
@data = (data || {}).transform_keys(&:to_s)
|
69
|
+
@metadata = (metadata || {}).transform_keys(&:to_s)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Underscored event type
|
73
|
+
#
|
74
|
+
# Products::Events::Created.new.type # => "products/events/created"
|
75
|
+
def type
|
76
|
+
self.class.name.underscore
|
52
77
|
end
|
53
78
|
end
|
54
79
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module FourthDimensional
|
4
|
+
module EventLoaders
|
5
|
+
# == FourthDimensional::EventLoaders::ActiveRecord
|
6
|
+
#
|
7
|
+
# FourthDimensional.configure do |config|
|
8
|
+
# config.event_loader = FourthDimensional::EventLoaders::ActiveRecord.new
|
9
|
+
# end
|
10
|
+
class ActiveRecord
|
11
|
+
# Loads events by +aggregate_id+ and deserializes them.
|
12
|
+
#
|
13
|
+
# FourthDimensional.config.event_loader.for_aggregate(aggregate_id)
|
14
|
+
def for_aggregate(aggregate_id)
|
15
|
+
Event.where(aggregate_id: aggregate_id)
|
16
|
+
.order(:version)
|
17
|
+
.map(&method(:deserialize_event))
|
18
|
+
end
|
19
|
+
|
20
|
+
# Deserializes a single event.
|
21
|
+
def deserialize_event(event)
|
22
|
+
event.event_type.camelize.constantize.new(aggregate_id: event.aggregate_id,
|
23
|
+
id: event.id,
|
24
|
+
version: event.version,
|
25
|
+
data: event.data,
|
26
|
+
metadata: event.metadata,
|
27
|
+
created_at: event.created_at,
|
28
|
+
updated_at: event.updated_at)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Saves commands and events in active record compatible database.
|
32
|
+
def save_commands_and_events(commands:, events:)
|
33
|
+
FourthDimensionalRecord.transaction do
|
34
|
+
create_commands!(commands)
|
35
|
+
create_events!(events).map(&method(:deserialize_event))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class FourthDimensionalRecord < ::ActiveRecord::Base # :nodoc:
|
40
|
+
self.abstract_class = true
|
41
|
+
end
|
42
|
+
|
43
|
+
class Command < FourthDimensionalRecord # :nodoc:
|
44
|
+
self.table_name = FourthDimensional.config.table_prefix + 'commands'
|
45
|
+
|
46
|
+
serialize :data, JSON
|
47
|
+
end
|
48
|
+
|
49
|
+
class Event < FourthDimensionalRecord # :nodoc:
|
50
|
+
self.table_name = FourthDimensional.config.table_prefix + 'events'
|
51
|
+
|
52
|
+
serialize :data, JSON
|
53
|
+
serialize :metadata, JSON
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def create_commands!(commands)
|
59
|
+
commands.each do |command|
|
60
|
+
Command.create!(aggregate_id: command.aggregate_id,
|
61
|
+
command_type: command.class.name.underscore,
|
62
|
+
data: command.to_h)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_events!(events)
|
67
|
+
versions = aggregate_versions
|
68
|
+
events.map do |event|
|
69
|
+
version = versions[event.aggregate_id] += 1
|
70
|
+
Event.create!(uuid: SecureRandom.uuid,
|
71
|
+
aggregate_id: event.aggregate_id,
|
72
|
+
version: version,
|
73
|
+
event_type: event.type,
|
74
|
+
data: event.data,
|
75
|
+
metadata: event.metadata)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def aggregate_versions
|
80
|
+
Hash.new { |hash, key| hash[key] = 0 }
|
81
|
+
.merge(Event.group(:aggregate_id).maximum(:version).to_h)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module FourthDimensional
|
2
|
+
# == Eventable
|
3
|
+
#
|
4
|
+
# Provides an api for registering event bindings.
|
5
|
+
#
|
6
|
+
# class CantHandleTheTruth
|
7
|
+
# include FourthDimensional::Eventable
|
8
|
+
#
|
9
|
+
# on TheTruth do |event|
|
10
|
+
# raise RunTimeError.new("an error occured that can not be rescued")
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
module Eventable
|
14
|
+
def self.included(klass)
|
15
|
+
klass.extend ClassMethods
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
# Binds an event to the aggregate. Raises a +KeyError+ if the event has
|
20
|
+
# already been bound.
|
21
|
+
#
|
22
|
+
# Post.on(PostAdded, -> (event) {})
|
23
|
+
# Post.on(PostAdded, -> (event) {}) # => raises KeyError
|
24
|
+
def on(klass, &block)
|
25
|
+
if event_bindings.has_key?(klass)
|
26
|
+
raise KeyError.new("#{klass.name} is already bound on #{self.name}")
|
27
|
+
end
|
28
|
+
|
29
|
+
event_bindings[klass] = block
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns an array of class names for the bound events.
|
33
|
+
#
|
34
|
+
# Post.on(PostAdded, -> (event) {})
|
35
|
+
# Post.on(PostDeleted, -> (event) {})
|
36
|
+
#
|
37
|
+
# Post.events # => [PostAdded, PostDeleted]
|
38
|
+
def events
|
39
|
+
event_bindings.keys
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns a hash of event classes and the callback.
|
43
|
+
#
|
44
|
+
# Post.on(PostAdded, -> (event) {})
|
45
|
+
# Post.on(PostDeleted, -> (event) {})
|
46
|
+
#
|
47
|
+
# Post.event_bindings # => {
|
48
|
+
# PostAdded => Proc,
|
49
|
+
# PostDeleted => Proc
|
50
|
+
# }
|
51
|
+
def event_bindings
|
52
|
+
@event_bindings ||= {}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
2
|
+
|
3
|
+
module FourthDimensional
|
4
|
+
# == FourthDimensional::RecordProjector
|
5
|
+
#
|
6
|
+
# class PostProjector < FourthDimensional::RecordProjector
|
7
|
+
# self.record_class = 'Post'
|
8
|
+
#
|
9
|
+
# on TitleChanged do |event|
|
10
|
+
# record.title = event.title
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
class RecordProjector
|
14
|
+
include Eventable
|
15
|
+
|
16
|
+
# Record class this projector creates
|
17
|
+
class_attribute :record_class
|
18
|
+
|
19
|
+
# The current instance of the projected record
|
20
|
+
attr_reader :record
|
21
|
+
|
22
|
+
def initialize(aggregate_id:)
|
23
|
+
@record = record_class.constantize.find_or_initialize_by(id: aggregate_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Invokes the event binding.
|
27
|
+
#
|
28
|
+
# post_projector.apply_event(TitleChanged.new(aggregate_id: aggregate_id,
|
29
|
+
# data: {title: 'new-post-title'}))
|
30
|
+
def apply_event(event)
|
31
|
+
callback = self.class.event_bindings[event.class]
|
32
|
+
return if callback.nil?
|
33
|
+
instance_exec(event, &callback)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Saves the record at it's current state.
|
37
|
+
#
|
38
|
+
# post_projector.save
|
39
|
+
def save
|
40
|
+
record.save
|
41
|
+
end
|
42
|
+
|
43
|
+
# Applies multiple events and saves at the end.
|
44
|
+
#
|
45
|
+
# projector.record.persisted? # => false
|
46
|
+
# projector.call(event1, event2)
|
47
|
+
# projector.record.persisted? # => true
|
48
|
+
def call(*events)
|
49
|
+
events.flatten.map(&method(:apply_event))
|
50
|
+
save
|
51
|
+
end
|
52
|
+
|
53
|
+
# Bulk apply and save events to multiple aggregates.
|
54
|
+
def self.call(*events)
|
55
|
+
events.flatten.group_by(&:aggregate_id).each do |aggregate_id, events|
|
56
|
+
new(aggregate_id: aggregate_id).call(events)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module FourthDimensional
|
2
|
+
# == FourthDimensional::Repository
|
3
|
+
#
|
4
|
+
# Event sourcing is a good application for the repository pattern since we
|
5
|
+
# need to have a single source track commands and events being applied to the
|
6
|
+
# system.
|
7
|
+
#
|
8
|
+
# The FourthDimensional::Repository is a wrapper around loading and persisting
|
9
|
+
# events/commands with dependency injection. This allows new repositories to
|
10
|
+
# be defined and easily registered.
|
11
|
+
#
|
12
|
+
# The loading and persisting of events/commands are separated to allow
|
13
|
+
# separating the reading and writing databases should that become necessary.
|
14
|
+
#
|
15
|
+
# class InMemoryEvents
|
16
|
+
# attr_reader :events
|
17
|
+
#
|
18
|
+
# def initialize
|
19
|
+
# @events = []
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# def self.instance
|
23
|
+
# @instance ||= InMemoryEvents.new
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# class InMemoryEventLoader
|
28
|
+
# def for_aggregate(aggregate_id)
|
29
|
+
# InMemoryEvents.events
|
30
|
+
# .filter { |event| event.aggregate_id == aggregate_id }
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# def save_commands_and_events(commands:, events:)
|
34
|
+
# InMemoryEvents.events.concat(events)
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# FourthDimensional.configure do |config|
|
39
|
+
# config.event_loader = InMemoryEventLoader.new
|
40
|
+
# end
|
41
|
+
class Repository
|
42
|
+
# The source to load events
|
43
|
+
attr_reader :event_loader
|
44
|
+
|
45
|
+
# An array of events saved
|
46
|
+
attr_reader :applied_events
|
47
|
+
|
48
|
+
# An array of commands called
|
49
|
+
attr_reader :called_commands
|
50
|
+
|
51
|
+
def initialize(event_loader:)
|
52
|
+
@event_loader = event_loader
|
53
|
+
@applied_events = []
|
54
|
+
@called_commands = []
|
55
|
+
end
|
56
|
+
|
57
|
+
# Delegates to +event_loader#for_aggregate+
|
58
|
+
#
|
59
|
+
# FourthDimensional.repository.events_for_aggregate(aggregate_id)
|
60
|
+
def events_for_aggregate(aggregate_id)
|
61
|
+
event_loader.for_aggregate(aggregate_id)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Loads events from +event_loader+ and applies them to a new instance of
|
65
|
+
# +aggregate_class+
|
66
|
+
#
|
67
|
+
# FourthDimensional.repository.load_aggregate(PostAggregate, aggregate_id) # => PostAggregate
|
68
|
+
def load_aggregate(aggregate_class, aggregate_id)
|
69
|
+
events_for_aggregate(aggregate_id)
|
70
|
+
.reduce(aggregate_class.new(id: aggregate_id)) do |aggregate, event|
|
71
|
+
aggregate.apply_existing_event(event)
|
72
|
+
aggregate
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Saves the command and events with the +event_loader+
|
77
|
+
#
|
78
|
+
# repository.save_command_and_events(FourthDimensional::CommandHandler::CommandAndEvents.new(
|
79
|
+
# command: AddPost,
|
80
|
+
# events: [PostAdded]
|
81
|
+
# ))
|
82
|
+
def save_command_and_events(command_and_events)
|
83
|
+
called_commands << command_and_events.command
|
84
|
+
applied_events.concat(command_and_events.events)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module FourthDimensional
|
4
|
+
class InstallGenerator < ::Rails::Generators::Base
|
5
|
+
source_root File.expand_path('templates', __dir__)
|
6
|
+
|
7
|
+
def initializer
|
8
|
+
template 'initializer.rb', 'config/initializers/fourth_dimensional.rb'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module FourthDimensional
|
5
|
+
class MigrationGenerator < ::Rails::Generators::Base
|
6
|
+
include ::Rails::Generators::Migration
|
7
|
+
source_root File.expand_path('templates', __dir__)
|
8
|
+
|
9
|
+
def install
|
10
|
+
migration_template(
|
11
|
+
'migration.rb.erb',
|
12
|
+
'db/migrate/create_fourth_dimensional_tables.rb',
|
13
|
+
migration_version: migration_version
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.next_migration_number(dirname)
|
18
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def migration_version
|
24
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
25
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fourth_dimensional
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Baylor Rae'
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-02-
|
11
|
+
date: 2019-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activemodel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activerecord
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
13
55
|
- !ruby/object:Gem::Dependency
|
14
56
|
name: bundler
|
15
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +108,40 @@ dependencies:
|
|
66
108
|
- - "~>"
|
67
109
|
- !ruby/object:Gem::Version
|
68
110
|
version: '1.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: simplecov
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.16'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.16'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: sqlite3
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.3'
|
132
|
+
- - "<"
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '1.4'
|
135
|
+
type: :development
|
136
|
+
prerelease: false
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - "~>"
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '1.3'
|
142
|
+
- - "<"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '1.4'
|
69
145
|
description: |-
|
70
146
|
Fourth Dimensional is an event sourcing library to account for the state of a
|
71
147
|
system in relation to time.
|
@@ -75,19 +151,22 @@ executables: []
|
|
75
151
|
extensions: []
|
76
152
|
extra_rdoc_files: []
|
77
153
|
files:
|
78
|
-
- ".gitignore"
|
79
|
-
- ".rspec"
|
80
|
-
- ".travis.yml"
|
81
|
-
- Gemfile
|
82
|
-
- Gemfile.lock
|
83
|
-
- README.md
|
84
|
-
- Rakefile
|
85
|
-
- bin/console
|
86
|
-
- bin/setup
|
87
|
-
- fourth_dimensional.gemspec
|
88
154
|
- lib/fourth_dimensional.rb
|
155
|
+
- lib/fourth_dimensional/aggregate_root.rb
|
156
|
+
- lib/fourth_dimensional/command.rb
|
157
|
+
- lib/fourth_dimensional/command_handler.rb
|
158
|
+
- lib/fourth_dimensional/configuration.rb
|
89
159
|
- lib/fourth_dimensional/event.rb
|
160
|
+
- lib/fourth_dimensional/event_loaders.rb
|
161
|
+
- lib/fourth_dimensional/event_loaders/active_record.rb
|
162
|
+
- lib/fourth_dimensional/eventable.rb
|
163
|
+
- lib/fourth_dimensional/railtie.rb
|
164
|
+
- lib/fourth_dimensional/record_projector.rb
|
165
|
+
- lib/fourth_dimensional/repository.rb
|
90
166
|
- lib/fourth_dimensional/version.rb
|
167
|
+
- lib/generators/fourth_dimensional/install_generator.rb
|
168
|
+
- lib/generators/fourth_dimensional/migration_generator.rb
|
169
|
+
- lib/generators/fourth_dimensional/templates/initializer.rb
|
91
170
|
homepage: https://github.com/baylorrae/fourth_dimensional
|
92
171
|
licenses: []
|
93
172
|
metadata: {}
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/.travis.yml
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
---
|
2
|
-
sudo: false
|
3
|
-
language: ruby
|
4
|
-
cache: bundler
|
5
|
-
rvm:
|
6
|
-
- 2.6.1
|
7
|
-
before_install: gem install bundler -v 1.17.2
|
8
|
-
deploy:
|
9
|
-
local-dir: ./doc
|
10
|
-
provider: pages
|
11
|
-
skip-cleanup: true
|
12
|
-
github-token: $GITHUB_TOKEN
|
13
|
-
keep-history: true
|
14
|
-
on:
|
15
|
-
branch: master
|
data/Gemfile
DELETED
data/Gemfile.lock
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
fourth_dimensional (0.1.0)
|
5
|
-
|
6
|
-
GEM
|
7
|
-
remote: https://rubygems.org/
|
8
|
-
specs:
|
9
|
-
diff-lcs (1.3)
|
10
|
-
rake (10.5.0)
|
11
|
-
rdoc (6.1.1)
|
12
|
-
rspec (3.8.0)
|
13
|
-
rspec-core (~> 3.8.0)
|
14
|
-
rspec-expectations (~> 3.8.0)
|
15
|
-
rspec-mocks (~> 3.8.0)
|
16
|
-
rspec-core (3.8.0)
|
17
|
-
rspec-support (~> 3.8.0)
|
18
|
-
rspec-expectations (3.8.2)
|
19
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
20
|
-
rspec-support (~> 3.8.0)
|
21
|
-
rspec-mocks (3.8.0)
|
22
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
23
|
-
rspec-support (~> 3.8.0)
|
24
|
-
rspec-support (3.8.0)
|
25
|
-
sdoc (1.0.0)
|
26
|
-
rdoc (>= 5.0)
|
27
|
-
|
28
|
-
PLATFORMS
|
29
|
-
ruby
|
30
|
-
|
31
|
-
DEPENDENCIES
|
32
|
-
bundler (~> 1.17)
|
33
|
-
fourth_dimensional!
|
34
|
-
rake (~> 10.0)
|
35
|
-
rspec (~> 3.0)
|
36
|
-
sdoc (~> 1.0)
|
37
|
-
|
38
|
-
BUNDLED WITH
|
39
|
-
1.17.2
|
data/README.md
DELETED
@@ -1,123 +0,0 @@
|
|
1
|
-
# Fourth Dimensional
|
2
|
-
|
3
|
-
Fourth Dimensional is an event sourcing library to account for the state of a
|
4
|
-
system in relation to time.
|
5
|
-
|
6
|
-
This project is a lightweight dsl for commands and events for use with
|
7
|
-
ActiveRecord. Eventually I'd like it to support Sequel as well.
|
8
|
-
|
9
|
-
[RDoc][rdoc_url]
|
10
|
-
|
11
|
-
[![Build Status]][travis_status]
|
12
|
-
|
13
|
-
## Installation
|
14
|
-
|
15
|
-
Add this line to your application's Gemfile:
|
16
|
-
|
17
|
-
```ruby
|
18
|
-
gem 'fourth_dimensional'
|
19
|
-
```
|
20
|
-
|
21
|
-
And then execute:
|
22
|
-
|
23
|
-
$ bundle
|
24
|
-
|
25
|
-
Or install it yourself as:
|
26
|
-
|
27
|
-
$ gem install fourth_dimensional
|
28
|
-
|
29
|
-
## Usage
|
30
|
-
|
31
|
-
This API is in flux, but here's the general idea.
|
32
|
-
|
33
|
-
```ruby
|
34
|
-
class PostAdded < FourthDimensional::Event
|
35
|
-
def title
|
36
|
-
data.fetch('title')
|
37
|
-
end
|
38
|
-
|
39
|
-
def body
|
40
|
-
data.fetch('body')
|
41
|
-
end
|
42
|
-
|
43
|
-
def published
|
44
|
-
data.fetch('published')
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
class AddPost < FourthDimensional::Command
|
49
|
-
attributes :title, :body, :published
|
50
|
-
validates_presence_of :title, :body
|
51
|
-
end
|
52
|
-
|
53
|
-
class PostCommandHandler < FourthDimensional::CommandHandler
|
54
|
-
on AddPost do |command|
|
55
|
-
with_aggregate(PostAggregate, command) do |post|
|
56
|
-
post.add(title: command.title,
|
57
|
-
body: command.body,
|
58
|
-
published: command.published)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
class PostAggregate < FourthDimensional::AggregateRoot
|
64
|
-
def add(title:, body:, published:)
|
65
|
-
apply(PostAdded,
|
66
|
-
data: {
|
67
|
-
title: title,
|
68
|
-
body: body,
|
69
|
-
published: published
|
70
|
-
}
|
71
|
-
)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
class PostProjector < FourthDimensional::RecordProjector
|
76
|
-
self.record_class = 'Post'
|
77
|
-
|
78
|
-
on PostAdded do |event|
|
79
|
-
record.title = event.title
|
80
|
-
record.body = event.body
|
81
|
-
record.published = event.published
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def main
|
86
|
-
FourthDimensional.configure do |config|
|
87
|
-
config.event_handlers = [
|
88
|
-
PostCommandHandler,
|
89
|
-
PostProjector
|
90
|
-
]
|
91
|
-
end
|
92
|
-
|
93
|
-
aggregate_id = SecureRandom.uuid
|
94
|
-
|
95
|
-
command = AddPost.new(aggregate_id: aggregate_id,
|
96
|
-
title: 'post-title',
|
97
|
-
body: 'post-body',
|
98
|
-
published: true)
|
99
|
-
|
100
|
-
# persists command and event if successful
|
101
|
-
FourthDimensional.execute_commands(command)
|
102
|
-
|
103
|
-
post = Post.find(aggregate_id)
|
104
|
-
expect(post.title).to eq('post-title')
|
105
|
-
expect(post.body).to eq('post-body')
|
106
|
-
expect(post.published).to be_truthy
|
107
|
-
end
|
108
|
-
```
|
109
|
-
|
110
|
-
## Development
|
111
|
-
|
112
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
113
|
-
|
114
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
115
|
-
|
116
|
-
## Contributing
|
117
|
-
|
118
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/baylorrae/fourth_dimensional.
|
119
|
-
|
120
|
-
[rdoc_url]: https://baylorrae.com/fourth_dimensional
|
121
|
-
|
122
|
-
[Build Status]: https://travis-ci.org/BaylorRae/fourth_dimensional.svg?branch=master
|
123
|
-
[travis_status]: https://travis-ci.org/BaylorRae/fourth_dimensional
|
data/Rakefile
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
require "bundler/gem_tasks"
|
2
|
-
require "rspec/core/rake_task"
|
3
|
-
require "sdoc"
|
4
|
-
require "rdoc/task"
|
5
|
-
|
6
|
-
RSpec::Core::RakeTask.new(:spec)
|
7
|
-
|
8
|
-
RDoc::Task.new do |rdoc|
|
9
|
-
rdoc.rdoc_dir = 'doc'
|
10
|
-
rdoc.options << '--format=sdoc'
|
11
|
-
rdoc.options << '--github'
|
12
|
-
rdoc.main = 'README.md'
|
13
|
-
rdoc.rdoc_files.include('README.md', 'lib/**/*.rb')
|
14
|
-
end
|
15
|
-
|
16
|
-
task :default => [:spec, :rdoc]
|
data/bin/console
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require "bundler/setup"
|
4
|
-
require "fourth_dimensional"
|
5
|
-
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start(__FILE__)
|
data/bin/setup
DELETED
data/fourth_dimensional.gemspec
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path("../lib", __FILE__)
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require "fourth_dimensional/version"
|
5
|
-
|
6
|
-
Gem::Specification.new do |spec|
|
7
|
-
spec.name = "fourth_dimensional"
|
8
|
-
spec.version = FourthDimensional::VERSION
|
9
|
-
spec.authors = ["Baylor Rae'"]
|
10
|
-
spec.email = ["baylor@thecodedeli.com"]
|
11
|
-
|
12
|
-
spec.summary = %q{Fourth Dimensional is an event sourcing library to account for the state of a
|
13
|
-
system in relation to time.}
|
14
|
-
spec.description = %q{Fourth Dimensional is an event sourcing library to account for the state of a
|
15
|
-
system in relation to time.}
|
16
|
-
spec.homepage = "https://github.com/baylorrae/fourth_dimensional"
|
17
|
-
|
18
|
-
# Specify which files should be added to the gem when it is released.
|
19
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
-
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
21
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
-
end
|
23
|
-
spec.bindir = "exe"
|
24
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
-
spec.require_paths = ["lib"]
|
26
|
-
|
27
|
-
spec.add_development_dependency "bundler", "~> 1.17"
|
28
|
-
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
-
spec.add_development_dependency "rspec", "~> 3.0"
|
30
|
-
spec.add_development_dependency "sdoc", "~> 1.0"
|
31
|
-
end
|