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 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