diff --git a/lib/errgonomic/rails/active_record_optional.rb b/lib/errgonomic/rails/active_record_optional.rb index 88b6e1a..6fdee38 100644 --- a/lib/errgonomic/rails/active_record_optional.rb +++ b/lib/errgonomic/rails/active_record_optional.rb @@ -223,6 +223,18 @@ def to_option end end +module Errgonomic + module Option + # An Option is already lifted. Lifting it again would nest it, and the + # nesting is invisible until something reaches for the inner value. + class Any + def to_option + self + end + end + end +end + module Errgonomic module Rails # Teach ActiveRecord SQL quoting to unwrap Options, quoting a None as diff --git a/test/rails_test.rb b/test/rails_test.rb index fb9da94..9494f38 100644 --- a/test/rails_test.rb +++ b/test/rails_test.rb @@ -244,6 +244,26 @@ def test_nested_options_and_results_refuse_to_serialize assert_raises(Errgonomic::SerializeError) { [Ok(5)].to_json } end + # to_option lifts a value that may be nil. An Option is already lifted, and + # a second lift nests invisibly: Some(Some(x)) still answers some?, so the + # mistake surfaces far from where it was made. + def test_to_option_is_idempotent + assert_equal Some(1), Some(1).to_option + assert_equal 1, Some(1).to_option.unwrap! + assert None().to_option.none? + end + + # The shape an application reaches for around an unwrapped association: + # lift it, then reach through it for an attribute that is already an Option. + def test_to_option_composes_through_an_unwrapped_association + author = Author.create!(name: 'Cixin Liu', bio: 'writes sci-fi') + shelved = Book.create!(title: 'The Dark Forest', author_id: author.id) + unshelved = Book.create!(title: 'Supernova Era') + + assert_equal 'writes sci-fi', shelved.author.to_option.and_then(&:bio).unwrap_or('unknown') + assert_equal 'unknown', unshelved.author.to_option.and_then(&:bio).unwrap_or('unknown') + end + def test_delegate_optional author = Author.create!(name: 'Cixin Liu') book = author.books.create!(title: 'Death\'s End')