errgonomic 0.8.3 → 0.9.0

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.
data/doctest_helper.rb CHANGED
@@ -1,3 +1,156 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_record'
4
+ require 'logger'
5
+
3
6
  require_relative 'lib/errgonomic'
7
+ require_relative 'lib/errgonomic/rails'
8
+
9
+ # The Rails integration patches ActiveRecord as it loads, so examples under
10
+ # lib/errgonomic/rails need a live connection and a model to run against.
11
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
12
+ ActiveRecord::Base.logger = Logger.new(File::NULL)
13
+
14
+ # One nullable column per type the cast boundary has to map.
15
+ ActiveRecord::Schema.verbose = false
16
+ ActiveRecord::Schema.define do
17
+ create_table 'writers', force: :cascade do |t|
18
+ t.string :name
19
+ t.text :bio
20
+ end
21
+
22
+ create_table 'articles', force: :cascade do |t|
23
+ t.string :title
24
+ t.references :writer
25
+ end
26
+
27
+ create_table 'reports', force: :cascade do |t|
28
+ t.string :type, null: false
29
+ t.string :title
30
+ t.text :summary
31
+ end
32
+
33
+ create_table 'notes', force: :cascade do |t|
34
+ t.boolean :pinned
35
+ t.string :title
36
+ t.text :body
37
+ t.json :meta
38
+ t.integer :rank
39
+ t.float :score
40
+ t.decimal :price
41
+ t.date :due_on
42
+ t.datetime :read_at
43
+ end
44
+ end
45
+
46
+ Errgonomic::Rails.setup_before
47
+
48
+ class Note < ActiveRecord::Base
49
+ include Errgonomic::Rails::ActiveRecordOptional
50
+ end
51
+
52
+ # ActionText and ActiveStorage declare singular associations of their own and
53
+ # read them raw. A reflection names its class as a string, so a model can be
54
+ # asked which readers it wrapped without either engine loaded.
55
+ class Dispatch < ActiveRecord::Base
56
+ self.table_name = 'notes'
57
+ include Errgonomic::Rails::ActiveRecordOptional
58
+ has_one :rich_text_body, class_name: 'ActionText::RichText', as: :record
59
+ end
60
+
61
+ # A subclass wraps nothing of its own: an inherited reader is already an
62
+ # Option, and wrapping it again would nest it.
63
+ class Report < ActiveRecord::Base
64
+ include Errgonomic::Rails::ActiveRecordOptional
65
+ end
66
+
67
+ class Briefing < Report; end
68
+
69
+ # An unconverted delegation target: its readers hand back plain values, and
70
+ # two of its methods take an argument, a keyword and a block.
71
+ class Writer < ActiveRecord::Base
72
+ def greeting(salutation, punctuation: '.')
73
+ "#{salutation}, #{name}#{punctuation}"
74
+ end
75
+
76
+ def styled_name
77
+ yield(name)
78
+ end
79
+ end
80
+
81
+ # A converted model reads its association as an Option.
82
+ class Article < ActiveRecord::Base
83
+ include Errgonomic::Rails::ActiveRecordOptional
84
+ belongs_to :writer, optional: true
85
+ delegate_optional :name, to: :writer, prefix: true
86
+ delegate_optional :name, to: :writer, prefix: :author
87
+ delegate_optional :bio, to: :writer
88
+ delegate_optional :greeting, :styled_name, to: :writer, prefix: true
89
+ delegate_optional :table_name, to: :class
90
+ end
91
+
92
+ # The same records read through a converted model, so the target's own
93
+ # reader is already an Option.
94
+ class Byline < ActiveRecord::Base
95
+ self.table_name = 'writers'
96
+ include Errgonomic::Rails::ActiveRecordOptional
97
+ end
98
+
99
+ # Unconverted, so the association reader hands back a plain record or nil.
100
+ class Draft < ActiveRecord::Base
101
+ self.table_name = 'articles'
102
+ belongs_to :writer, optional: true
103
+ belongs_to :byline, class_name: 'Byline', foreign_key: :writer_id, optional: true
104
+ delegate_optional :name, to: :writer, prefix: true
105
+ delegate_optional :name, to: :byline, prefix: true
106
+ end
107
+
108
+ # A mechanical swap from Rails' delegate carries allow_nil: true along, and
109
+ # a delegation declared private stays off the public surface.
110
+ class Reprint < ActiveRecord::Base
111
+ self.table_name = 'articles'
112
+ belongs_to :writer, optional: true
113
+ delegate_optional :name, to: :writer, prefix: true, allow_nil: true
114
+ delegate_optional :bio, to: :writer, private: true
115
+ end
116
+
117
+ # Two validators that answer differently for the same wrapped value.
118
+ class Memo < ActiveRecord::Base
119
+ self.table_name = 'notes'
120
+ include Errgonomic::Rails::ActiveRecordOptional
121
+ validates :title, presence: true
122
+ validates :body, some: true
123
+ end
124
+
125
+ # yard-doctest sends any expectation that answers nil? to assert_nil, and
126
+ # under the Rails integration None() answers it. Compare an expected Option by
127
+ # value, so `# => None()` keeps meaning what it says.
128
+ module DoctestOptionEquality
129
+ def assert_example(example, expected, actual, bind)
130
+ # An expectation is a literal in the example's own binding, so leaving
131
+ # the other branches to super costs nothing but evaluating it twice.
132
+ return super unless evaluate_with_assertion(expected, bind).is_a?(Errgonomic::Option::Any)
133
+
134
+ assert_equal(evaluate_with_assertion(expected, bind), evaluate_with_assertion(actual, bind))
135
+ rescue Minitest::Assertion => e
136
+ add_filepath_to_backtrace(e, example.filepath)
137
+ raise e
138
+ end
139
+ end
140
+
141
+ YARD::Doctest::Example.prepend(DoctestOptionEquality)
142
+
143
+ # A declared default is cast on its way into a new record rather than assigned
144
+ # through a writer.
145
+ class DefaultedNote < ActiveRecord::Base
146
+ self.table_name = 'notes'
147
+ attribute :rank, :integer, default: Some(0)
148
+ attribute :title, :string, default: None()
149
+ end
150
+
151
+ # A Proc default is called when the record is built, so what it returns meets
152
+ # the column type exactly where a literal default does.
153
+ class ProcDefaultedNote < ActiveRecord::Base
154
+ self.table_name = 'notes'
155
+ attribute :title, :string, default: -> { Some('Wanderer') }
156
+ end
data/gem-groups.json CHANGED
@@ -1 +1 @@
1
- {"errgonomic":["default"],"yard":["development"],"yard-doctest":["development"],"activerecord":["development"],"minitest":["development"],"rails":["development"],"rake":["development"],"rspec":["development"],"rubocop":["development"],"rubocop-yard":["development"],"solargraph":["development"],"sqlite3":["development"],"concurrent-ruby":["default","development"],"drb":["development"],"prism":["development"],"activemodel":["development"],"activesupport":["development"],"base64":["development"],"bigdecimal":["development"],"connection_pool":["development"],"i18n":["development"],"json":["development"],"logger":["development"],"securerandom":["development"],"timeout":["development"],"tzinfo":["development"],"uri":["development"],"action_text-trix":["development"],"actioncable":["development"],"actionmailbox":["development"],"actionmailer":["development"],"actionpack":["development"],"actiontext":["development"],"actionview":["development"],"activejob":["development"],"activestorage":["development"],"builder":["development"],"bundler":["development"],"crass":["development"],"date":["development"],"erb":["development"],"erubi":["development"],"globalid":["development"],"io-console":["development"],"irb":["development"],"loofah":["development"],"mail":["development"],"marcel":["development"],"mini_mime":["development"],"net-imap":["development"],"net-pop":["development"],"net-protocol":["development"],"net-smtp":["development"],"nio4r":["development"],"nokogiri":["development"],"pp":["development"],"prettyprint":["development"],"psych":["development"],"racc":["development"],"rack":["development"],"rack-session":["development"],"rack-test":["development"],"rackup":["development"],"rails-dom-testing":["development"],"rails-html-sanitizer":["development"],"railties":["development"],"rdoc":["development"],"reline":["development"],"stringio":["development"],"thor":["development"],"tsort":["development"],"useragent":["development"],"websocket-driver":["development"],"websocket-extensions":["development"],"zeitwerk":["development"],"diff-lcs":["development"],"rspec-core":["development"],"rspec-expectations":["development"],"rspec-mocks":["development"],"rspec-support":["development"],"ast":["development"],"language_server-protocol":["development"],"lint_roller":["development"],"parallel":["development"],"parser":["development"],"rainbow":["development"],"regexp_parser":["development"],"rubocop-ast":["development"],"ruby-progressbar":["development"],"unicode-display_width":["development"],"unicode-emoji":["development"],"backport":["development"],"benchmark":["development"],"commander":["development"],"highline":["development"],"jaro_winkler":["development"],"kramdown":["development"],"kramdown-parser-gfm":["development"],"observer":["development"],"open3":["development"],"ostruct":["development"],"parlour":["development"],"rbs":["development"],"reverse_markdown":["development"],"rexml":["development"],"sorbet-runtime":["development"],"sord":["development"],"tilt":["development"],"yard-activesupport-concern":["development"],"yard-solargraph":["development"]}
1
+ {"errgonomic":["default"],"yard":["development"],"yard-doctest":["development"],"activerecord":["development"],"bcrypt":["development"],"minitest":["development"],"rails":["development"],"rake":["development"],"rspec":["development"],"rubocop":["development"],"rubocop-yard":["development"],"solargraph":["development"],"sqlite3":["development"],"concurrent-ruby":["default","development"],"drb":["development"],"prism":["development"],"activemodel":["development"],"activesupport":["development"],"base64":["development"],"bigdecimal":["development"],"connection_pool":["development"],"i18n":["development"],"json":["development"],"logger":["development"],"securerandom":["development"],"timeout":["development"],"tzinfo":["development"],"uri":["development"],"action_text-trix":["development"],"actioncable":["development"],"actionmailbox":["development"],"actionmailer":["development"],"actionpack":["development"],"actiontext":["development"],"actionview":["development"],"activejob":["development"],"activestorage":["development"],"builder":["development"],"bundler":["development"],"crass":["development"],"date":["development"],"erb":["development"],"erubi":["development"],"globalid":["development"],"io-console":["development"],"irb":["development"],"loofah":["development"],"mail":["development"],"marcel":["development"],"mini_mime":["development"],"net-imap":["development"],"net-pop":["development"],"net-protocol":["development"],"net-smtp":["development"],"nio4r":["development"],"nokogiri":["development"],"pp":["development"],"prettyprint":["development"],"psych":["development"],"racc":["development"],"rack":["development"],"rack-session":["development"],"rack-test":["development"],"rackup":["development"],"rails-dom-testing":["development"],"rails-html-sanitizer":["development"],"railties":["development"],"rdoc":["development"],"reline":["development"],"stringio":["development"],"thor":["development"],"tsort":["development"],"useragent":["development"],"websocket-driver":["development"],"websocket-extensions":["development"],"zeitwerk":["development"],"diff-lcs":["development"],"rspec-core":["development"],"rspec-expectations":["development"],"rspec-mocks":["development"],"rspec-support":["development"],"ast":["development"],"language_server-protocol":["development"],"lint_roller":["development"],"parallel":["development"],"parser":["development"],"rainbow":["development"],"regexp_parser":["development"],"rubocop-ast":["development"],"ruby-progressbar":["development"],"unicode-display_width":["development"],"unicode-emoji":["development"],"backport":["development"],"benchmark":["development"],"commander":["development"],"highline":["development"],"jaro_winkler":["development"],"kramdown":["development"],"kramdown-parser-gfm":["development"],"observer":["development"],"open3":["development"],"ostruct":["development"],"parlour":["development"],"rbs":["development"],"reverse_markdown":["development"],"rexml":["development"],"sorbet-runtime":["development"],"sord":["development"],"tilt":["development"],"yard-activesupport-concern":["development"],"yard-solargraph":["development"]}
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../option'
4
+ require_relative '../result'
5
+
6
+ # The all-or-nothing collection, which Rust spells as a collect into
7
+ # Option<Vec<T>> or Result<Vec<T>, E>. On Enumerable rather than Array so it
8
+ # composes with map, and so anything that yields wrappers can be sequenced.
9
+ module Enumerable
10
+ # Collect an Enumerable of Options into an Option of an Array, stopping at
11
+ # the first None. A member that is not an Option raises unconditionally:
12
+ # with_ambiguous_downstream_errors relaxes what a block returned, not what
13
+ # a caller passed in.
14
+ #
15
+ # @return [Errgonomic::Option::Any]
16
+ #
17
+ # @example
18
+ # [Some(1), Some(2)].sequence_options # => Some([1, 2])
19
+ #
20
+ # @example
21
+ # [Some(1), None(), Some(3)].sequence_options # => None()
22
+ #
23
+ # @example
24
+ # [].sequence_options # => Some([])
25
+ #
26
+ # @example a lazy enumerable is read only as far as the first None
27
+ # seen = []
28
+ # lazy = [Some(1), None(), Some(3)].lazy.map { |o| seen << o; o }
29
+ # lazy.sequence_options # => None()
30
+ # seen.size # => 2
31
+ #
32
+ # @example
33
+ # [Some(1), 2].sequence_options # => raise Errgonomic::TypeMismatchError, "cannot sequence_options Integer; it is not an Option"
34
+ #
35
+ # @example a Hash yields pairs, which are Arrays
36
+ # { a: Some(1) }.sequence_options # => raise Errgonomic::TypeMismatchError, "cannot sequence_options Array; it is not an Option"
37
+ #
38
+ # @example sequence a Hash by its values
39
+ # { a: Some(1), b: Some(2) }.values.sequence_options # => Some([1, 2])
40
+ def sequence_options
41
+ values = []
42
+ each do |member|
43
+ unless member.is_a?(Errgonomic::Option::Any)
44
+ raise Errgonomic::TypeMismatchError,
45
+ "cannot sequence_options #{member.class}; it is not an Option"
46
+ end
47
+ return None() if member.none?
48
+
49
+ values << member.value
50
+ end
51
+ Some(values)
52
+ end
53
+
54
+ # Collect an Enumerable of Results into a Result of an Array, stopping at
55
+ # the first Err, which is returned as it stands so it keeps its error.
56
+ # A member that is not a Result raises on the same terms as
57
+ # sequence_options.
58
+ #
59
+ # @return [Errgonomic::Result::Any]
60
+ #
61
+ # @example
62
+ # [Ok(1), Ok(2)].sequence_results # => Ok([1, 2])
63
+ #
64
+ # @example
65
+ # [Ok(1), Err(:nope), Ok(3)].sequence_results # => Err(:nope)
66
+ #
67
+ # @example
68
+ # [].sequence_results # => Ok([])
69
+ #
70
+ # @example a lazy enumerable is read only as far as the first Err
71
+ # seen = []
72
+ # lazy = [Ok(1), Err(:nope), Ok(3)].lazy.map { |r| seen << r; r }
73
+ # lazy.sequence_results # => Err(:nope)
74
+ # seen.size # => 2
75
+ #
76
+ # @example
77
+ # [Ok(1), Some(2)].sequence_results # => raise Errgonomic::TypeMismatchError, "cannot sequence_results Errgonomic::Option::Some; it is not a Result"
78
+ #
79
+ # @example sequence a Hash by its values
80
+ # { a: Ok(1), b: Ok(2) }.values.sequence_results # => Ok([1, 2])
81
+ def sequence_results
82
+ values = []
83
+ each do |member|
84
+ unless member.is_a?(Errgonomic::Result::Any)
85
+ raise Errgonomic::TypeMismatchError,
86
+ "cannot sequence_results #{member.class}; it is not a Result"
87
+ end
88
+ return member if member.err?
89
+
90
+ values << member.value
91
+ end
92
+ Ok(values)
93
+ end
94
+ end