statefully 0.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/lib/statefully/diff.rb +131 -0
- data/lib/statefully/state.rb +146 -0
- data/lib/statefully.rb +2 -0
- data/spec/diff_spec.rb +42 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/state_spec.rb +80 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 325ca9dcf556d7c2493333c687e6f2c1046bc2c8
|
4
|
+
data.tar.gz: 8b7127063fd59e6ddefb39b6a945b88bd46f09b2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 20a634a516a98ca760fafad39b6df32e03415f6e5a46f55726ba179c6b3dde95c8e3ca153c76c697731fc34659f4a20a6dc850174a35ef3529a16ea348ae37d8
|
7
|
+
data.tar.gz: 5c5c8195b0317a7f2edba1ae64f27971f719aa49068a2cebc6146d7b67e9a7e666286d8c21547b13d6e053db0c85c501be146038eebd9c4470da50204c95717c
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module Statefully
|
5
|
+
class Diff
|
6
|
+
attr_reader :added, :changed
|
7
|
+
|
8
|
+
def self.create(current, previous)
|
9
|
+
changes = Builder.new(current, previous).build
|
10
|
+
changes.empty? ? None.instance : new(**changes).freeze
|
11
|
+
end
|
12
|
+
|
13
|
+
def empty?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
"<#{self.class.name} #{inspect_details}>"
|
19
|
+
end
|
20
|
+
|
21
|
+
def added?(key)
|
22
|
+
added.key?(key)
|
23
|
+
end
|
24
|
+
|
25
|
+
def changed?(key)
|
26
|
+
changed.key?(key)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def inspect_details
|
32
|
+
[inspect_added, inspect_changed].compact.join(', ')
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect_added
|
36
|
+
added.empty? ? nil : "added=#{Inspect.from_hash(added)}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def inspect_changed
|
40
|
+
changed.empty? ? nil : "changed=#{Inspect.from_hash(changed)}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(added:, changed:)
|
44
|
+
@added = added.freeze
|
45
|
+
@changed = changed.freeze
|
46
|
+
end
|
47
|
+
|
48
|
+
class None
|
49
|
+
include Singleton
|
50
|
+
|
51
|
+
def empty?
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
def added
|
56
|
+
{}
|
57
|
+
end
|
58
|
+
|
59
|
+
def changed
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
def inspect
|
64
|
+
"<#{self.class.name}>"
|
65
|
+
end
|
66
|
+
end # class None
|
67
|
+
|
68
|
+
class Change
|
69
|
+
attr_reader :current, :previous
|
70
|
+
|
71
|
+
def initialize(current, previous)
|
72
|
+
@current = current
|
73
|
+
@previous = previous
|
74
|
+
end
|
75
|
+
|
76
|
+
def none?
|
77
|
+
@current == @previous
|
78
|
+
end
|
79
|
+
|
80
|
+
def inspect
|
81
|
+
"#<#{self.class.name} " \
|
82
|
+
"#{Inspect.from_fields(current: current, previous: previous)}>"
|
83
|
+
end
|
84
|
+
end # class Change
|
85
|
+
|
86
|
+
class Builder
|
87
|
+
def initialize(current, previous)
|
88
|
+
@current = current
|
89
|
+
@previous = previous
|
90
|
+
end
|
91
|
+
|
92
|
+
def build
|
93
|
+
empty? ? {} : { added: added, changed: changed }
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def added
|
99
|
+
@added ||=
|
100
|
+
(current_keys - previous_keys)
|
101
|
+
.map { |key| [key, @current.fetch(key)] }
|
102
|
+
.to_h
|
103
|
+
end
|
104
|
+
|
105
|
+
def changed
|
106
|
+
@changed ||=
|
107
|
+
(current_keys & previous_keys)
|
108
|
+
.map { |key| [key, change_for(key)] }
|
109
|
+
.to_h
|
110
|
+
.reject { |_, val| val.none? }
|
111
|
+
end
|
112
|
+
|
113
|
+
def change_for(key)
|
114
|
+
Change.new(@current.fetch(key), @previous.fetch(key)).freeze
|
115
|
+
end
|
116
|
+
|
117
|
+
def empty?
|
118
|
+
added.empty? && changed.empty?
|
119
|
+
end
|
120
|
+
|
121
|
+
def current_keys
|
122
|
+
Set.new(@current.keys)
|
123
|
+
end
|
124
|
+
|
125
|
+
def previous_keys
|
126
|
+
Set.new(@previous.keys)
|
127
|
+
end
|
128
|
+
end # class Builder
|
129
|
+
private_constant :Builder
|
130
|
+
end # class Diff
|
131
|
+
end # module Statefully
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module Statefully
|
5
|
+
class State
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_reader :previous
|
9
|
+
def_delegators :@_members, :key?, :keys, :fetch
|
10
|
+
|
11
|
+
def self.create(**values)
|
12
|
+
Success.send(:new, values, previous: None.instance).freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
def diff
|
16
|
+
Diff.create(self, previous)
|
17
|
+
end
|
18
|
+
|
19
|
+
def history
|
20
|
+
([diff] + previous.history).freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
def success?
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def resolve
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
inspect_details({})
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_reader :_members
|
38
|
+
|
39
|
+
def initialize(values, previous:)
|
40
|
+
@_members = values.freeze
|
41
|
+
@previous = previous
|
42
|
+
end
|
43
|
+
private_class_method :new
|
44
|
+
|
45
|
+
def inspect_details(extras)
|
46
|
+
details = [self.class.name]
|
47
|
+
fields = _members.merge(extras)
|
48
|
+
details << Inspect.from_fields(fields) unless fields.empty?
|
49
|
+
"#<#{details.join(' ')}>"
|
50
|
+
end
|
51
|
+
|
52
|
+
# This method reeks of :reek:TooManyStatements.
|
53
|
+
def method_missing(name, *args, &block)
|
54
|
+
sym_name = name.to_sym
|
55
|
+
return fetch(sym_name) if key?(sym_name)
|
56
|
+
str_name = name.to_s
|
57
|
+
modifier = str_name[-1]
|
58
|
+
return super unless %w[? !].include?(modifier)
|
59
|
+
base = str_name[0...-1].to_sym
|
60
|
+
known = key?(base)
|
61
|
+
return known if modifier == '?'
|
62
|
+
return fetch(base) if known
|
63
|
+
raise Missing, base
|
64
|
+
end
|
65
|
+
|
66
|
+
# This method reeks of :reek:BooleanParameter.
|
67
|
+
def respond_to_missing?(name, _include_private = false)
|
68
|
+
str_name = name.to_s
|
69
|
+
key?(name.to_sym) || %w[? !].any?(&str_name.method(:end_with?)) || super
|
70
|
+
end
|
71
|
+
|
72
|
+
class None < State
|
73
|
+
include Singleton
|
74
|
+
|
75
|
+
def history
|
76
|
+
[]
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def initialize
|
82
|
+
@_members = {}.freeze
|
83
|
+
@previous = self
|
84
|
+
end
|
85
|
+
end # class None
|
86
|
+
private_constant :None
|
87
|
+
|
88
|
+
# Success is a not-yet failed State.
|
89
|
+
class Success < State
|
90
|
+
def succeed(**values)
|
91
|
+
self
|
92
|
+
.class
|
93
|
+
.send(:new, _members.merge(values).freeze, previous: self)
|
94
|
+
.freeze
|
95
|
+
end
|
96
|
+
|
97
|
+
def fail(error)
|
98
|
+
Failure.send(:new, _members, error, previous: self).freeze
|
99
|
+
end
|
100
|
+
end # class Success
|
101
|
+
private_constant :Success
|
102
|
+
|
103
|
+
# Failure is a failed State.
|
104
|
+
class Failure < State
|
105
|
+
attr_reader :error
|
106
|
+
|
107
|
+
def initialize(values, error, previous:)
|
108
|
+
super(values, previous: previous)
|
109
|
+
@error = error
|
110
|
+
end
|
111
|
+
|
112
|
+
def diff
|
113
|
+
error
|
114
|
+
end
|
115
|
+
|
116
|
+
def success?
|
117
|
+
false
|
118
|
+
end
|
119
|
+
|
120
|
+
def resolve
|
121
|
+
raise error
|
122
|
+
end
|
123
|
+
|
124
|
+
def inspect
|
125
|
+
inspect_details(error: error.inspect)
|
126
|
+
end
|
127
|
+
end # class Failure
|
128
|
+
|
129
|
+
class Missing < RuntimeError
|
130
|
+
attr_reader :field
|
131
|
+
|
132
|
+
def initialize(field)
|
133
|
+
@field = field
|
134
|
+
super("field '#{field}' missing from state")
|
135
|
+
end
|
136
|
+
end # class Missing
|
137
|
+
|
138
|
+
module Inspect
|
139
|
+
def from_fields(input)
|
140
|
+
input.map { |key, val| "#{key}=#{val.inspect}" }.join(', ')
|
141
|
+
end
|
142
|
+
module_function :from_fields
|
143
|
+
end # module Inspect
|
144
|
+
private_constant :Inspect
|
145
|
+
end # class State
|
146
|
+
end # module Statefully
|
data/lib/statefully.rb
ADDED
data/spec/diff_spec.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Statefully
|
4
|
+
describe Diff do
|
5
|
+
subject { described_class.create(current, previous) }
|
6
|
+
|
7
|
+
context 'when key added' do
|
8
|
+
let(:current) { State.create(key: 'val') }
|
9
|
+
let(:previous) { State.create }
|
10
|
+
|
11
|
+
it { expect(subject).not_to be_empty }
|
12
|
+
it { expect(subject.added).to have_key(:key) }
|
13
|
+
it { expect(subject.added?(:key)).to be_truthy }
|
14
|
+
it { expect(subject.added.fetch(:key)).to eq 'val' }
|
15
|
+
it { expect(subject.changed).to be_empty }
|
16
|
+
end # context 'when key added'
|
17
|
+
|
18
|
+
context 'when key changed' do
|
19
|
+
let(:current) { State.create(key: 'new') }
|
20
|
+
let(:previous) { State.create(key: 'old') }
|
21
|
+
|
22
|
+
it { expect(subject).not_to be_empty }
|
23
|
+
it { expect(subject.added).to be_empty }
|
24
|
+
it { expect(subject.changed).to have_key(:key) }
|
25
|
+
it { expect(subject.changed?(:key)).to be_truthy }
|
26
|
+
|
27
|
+
context 'with change' do
|
28
|
+
let(:change) { subject.changed.fetch(:key) }
|
29
|
+
|
30
|
+
it { expect(change.current).to eq 'new' }
|
31
|
+
it { expect(change.previous).to eq 'old' }
|
32
|
+
end # context 'with change'
|
33
|
+
end # context 'when key changed'
|
34
|
+
|
35
|
+
context 'when nothing changed' do
|
36
|
+
let(:current) { State.create(key: 'key') }
|
37
|
+
let(:previous) { current }
|
38
|
+
|
39
|
+
it { expect(subject).to be_empty }
|
40
|
+
end # context 'when nothing changed'
|
41
|
+
end # describe Diff
|
42
|
+
end # module Statefully
|
data/spec/spec_helper.rb
ADDED
data/spec/state_spec.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Statefully
|
4
|
+
describe State do
|
5
|
+
describe '.create' do
|
6
|
+
subject { described_class.create(key: 'val') }
|
7
|
+
it { expect(subject).to be_success }
|
8
|
+
end # describe '.create'
|
9
|
+
end # describe State
|
10
|
+
|
11
|
+
describe 'State::Success' do
|
12
|
+
let(:val) { 'val' }
|
13
|
+
|
14
|
+
subject { State.create(old_key: val) }
|
15
|
+
|
16
|
+
describe 'methods delegated to the underlying Hash' do
|
17
|
+
it { expect(subject.keys).to eq [:old_key] }
|
18
|
+
it { expect(subject.key?(:old_key)).to be_truthy }
|
19
|
+
end # describe 'methods delegated to the underlying Hash'
|
20
|
+
|
21
|
+
describe "methods dynamically dispatched using 'method_missing'" do
|
22
|
+
it { expect(subject.old_key).to eq val }
|
23
|
+
it { expect(subject.old_key?).to be_truthy }
|
24
|
+
it { expect(subject.old_key!).to eq val }
|
25
|
+
|
26
|
+
it { expect { subject.new_key }.to raise_error NoMethodError }
|
27
|
+
it { expect(subject.new_key?).to be_falsey }
|
28
|
+
it { expect { subject.new_key! }.to raise_error State::Missing }
|
29
|
+
end # describe "methods dynamically dispatched using 'method_missing'"
|
30
|
+
|
31
|
+
describe 'trivial readers' do
|
32
|
+
it { expect(subject.resolve).to eq subject }
|
33
|
+
it { expect(subject).to be_success }
|
34
|
+
end # describe 'trivial readers'
|
35
|
+
|
36
|
+
describe '#succeed' do
|
37
|
+
let(:new_val) { 'new_val' }
|
38
|
+
let(:succeeded) { subject.succeed(new_key: new_val) }
|
39
|
+
|
40
|
+
it { expect(succeeded).to be_success }
|
41
|
+
it { expect(succeeded.old_key).to eq val }
|
42
|
+
it { expect(succeeded.new_key).to eq new_val }
|
43
|
+
it { expect(succeeded.keys).to eq %i[old_key new_key] }
|
44
|
+
it { expect(succeeded.previous).to eq subject }
|
45
|
+
it { expect(succeeded.resolve).to eq succeeded }
|
46
|
+
|
47
|
+
context 'with history' do
|
48
|
+
let(:history) { succeeded.history }
|
49
|
+
|
50
|
+
it { expect(history.size).to eq 2 }
|
51
|
+
it { expect(history.first.added).to include :new_key }
|
52
|
+
it { expect(history.last.added).to include :old_key }
|
53
|
+
end # context 'with history'
|
54
|
+
end # describe '#succeed'
|
55
|
+
|
56
|
+
describe '#fail' do
|
57
|
+
let(:error) { RuntimeError.new('snakes on a plane') }
|
58
|
+
let(:failed) { subject.fail(error) }
|
59
|
+
|
60
|
+
it { expect(failed).not_to be_success }
|
61
|
+
it { expect(failed.old_key).to eq val }
|
62
|
+
it { expect(failed.previous).to eq subject }
|
63
|
+
it { expect(failed.error).to eq error }
|
64
|
+
|
65
|
+
it 'raises passed error on #resolve' do
|
66
|
+
expect { failed.resolve }.to raise_error do |err|
|
67
|
+
expect(err).to eq error
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'with history' do
|
72
|
+
let(:history) { failed.history }
|
73
|
+
|
74
|
+
it { expect(history.size).to eq 2 }
|
75
|
+
it { expect(history.first).to eq error }
|
76
|
+
it { expect(history.last.added).to include :old_key }
|
77
|
+
end # context 'with history'
|
78
|
+
end # describe '#fail'
|
79
|
+
end # describe 'State::Success'
|
80
|
+
end # module Statefully
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: statefully
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Marcin Wyszynski
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-05-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.14'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.14.6
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.14'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.14.6
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rake
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '12.0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '12.0'
|
47
|
+
description: Immutable state with helpers to build awesome things
|
48
|
+
email:
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- lib/statefully.rb
|
54
|
+
- lib/statefully/diff.rb
|
55
|
+
- lib/statefully/state.rb
|
56
|
+
- spec/diff_spec.rb
|
57
|
+
- spec/spec_helper.rb
|
58
|
+
- spec/state_spec.rb
|
59
|
+
homepage: https://github.com/marcinwyszynski/statefully
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
metadata: {}
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 2.6.12
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: Immutable state with helpers to build awesome things
|
83
|
+
test_files:
|
84
|
+
- spec/diff_spec.rb
|
85
|
+
- spec/spec_helper.rb
|
86
|
+
- spec/state_spec.rb
|