statefully 0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 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
@@ -0,0 +1,2 @@
1
+ require 'statefully/diff'
2
+ require 'statefully/state'
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
@@ -0,0 +1,5 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ require 'pry'
5
+ require 'statefully'
@@ -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