From a5111bc441584784a9a5e0d057f26cdec860d7d3 Mon Sep 17 00:00:00 2001 From: Erwan Thomas Date: Sat, 4 Mar 2023 11:24:41 +0100 Subject: [PATCH 1/3] Split messaging.rb into module-based files --- lib/sheetah/messaging.rb | 196 +---------------------------- lib/sheetah/messaging/constants.rb | 17 +++ lib/sheetah/messaging/message.rb | 57 +++++++++ lib/sheetah/messaging/messenger.rb | 134 ++++++++++++++++++++ 4 files changed, 211 insertions(+), 193 deletions(-) create mode 100644 lib/sheetah/messaging/constants.rb create mode 100644 lib/sheetah/messaging/message.rb create mode 100644 lib/sheetah/messaging/messenger.rb diff --git a/lib/sheetah/messaging.rb b/lib/sheetah/messaging.rb index e07e818..751d300 100644 --- a/lib/sheetah/messaging.rb +++ b/lib/sheetah/messaging.rb @@ -1,195 +1,5 @@ # frozen_string_literal: true -module Sheetah - module Messaging - module SCOPES - SHEET = "SHEET" - ROW = "ROW" - COL = "COL" - CELL = "CELL" - end - - module SEVERITIES - WARN = "WARN" - ERROR = "ERROR" - end - - # TODO: list all possible message code in a systematic way, - # so that i18n do not miss any by mistake. - class Message - def initialize( - code:, - code_data: nil, - scope: nil, - scope_data: nil, - severity: nil - ) - @code = code - @code_data = code_data || nil - @scope = scope || SCOPES::SHEET - @scope_data = scope_data || nil - @severity = severity || SEVERITIES::WARN - end - - attr_reader( - :code, - :code_data, - :scope, - :scope_data, - :severity - ) - - def ==(other) - other.is_a?(self.class) && - code == other.code && - code_data == other.code_data && - scope == other.scope && - scope_data == other.scope_data && - severity == other.severity - end - - def to_s - parts = [scoping_to_s, "#{severity}: #{code}", code_data] - parts.compact! - parts.join(" ") - end - - private - - def scoping_to_s - case scope - when SCOPES::SHEET then "[#{scope}]" - when SCOPES::ROW then "[#{scope}: #{scope_data[:row]}]" - when SCOPES::COL then "[#{scope}: #{scope_data[:col]}]" - when SCOPES::CELL then "[#{scope}: #{scope_data[:col]}#{scope_data[:row]}]" - end - end - end - - class Messenger - def initialize( - scope: SCOPES::SHEET, - scope_data: nil - ) - @scope = scope.freeze - @scope_data = scope_data.freeze - @messages = [] - end - - attr_reader :scope, :scope_data, :messages - - def ==(other) - other.is_a?(self.class) && - scope == other.scope && - scope_data == other.scope_data && - messages == other.messages - end - - def dup - self.class.new( - scope: @scope, - scope_data: @scope_data - ) - end - - def scoping!(scope, scope_data, &block) - scope = scope.freeze - scope_data = scope_data.freeze - - if block - replace_scoping_block(scope, scope_data, &block) - else - replace_scoping_noblock(scope, scope_data) - end - end - - def scoping(...) - dup.scoping!(...) - end - - def scope_row!(row, &block) - scope = case @scope - when SCOPES::COL, SCOPES::CELL - SCOPES::CELL - else - SCOPES::ROW - end - - scope_data = @scope_data.dup || {} - scope_data[:row] = row - - scoping!(scope, scope_data, &block) - end - - def scope_col!(col, &block) - scope = case @scope - when SCOPES::ROW, SCOPES::CELL - SCOPES::CELL - else - SCOPES::COL - end - - scope_data = @scope_data.dup || {} - scope_data[:col] = col - - scoping!(scope, scope_data, &block) - end - - def scope_row(...) - dup.scope_row!(...) - end - - def scope_col(...) - dup.scope_col!(...) - end - - def warn(code, data = nil) - add(SEVERITIES::WARN, code, data) - end - - def error(code, data = nil) - add(SEVERITIES::ERROR, code, data) - end - - def exception(error) - error(error.msg_code) - end - - private - - def add(severity, code, data) - messages << Message.new( - code: code, - code_data: data, - scope: @scope, - scope_data: @scope_data, - severity: severity - ) - - self - end - - def replace_scoping_noblock(new_scope, new_scope_data) - @scope = new_scope - @scope_data = new_scope_data - - self - end - - def replace_scoping_block(new_scope, new_scope_data) - prev_scope = @scope - prev_scope_data = @scope_data - - @scope = new_scope - @scope_data = new_scope_data - - begin - yield self - ensure - @scope = prev_scope - @scope_data = prev_scope_data - end - end - end - end -end +require_relative "messaging/constants" +require_relative "messaging/message" +require_relative "messaging/messenger" diff --git a/lib/sheetah/messaging/constants.rb b/lib/sheetah/messaging/constants.rb new file mode 100644 index 0000000..765ec45 --- /dev/null +++ b/lib/sheetah/messaging/constants.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Sheetah + module Messaging + module SCOPES + SHEET = "SHEET" + ROW = "ROW" + COL = "COL" + CELL = "CELL" + end + + module SEVERITIES + WARN = "WARN" + ERROR = "ERROR" + end + end +end diff --git a/lib/sheetah/messaging/message.rb b/lib/sheetah/messaging/message.rb new file mode 100644 index 0000000..04cec38 --- /dev/null +++ b/lib/sheetah/messaging/message.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative "constants" + +module Sheetah + module Messaging + class Message + def initialize( + code:, + code_data: nil, + scope: nil, + scope_data: nil, + severity: nil + ) + @code = code + @code_data = code_data || nil + @scope = scope || SCOPES::SHEET + @scope_data = scope_data || nil + @severity = severity || SEVERITIES::WARN + end + + attr_reader( + :code, + :code_data, + :scope, + :scope_data, + :severity + ) + + def ==(other) + other.is_a?(self.class) && + code == other.code && + code_data == other.code_data && + scope == other.scope && + scope_data == other.scope_data && + severity == other.severity + end + + def to_s + parts = [scoping_to_s, "#{severity}: #{code}", code_data] + parts.compact! + parts.join(" ") + end + + private + + def scoping_to_s + case scope + when SCOPES::SHEET then "[#{scope}]" + when SCOPES::ROW then "[#{scope}: #{scope_data[:row]}]" + when SCOPES::COL then "[#{scope}: #{scope_data[:col]}]" + when SCOPES::CELL then "[#{scope}: #{scope_data[:col]}#{scope_data[:row]}]" + end + end + end + end +end diff --git a/lib/sheetah/messaging/messenger.rb b/lib/sheetah/messaging/messenger.rb new file mode 100644 index 0000000..2e6fe41 --- /dev/null +++ b/lib/sheetah/messaging/messenger.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require_relative "constants" +require_relative "message" + +module Sheetah + module Messaging + class Messenger + def initialize( + scope: SCOPES::SHEET, + scope_data: nil + ) + @scope = scope.freeze + @scope_data = scope_data.freeze + @messages = [] + end + + attr_reader :scope, :scope_data, :messages + + def ==(other) + other.is_a?(self.class) && + scope == other.scope && + scope_data == other.scope_data && + messages == other.messages + end + + def dup + self.class.new( + scope: @scope, + scope_data: @scope_data + ) + end + + def scoping!(scope, scope_data, &block) + scope = scope.freeze + scope_data = scope_data.freeze + + if block + replace_scoping_block(scope, scope_data, &block) + else + replace_scoping_noblock(scope, scope_data) + end + end + + def scoping(...) + dup.scoping!(...) + end + + def scope_row!(row, &block) + scope = case @scope + when SCOPES::COL, SCOPES::CELL + SCOPES::CELL + else + SCOPES::ROW + end + + scope_data = @scope_data.dup || {} + scope_data[:row] = row + + scoping!(scope, scope_data, &block) + end + + def scope_col!(col, &block) + scope = case @scope + when SCOPES::ROW, SCOPES::CELL + SCOPES::CELL + else + SCOPES::COL + end + + scope_data = @scope_data.dup || {} + scope_data[:col] = col + + scoping!(scope, scope_data, &block) + end + + def scope_row(...) + dup.scope_row!(...) + end + + def scope_col(...) + dup.scope_col!(...) + end + + def warn(code, data = nil) + add(SEVERITIES::WARN, code, data) + end + + def error(code, data = nil) + add(SEVERITIES::ERROR, code, data) + end + + def exception(error) + error(error.msg_code) + end + + private + + def add(severity, code, data) + messages << Message.new( + code: code, + code_data: data, + scope: @scope, + scope_data: @scope_data, + severity: severity + ) + + self + end + + def replace_scoping_noblock(new_scope, new_scope_data) + @scope = new_scope + @scope_data = new_scope_data + + self + end + + def replace_scoping_block(new_scope, new_scope_data) + prev_scope = @scope + prev_scope_data = @scope_data + + @scope = new_scope + @scope_data = new_scope_data + + begin + yield self + ensure + @scope = prev_scope + @scope_data = prev_scope_data + end + end + end + end +end From fcd61fb0166e964ee3730506c5b4dd60008d8711 Mon Sep 17 00:00:00 2001 From: Erwan Thomas Date: Wed, 30 Aug 2023 15:48:47 +0200 Subject: [PATCH 2/3] Remove implicit message codes from exceptions --- lib/sheetah/errors/error.rb | 39 -------- lib/sheetah/messaging/messenger.rb | 4 - lib/sheetah/sheet.rb | 3 + spec/sheetah/errors/error_spec.rb | 108 +---------------------- spec/sheetah/errors/spec_error_spec.rb | 4 +- spec/sheetah/errors/type_error_spec.rb | 2 +- spec/sheetah/messaging/messenger_spec.rb | 26 ------ spec/sheetah/sheet_processor_spec.rb | 13 ++- spec/sheetah/specification_spec.rb | 27 +++--- 9 files changed, 26 insertions(+), 200 deletions(-) diff --git a/lib/sheetah/errors/error.rb b/lib/sheetah/errors/error.rb index 67cd4c2..5707a3c 100644 --- a/lib/sheetah/errors/error.rb +++ b/lib/sheetah/errors/error.rb @@ -3,45 +3,6 @@ module Sheetah module Errors class Error < StandardError - class << self - def inherited(klass) - super - - klass.msg_code! if klass.detect_msg_code? - end - - attr_reader :msg_code - - def detect_msg_code? - name && /^[a-z0-9:]+$/i.match?(name) - end - - def msg_code!(msg_code = build_msg_code) - @msg_code = msg_code - end - - private - - def build_msg_code - unless detect_msg_code? - raise ::TypeError, "Cannot build msg_code from anonymous exception: #{inspect}" - end - - msg_code = name.dup - msg_code.gsub!("::", ".") - msg_code.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') - msg_code.gsub!(/([a-z\d])([A-Z])/, '\1_\2') - msg_code.downcase! - - msg_code - end - end - - msg_code! - - def msg_code - self.class.msg_code - end end end end diff --git a/lib/sheetah/messaging/messenger.rb b/lib/sheetah/messaging/messenger.rb index 2e6fe41..6d8bf27 100644 --- a/lib/sheetah/messaging/messenger.rb +++ b/lib/sheetah/messaging/messenger.rb @@ -90,10 +90,6 @@ def error(code, data = nil) add(SEVERITIES::ERROR, code, data) end - def exception(error) - error(error.msg_code) - end - private def add(severity, code, data) diff --git a/lib/sheetah/sheet.rb b/lib/sheetah/sheet.rb index 2973eda..8dfc9b4 100644 --- a/lib/sheetah/sheet.rb +++ b/lib/sheetah/sheet.rb @@ -42,6 +42,9 @@ def handle_sheet_error end class Error < Errors::Error + def msg_code + "sheet_error" + end end class Header diff --git a/spec/sheetah/errors/error_spec.rb b/spec/sheetah/errors/error_spec.rb index 518a0ea..348b8fc 100644 --- a/spec/sheetah/errors/error_spec.rb +++ b/spec/sheetah/errors/error_spec.rb @@ -4,112 +4,6 @@ RSpec.describe Sheetah::Errors::Error do it "is some kind of StandardError" do - expect(described_class.superclass).to be(StandardError) - end - - describe "class msg_code" do - it "has a msg_code" do - expect(described_class.msg_code).to eq("sheetah.errors.error") - end - - context "when inherited" do - context "when first defined anonymously" do - let(:subclass) do - Class.new(described_class) - end - - context "when kept anonymous" do - it "doesn't have a msg_code by default" do - expect(subclass.msg_code).to be_nil - end - - it "cannot deduce a msg_code" do - expect do - subclass.msg_code! - end.to raise_error(TypeError, /cannot build msg_code/i) - end - - it "may have a custom msg_code" do - subclass.msg_code! "foo.bar.baz" - expect(subclass.msg_code).to eq("foo.bar.baz") - end - end - - context "when named afterwards" do - before do - stub_const("Foizjeofijow::OIJDFO834", subclass) - end - - it "doesn't have a msg_code by default" do - expect(subclass.msg_code).to be_nil - end - - it "can deduce a msg_code" do - subclass.msg_code! - expect(subclass.msg_code).to eq("foizjeofijow.oijdfo834") - end - - it "may have a custom msg_code" do - subclass.msg_code! "foo.bar.baz" - expect(subclass.msg_code).to eq("foo.bar.baz") - end - end - end - - context "when first defined with a name" do - let(:namespace) { Module.new } - - let(:subclass) do - class namespace::OIJDFO834 < described_class # rubocop:disable RSpec/LeakyConstantDeclaration,Lint/ConstantDefinitionInBlock,Style/ClassAndModuleChildren - self - end - end - - context "when fully named" do - before do - stub_const("Foizjeofijow", namespace) - end - - it "has a msg_code by default" do - expect(subclass.msg_code).to eq("foizjeofijow.oijdfo834") - end - - it "can deduce the same msg_code" do - expect do - subclass.msg_code! - end.not_to change(subclass, :msg_code) - end - - it "may have a custom msg_code" do - subclass.msg_code! "foo.bar.baz" - expect(subclass.msg_code).to eq("foo.bar.baz") - end - end - - context "when not fully named" do - it "doesn't have a msg_code by default" do - expect(subclass.msg_code).to be_nil - end - - it "cannot deduce a msg_code" do - expect do - subclass.msg_code! - end.to raise_error(TypeError, /cannot build msg_code/i) - end - - it "may have a custom msg_code" do - subclass.msg_code! "foo.bar.baz" - expect(subclass.msg_code).to eq("foo.bar.baz") - end - end - end - end - end - - describe "#msg_code" do - it "delegates to the class" do - allow(described_class).to receive(:msg_code).and_return(msg_code = double) - expect(subject.msg_code).to be(msg_code) - end + expect(described_class).to have_attributes(superclass: StandardError) end end diff --git a/spec/sheetah/errors/spec_error_spec.rb b/spec/sheetah/errors/spec_error_spec.rb index 9ed4f70..a58e4dc 100644 --- a/spec/sheetah/errors/spec_error_spec.rb +++ b/spec/sheetah/errors/spec_error_spec.rb @@ -3,7 +3,7 @@ require "sheetah/errors/spec_error" RSpec.describe Sheetah::Errors::SpecError do - it "has a msg_code" do - expect(described_class.msg_code).to eq("sheetah.errors.spec_error") + it "is some kind of Error" do + expect(described_class).to have_attributes(superclass: Sheetah::Errors::Error) end end diff --git a/spec/sheetah/errors/type_error_spec.rb b/spec/sheetah/errors/type_error_spec.rb index 42f5782..6a87053 100644 --- a/spec/sheetah/errors/type_error_spec.rb +++ b/spec/sheetah/errors/type_error_spec.rb @@ -4,6 +4,6 @@ RSpec.describe Sheetah::Errors::TypeError do it "is some kind of Error" do - expect(described_class.superclass).to be(Sheetah::Errors::Error) + expect(described_class).to have_attributes(superclass: Sheetah::Errors::Error) end end diff --git a/spec/sheetah/messaging/messenger_spec.rb b/spec/sheetah/messaging/messenger_spec.rb index e6a9ad3..fad393d 100644 --- a/spec/sheetah/messaging/messenger_spec.rb +++ b/spec/sheetah/messaging/messenger_spec.rb @@ -423,31 +423,5 @@ def stub_scope_col!(receiver, *args, &block) ) end end - - describe "#exception" do - let(:e) { double } - - before do - allow(e).to receive(:msg_code).and_return(code) - end - - it "returns the receiver" do - expect(messenger.exception(e)).to be(messenger) - end - - it "adds the exception's msg_code as an error" do - messenger.exception(e) - - expect(messenger.messages).to contain_exactly( - Sheetah::Messaging::Message.new( - code: code, - code_data: nil, - scope: scope, - scope_data: scope_data, - severity: severities::ERROR - ) - ) - end - end end end diff --git a/spec/sheetah/sheet_processor_spec.rb b/spec/sheetah/sheet_processor_spec.rb index ee6e56d..75f4b9e 100644 --- a/spec/sheetah/sheet_processor_spec.rb +++ b/spec/sheetah/sheet_processor_spec.rb @@ -97,15 +97,12 @@ def stub_sheet_open_ko(failure = double) end context "when there is a sheet error" do - let(:error_class) do - klass = Class.new(Sheetah::Sheet::Error) - stub_const("Foo::Bar::BazError", klass) - klass.msg_code! - klass + let(:error) do + instance_double(Sheetah::Sheet::Error, msg_code: code) end - let(:error) do - error_class.exception + let(:code) do + double end before do @@ -118,7 +115,7 @@ def stub_sheet_open_ko(failure = double) result: Failure(), messages: [ Sheetah::Messaging::Message.new( - code: "foo.bar.baz_error", + code: code, code_data: nil, scope: "SHEET", scope_data: nil, diff --git a/spec/sheetah/specification_spec.rb b/spec/sheetah/specification_spec.rb index b16a132..0690531 100644 --- a/spec/sheetah/specification_spec.rb +++ b/spec/sheetah/specification_spec.rb @@ -134,25 +134,26 @@ end end - describe "errors" do - example "invalid pattern" do - expect(described_class::InvalidPatternError).to have_attributes( - superclass: Sheetah::Errors::SpecError, - msg_code: "sheetah.specification.invalid_pattern_error" + describe "::InvalidPatternError" do + it "is some kind of SpecError" do + expect(described_class::InvalidPatternError).to( + have_attributes(superclass: Sheetah::Errors::SpecError) ) end + end - example "mutable pattern" do - expect(described_class::MutablePatternError).to have_attributes( - superclass: Sheetah::Errors::SpecError, - msg_code: "sheetah.specification.mutable_pattern_error" + describe "::MutablePatternError" do + it "is some kind of SpecError" do + expect(described_class::MutablePatternError).to( + have_attributes(superclass: Sheetah::Errors::SpecError) ) end + end - example "duplicated pattern" do - expect(described_class::DuplicatedPatternError).to have_attributes( - superclass: Sheetah::Errors::SpecError, - msg_code: "sheetah.specification.duplicated_pattern_error" + describe "::DuplicatedPatternError" do + it "is some kind of SpecError" do + expect(described_class::DuplicatedPatternError).to( + have_attributes(superclass: Sheetah::Errors::SpecError) ) end end From daca9d2d6592dfce0b40a68c269436a9b26d0504 Mon Sep 17 00:00:00 2001 From: Erwan Thomas Date: Mon, 6 Mar 2023 08:22:17 +0100 Subject: [PATCH 3/3] Prefer class-based messages Add custom validations on Message classes to guarantee at runtime their structural integrity. Such validations are opt-out, because they are considered useful and lightweight enough for most use cases. Still, they can be disabled using a global configuration parameter if need be. --- lib/sheetah/backends.rb | 8 +- lib/sheetah/headers.rb | 15 +- lib/sheetah/messaging.rb | 23 ++- lib/sheetah/messaging/config.rb | 13 ++ lib/sheetah/messaging/message.rb | 44 ++++- lib/sheetah/messaging/message_validations.rb | 129 ++++++++++++++ .../messaging/messages/cleaned_string.rb | 21 +++ .../messaging/messages/duplicated_header.rb | 21 +++ .../messaging/messages/invalid_header.rb | 21 +++ .../messaging/messages/missing_column.rb | 21 +++ .../messaging/messages/must_be_array.rb | 21 +++ .../messaging/messages/must_be_boolsy.rb | 26 +++ .../messaging/messages/must_be_date.rb | 26 +++ .../messaging/messages/must_be_email.rb | 26 +++ .../messaging/messages/must_be_string.rb | 21 +++ lib/sheetah/messaging/messages/must_exist.rb | 21 +++ .../messages/no_applicable_backend.rb | 21 +++ lib/sheetah/messaging/messages/sheet_error.rb | 21 +++ lib/sheetah/messaging/messenger.rb | 25 ++- lib/sheetah/sheet.rb | 5 +- lib/sheetah/sheet_processor.rb | 2 +- lib/sheetah/types/composites/array.rb | 3 +- lib/sheetah/types/scalars/boolsy_cast.rb | 8 +- lib/sheetah/types/scalars/date_string_cast.rb | 6 +- lib/sheetah/types/scalars/email_cast.rb | 7 +- lib/sheetah/types/scalars/scalar_cast.rb | 6 +- lib/sheetah/types/scalars/string.rb | 3 +- spec/sheetah/backends_spec.rb | 2 +- spec/sheetah/headers_spec.rb | 40 ++--- spec/sheetah/messaging/config_spec.rb | 31 ++++ spec/sheetah/messaging/message_spec.rb | 159 ++++++++++++++++++ .../messaging/messages/cleaned_string_spec.rb | 21 +++ .../messages/duplicated_header_spec.rb | 21 +++ .../messaging/messages/invalid_header_spec.rb | 21 +++ .../messaging/messages/missing_column_spec.rb | 21 +++ .../messaging/messages/must_be_array_spec.rb | 21 +++ .../messaging/messages/must_be_boolsy_spec.rb | 21 +++ .../messaging/messages/must_be_date_spec.rb | 21 +++ .../messaging/messages/must_be_email_spec.rb | 21 +++ .../messaging/messages/must_be_string_spec.rb | 21 +++ .../messaging/messages/must_exist_spec.rb | 21 +++ .../messages/no_applicable_backend_spec.rb | 21 +++ .../messaging/messages/sheet_error_spec.rb | 21 +++ spec/sheetah/messaging/messenger_spec.rb | 40 +---- spec/sheetah/messaging_spec.rb | 41 +++++ spec/sheetah/sheet_processor_spec.rb | 10 +- spec/sheetah/types/composites/array_spec.rb | 4 +- .../sheetah/types/scalars/boolsy_cast_spec.rb | 8 +- .../types/scalars/date_string_cast_spec.rb | 32 +++- spec/sheetah/types/scalars/email_cast_spec.rb | 8 +- .../sheetah/types/scalars/scalar_cast_spec.rb | 7 +- spec/sheetah/types/scalars/string_spec.rb | 6 +- spec/support.rb | 1 + spec/support/sheetah.rb | 7 + 54 files changed, 1089 insertions(+), 123 deletions(-) create mode 100644 lib/sheetah/messaging/config.rb create mode 100644 lib/sheetah/messaging/message_validations.rb create mode 100644 lib/sheetah/messaging/messages/cleaned_string.rb create mode 100644 lib/sheetah/messaging/messages/duplicated_header.rb create mode 100644 lib/sheetah/messaging/messages/invalid_header.rb create mode 100644 lib/sheetah/messaging/messages/missing_column.rb create mode 100644 lib/sheetah/messaging/messages/must_be_array.rb create mode 100644 lib/sheetah/messaging/messages/must_be_boolsy.rb create mode 100644 lib/sheetah/messaging/messages/must_be_date.rb create mode 100644 lib/sheetah/messaging/messages/must_be_email.rb create mode 100644 lib/sheetah/messaging/messages/must_be_string.rb create mode 100644 lib/sheetah/messaging/messages/must_exist.rb create mode 100644 lib/sheetah/messaging/messages/no_applicable_backend.rb create mode 100644 lib/sheetah/messaging/messages/sheet_error.rb create mode 100644 spec/sheetah/messaging/config_spec.rb create mode 100644 spec/sheetah/messaging/messages/cleaned_string_spec.rb create mode 100644 spec/sheetah/messaging/messages/duplicated_header_spec.rb create mode 100644 spec/sheetah/messaging/messages/invalid_header_spec.rb create mode 100644 spec/sheetah/messaging/messages/missing_column_spec.rb create mode 100644 spec/sheetah/messaging/messages/must_be_array_spec.rb create mode 100644 spec/sheetah/messaging/messages/must_be_boolsy_spec.rb create mode 100644 spec/sheetah/messaging/messages/must_be_date_spec.rb create mode 100644 spec/sheetah/messaging/messages/must_be_email_spec.rb create mode 100644 spec/sheetah/messaging/messages/must_be_string_spec.rb create mode 100644 spec/sheetah/messaging/messages/must_exist_spec.rb create mode 100644 spec/sheetah/messaging/messages/no_applicable_backend_spec.rb create mode 100644 spec/sheetah/messaging/messages/sheet_error_spec.rb create mode 100644 spec/sheetah/messaging_spec.rb create mode 100644 spec/support/sheetah.rb diff --git a/lib/sheetah/backends.rb b/lib/sheetah/backends.rb index 946ad7c..ef68a34 100644 --- a/lib/sheetah/backends.rb +++ b/lib/sheetah/backends.rb @@ -1,15 +1,13 @@ # frozen_string_literal: true require_relative "backends_registry" +require_relative "messaging/messages/no_applicable_backend" require_relative "utils/monadic_result" module Sheetah module Backends @registry = BackendsRegistry.new - SimpleError = Struct.new(:msg_code) - private_constant :SimpleError - class << self attr_reader :registry @@ -17,7 +15,9 @@ def open(*args, **opts, &block) backend = opts.delete(:backend) || registry.get(*args, **opts) if backend.nil? - return Utils::MonadicResult::Failure.new(SimpleError.new("no_applicable_backend")) + return Utils::MonadicResult::Failure.new( + Messaging::Messages::NoApplicableBackend.new + ) end backend.open(*args, **opts, &block) diff --git a/lib/sheetah/headers.rb b/lib/sheetah/headers.rb index 35e918c..ddbfa4c 100644 --- a/lib/sheetah/headers.rb +++ b/lib/sheetah/headers.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true require "set" +require_relative "messaging/messages/invalid_header" +require_relative "messaging/messages/duplicated_header" +require_relative "messaging/messages/missing_column" module Sheetah class Headers @@ -51,7 +54,9 @@ def result @failure = true missing_columns.each do |column| - @messenger.error("missing_column", column.header) + @messenger.error( + Messaging::Messages::MissingColumn.new(code_data: column.header) + ) end end @@ -69,7 +74,9 @@ def add_ensure_column_is_specified(header, column) unless @specification.ignore_unspecified_columns? @failure = true - @messenger.error("invalid_header", header.value) + @messenger.error( + Messaging::Messages::InvalidHeader.new(code_data: header.value) + ) end false @@ -79,7 +86,9 @@ def add_ensure_column_is_unique(header, column) return true if @columns.add?(column) @failure = true - @messenger.error("duplicated_header", header.value) + @messenger.error( + Messaging::Messages::DuplicatedHeader.new(code_data: header.value) + ) false end diff --git a/lib/sheetah/messaging.rb b/lib/sheetah/messaging.rb index 751d300..fe51bf0 100644 --- a/lib/sheetah/messaging.rb +++ b/lib/sheetah/messaging.rb @@ -1,5 +1,22 @@ # frozen_string_literal: true -require_relative "messaging/constants" -require_relative "messaging/message" -require_relative "messaging/messenger" +module Sheetah + module Messaging + require_relative "messaging/config" + require_relative "messaging/constants" + require_relative "messaging/message" + require_relative "messaging/messenger" + + class << self + attr_accessor :config + + def configure + config = self.config.dup + yield config + self.config = config.freeze + end + end + + self.config = Config.new.freeze + end +end diff --git a/lib/sheetah/messaging/config.rb b/lib/sheetah/messaging/config.rb new file mode 100644 index 0000000..10b4b3e --- /dev/null +++ b/lib/sheetah/messaging/config.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Sheetah + module Messaging + class Config + def initialize(validate_messages: true) + @validate_messages = validate_messages + end + + attr_accessor :validate_messages + end + end +end diff --git a/lib/sheetah/messaging/message.rb b/lib/sheetah/messaging/message.rb index 04cec38..17224c7 100644 --- a/lib/sheetah/messaging/message.rb +++ b/lib/sheetah/messaging/message.rb @@ -1,25 +1,45 @@ # frozen_string_literal: true require_relative "constants" +require_relative "config" +require_relative "message_validations" module Sheetah module Messaging class Message + include MessageValidations + + CODE = nil + + def self.code + self::CODE + end + + def self.new(**opts) + code ? super(code: code, **opts) : super + end + + def self.new!(...) + new(...).tap(&:validate) + end + def initialize( code:, code_data: nil, - scope: nil, + scope: SCOPES::SHEET, scope_data: nil, - severity: nil + severity: SEVERITIES::WARN, + validatable: Messaging.config.validate_messages ) @code = code - @code_data = code_data || nil - @scope = scope || SCOPES::SHEET - @scope_data = scope_data || nil - @severity = severity || SEVERITIES::WARN + @code_data = code_data + @scope = scope + @scope_data = scope_data + @severity = severity + @validatable = validatable end - attr_reader( + attr_accessor( :code, :code_data, :scope, @@ -42,6 +62,16 @@ def to_s parts.join(" ") end + def to_h + { + code: code, + code_data: code_data, + scope: scope, + scope_data: scope_data, + severity: severity, + } + end + private def scoping_to_s diff --git a/lib/sheetah/messaging/message_validations.rb b/lib/sheetah/messaging/message_validations.rb new file mode 100644 index 0000000..1a42434 --- /dev/null +++ b/lib/sheetah/messaging/message_validations.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require_relative "../errors/error" +require_relative "constants" + +module Sheetah + module Messaging + module MessageValidations + class InvalidMessage < Errors::Error + end + + class BaseValidator + def self.cell + include CellValidator + end + + def self.col + include ColValidator + end + + def self.row + include RowValidator + end + + def self.sheet + include SheetValidator + end + + def validate(message) + errors = [] + + errors << "code" unless validate_code(message) + errors << "code_data" unless validate_code_data(message) + errors << "scope" unless validate_scope(message) + errors << "scope_data" unless validate_scope_data(message) + + return if errors.empty? + + raise InvalidMessage, "#{errors.join(", ")} <#{message.class}>#{message.to_h}" + end + + def validate_code(message) + message.code == message.class.code + end + end + + module CellValidator + def validate_scope(message) + message.scope == SCOPES::CELL + end + + def validate_scope_data(message) + case message.scope_data + in { col: String, row: Integer } + true + else + false + end + end + end + + module ColValidator + def validate_scope(message) + message.scope == SCOPES::COL + end + + def validate_scope_data(message) + case message.scope_data + in { col: String } + true + else + false + end + end + end + + module RowValidator + def validate_scope(message) + message.scope == SCOPES::ROW + end + + def validate_scope_data(message) + case message.scope_data + in { row: Integer } + true + else + false + end + end + end + + module SheetValidator + def validate_scope(message) + message.scope == SCOPES::SHEET + end + + def validate_scope_data(message) + message.scope_data.nil? + end + end + + module ClassMethods + def validate_with(&block) + @validator = Class.new(BaseValidator, &block).new.freeze + end + + def validator + if defined?(@validator) + @validator + elsif superclass.respond_to?(:validator) + superclass.validator + end + end + + def validate(message) + validator&.validate(message) + end + end + + def self.included(message_class) + message_class.extend(ClassMethods) + end + + def validate + self.class.validate(self) if @validatable + end + end + end +end diff --git a/lib/sheetah/messaging/messages/cleaned_string.rb b/lib/sheetah/messaging/messages/cleaned_string.rb new file mode 100644 index 0000000..25aafcd --- /dev/null +++ b/lib/sheetah/messaging/messages/cleaned_string.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class CleanedString < Message + CODE = "cleaned_string" + + validate_with do + cell + + def validate_code_data(message) + message.code_data.nil? + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messages/duplicated_header.rb b/lib/sheetah/messaging/messages/duplicated_header.rb new file mode 100644 index 0000000..500abe0 --- /dev/null +++ b/lib/sheetah/messaging/messages/duplicated_header.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class DuplicatedHeader < Message + CODE = "duplicated_header" + + validate_with do + col + + def validate_code_data(message) + message.code_data.is_a?(String) + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messages/invalid_header.rb b/lib/sheetah/messaging/messages/invalid_header.rb new file mode 100644 index 0000000..d454c95 --- /dev/null +++ b/lib/sheetah/messaging/messages/invalid_header.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class InvalidHeader < Message + CODE = "invalid_header" + + validate_with do + col + + def validate_code_data(message) + message.code_data.is_a?(String) + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messages/missing_column.rb b/lib/sheetah/messaging/messages/missing_column.rb new file mode 100644 index 0000000..2104bda --- /dev/null +++ b/lib/sheetah/messaging/messages/missing_column.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class MissingColumn < Message + CODE = "missing_column" + + validate_with do + sheet + + def validate_code_data(message) + message.code_data.is_a?(String) + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messages/must_be_array.rb b/lib/sheetah/messaging/messages/must_be_array.rb new file mode 100644 index 0000000..ca5965c --- /dev/null +++ b/lib/sheetah/messaging/messages/must_be_array.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class MustBeArray < Message + CODE = "must_be_array" + + validate_with do + cell + + def validate_code_data(message) + message.code_data.nil? + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messages/must_be_boolsy.rb b/lib/sheetah/messaging/messages/must_be_boolsy.rb new file mode 100644 index 0000000..bab15a0 --- /dev/null +++ b/lib/sheetah/messaging/messages/must_be_boolsy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class MustBeBoolsy < Message + CODE = "must_be_boolsy" + + validate_with do + cell + + def validate_code_data(message) + case message.code_data + in { value: String } + true + else + false + end + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messages/must_be_date.rb b/lib/sheetah/messaging/messages/must_be_date.rb new file mode 100644 index 0000000..4caea14 --- /dev/null +++ b/lib/sheetah/messaging/messages/must_be_date.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class MustBeDate < Message + CODE = "must_be_date" + + validate_with do + cell + + def validate_code_data(message) + case message.code_data + in { format: String } + true + else + false + end + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messages/must_be_email.rb b/lib/sheetah/messaging/messages/must_be_email.rb new file mode 100644 index 0000000..1ab73c8 --- /dev/null +++ b/lib/sheetah/messaging/messages/must_be_email.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class MustBeEmail < Message + CODE = "must_be_email" + + validate_with do + cell + + def validate_code_data(message) + case message.code_data + in { value: String } + true + else + false + end + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messages/must_be_string.rb b/lib/sheetah/messaging/messages/must_be_string.rb new file mode 100644 index 0000000..b9e7049 --- /dev/null +++ b/lib/sheetah/messaging/messages/must_be_string.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class MustBeString < Message + CODE = "must_be_string" + + validate_with do + cell + + def validate_code_data(message) + message.code_data.nil? + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messages/must_exist.rb b/lib/sheetah/messaging/messages/must_exist.rb new file mode 100644 index 0000000..4d7a7fb --- /dev/null +++ b/lib/sheetah/messaging/messages/must_exist.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class MustExist < Message + CODE = "must_exist" + + validate_with do + cell + + def validate_code_data(message) + message.code_data.nil? + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messages/no_applicable_backend.rb b/lib/sheetah/messaging/messages/no_applicable_backend.rb new file mode 100644 index 0000000..eb2de43 --- /dev/null +++ b/lib/sheetah/messaging/messages/no_applicable_backend.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class NoApplicableBackend < Message + CODE = "no_applicable_backend" + + validate_with do + sheet + + def validate_code_data(message) + message.code_data.nil? + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messages/sheet_error.rb b/lib/sheetah/messaging/messages/sheet_error.rb new file mode 100644 index 0000000..a907f37 --- /dev/null +++ b/lib/sheetah/messaging/messages/sheet_error.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../message" + +module Sheetah + module Messaging + module Messages + class SheetError < Message + CODE = "sheet_error" + + validate_with do + sheet + + def validate_code_data(message) + message.code_data.nil? + end + end + end + end + end +end diff --git a/lib/sheetah/messaging/messenger.rb b/lib/sheetah/messaging/messenger.rb index 6d8bf27..5577c31 100644 --- a/lib/sheetah/messaging/messenger.rb +++ b/lib/sheetah/messaging/messenger.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require_relative "constants" -require_relative "message" module Sheetah module Messaging @@ -82,24 +81,24 @@ def scope_col(...) dup.scope_col!(...) end - def warn(code, data = nil) - add(SEVERITIES::WARN, code, data) + def warn(message) + add(message, severity: SEVERITIES::WARN) end - def error(code, data = nil) - add(SEVERITIES::ERROR, code, data) + def error(message) + add(message, severity: SEVERITIES::ERROR) end private - def add(severity, code, data) - messages << Message.new( - code: code, - code_data: data, - scope: @scope, - scope_data: @scope_data, - severity: severity - ) + def add(message, severity:) + message.scope = @scope + message.scope_data = @scope_data + message.severity = severity + + message.validate + + messages << message self end diff --git a/lib/sheetah/sheet.rb b/lib/sheetah/sheet.rb index 8dfc9b4..3249a70 100644 --- a/lib/sheetah/sheet.rb +++ b/lib/sheetah/sheet.rb @@ -2,6 +2,7 @@ require_relative "sheet/col_converter" require_relative "errors/error" +require_relative "messaging/messages/sheet_error" require_relative "utils/monadic_result" module Sheetah @@ -42,8 +43,8 @@ def handle_sheet_error end class Error < Errors::Error - def msg_code - "sheet_error" + def to_message + Messaging::Messages::SheetError.new end end diff --git a/lib/sheetah/sheet_processor.rb b/lib/sheetah/sheet_processor.rb index f7ce3e2..8e15ed0 100644 --- a/lib/sheetah/sheet_processor.rb +++ b/lib/sheetah/sheet_processor.rb @@ -52,7 +52,7 @@ def build_row_processor(sheet, messenger) def handle_result(result, messenger) result.or do |failure| - messenger.error(failure.msg_code) if failure.respond_to?(:msg_code) + messenger.error(failure.to_message) if failure.respond_to?(:to_message) end SheetProcessorResult.new(result: result.discard, messages: messenger.messages) diff --git a/lib/sheetah/types/composites/array.rb b/lib/sheetah/types/composites/array.rb index 6a726b5..f64bfa8 100644 --- a/lib/sheetah/types/composites/array.rb +++ b/lib/sheetah/types/composites/array.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true require_relative "composite" +require_relative "../../messaging/messages/must_be_array" module Sheetah module Types module Composites Array = Composite.cast do |value, _messenger| - throw :failure, "must_be_array" unless value.is_a?(::Array) + throw :failure, Messaging::Messages::MustBeArray.new unless value.is_a?(::Array) value end diff --git a/lib/sheetah/types/scalars/boolsy_cast.rb b/lib/sheetah/types/scalars/boolsy_cast.rb index aeba7f7..e23ac2e 100644 --- a/lib/sheetah/types/scalars/boolsy_cast.rb +++ b/lib/sheetah/types/scalars/boolsy_cast.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative "../../messaging/messages/must_be_boolsy" require_relative "../cast" module Sheetah @@ -17,14 +18,15 @@ def initialize(truthy: TRUTHY, falsy: FALSY, **) @falsy = falsy end - def call(value, messenger) + def call(value, _messenger) if @truthy.include?(value) true elsif @falsy.include?(value) false else - messenger.error("must_be_boolsy", value: value.inspect) - throw :failure + throw :failure, Messaging::Messages::MustBeBoolsy.new( + code_data: { value: value.inspect } + ) end end end diff --git a/lib/sheetah/types/scalars/date_string_cast.rb b/lib/sheetah/types/scalars/date_string_cast.rb index 47617a3..4451e1f 100644 --- a/lib/sheetah/types/scalars/date_string_cast.rb +++ b/lib/sheetah/types/scalars/date_string_cast.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "date" +require_relative "../../messaging/messages/must_be_date" require_relative "../cast" module Sheetah @@ -17,7 +18,7 @@ def initialize(date_fmt: DATE_FMT, accept_date: true, **) @accept_date = accept_date end - def call(value, messenger) + def call(value, _messenger) case value when ::Date return value if @accept_date @@ -26,8 +27,7 @@ def call(value, messenger) return date if date end - messenger.error("must_be_date", format: @date_fmt) - throw :failure + throw :failure, Messaging::Messages::MustBeDate.new(code_data: { format: @date_fmt }) end private diff --git a/lib/sheetah/types/scalars/email_cast.rb b/lib/sheetah/types/scalars/email_cast.rb index 3e86dcc..066b252 100644 --- a/lib/sheetah/types/scalars/email_cast.rb +++ b/lib/sheetah/types/scalars/email_cast.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "uri" +require_relative "../../messaging/messages/must_be_email" require_relative "../cast" module Sheetah @@ -16,12 +17,10 @@ def initialize(email_matcher: EMAIL_REGEXP, **) @email_matcher = email_matcher end - def call(value, messenger) + def call(value, _messenger) return value if @email_matcher.match?(value) - messenger.error("must_be_email", value: value.inspect) - - throw :failure + throw :failure, Messaging::Messages::MustBeEmail.new(code_data: { value: value.inspect }) end end end diff --git a/lib/sheetah/types/scalars/scalar_cast.rb b/lib/sheetah/types/scalars/scalar_cast.rb index 4de44ea..2d8635b 100644 --- a/lib/sheetah/types/scalars/scalar_cast.rb +++ b/lib/sheetah/types/scalars/scalar_cast.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true require_relative "../../utils/cell_string_cleaner" +require_relative "../../messaging/messages/must_exist" +require_relative "../../messaging/messages/cleaned_string" require_relative "../cast" module Sheetah @@ -28,7 +30,7 @@ def handle_nil(value) if @nullable throw :success, nil else - throw :failure, "must_exist" + throw :failure, Messaging::Messages::MustExist.new end end @@ -37,7 +39,7 @@ def handle_garbage(value, messenger) clean_string = Utils::CellStringCleaner.call(value) - messenger.warn("cleaned_string") if clean_string != value + messenger.warn(Messaging::Messages::CleanedString.new) if clean_string != value clean_string end diff --git a/lib/sheetah/types/scalars/string.rb b/lib/sheetah/types/scalars/string.rb index e013c0f..32174b2 100644 --- a/lib/sheetah/types/scalars/string.rb +++ b/lib/sheetah/types/scalars/string.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "scalar" +require_relative "../../messaging/messages/must_be_string" module Sheetah module Types @@ -10,7 +11,7 @@ module Scalars # is an instance of a String subclass next value.to_s if value.is_a?(::String) - throw :failure, "must_be_string" + throw :failure, Messaging::Messages::MustBeString.new end end end diff --git a/spec/sheetah/backends_spec.rb b/spec/sheetah/backends_spec.rb index 2922b2a..7311455 100644 --- a/spec/sheetah/backends_spec.rb +++ b/spec/sheetah/backends_spec.rb @@ -37,7 +37,7 @@ result = described_class.open(foo, bar: bar) expect(result).to be_failure - expect(result.failure).to have_attributes(msg_code: "no_applicable_backend") + expect(result.failure).to eq(Sheetah::Messaging::Messages::NoApplicableBackend.new) end end end diff --git a/spec/sheetah/headers_spec.rb b/spec/sheetah/headers_spec.rb index 34d8d83..e47bf50 100644 --- a/spec/sheetah/headers_spec.rb +++ b/spec/sheetah/headers_spec.rb @@ -26,8 +26,8 @@ end let(:sheet_headers) do - Array.new(5) do - instance_double(Sheetah::Sheet::Header, col: double, value: double) + Array.new(5) do |i| + instance_double(Sheetah::Sheet::Header, col: "FOO", value: "header#{i}") end end @@ -94,16 +94,14 @@ def stub_specification(column_by_header) end it "messages the error" do - expect(messenger.messages).to eq( - [ - Sheetah::Messaging::Message.new( - severity: "ERROR", - code: "invalid_header", - code_data: sheet_headers[4].value, - scope: "COL", - scope_data: { col: sheet_headers[4].col } - ), - ] + expect(messenger.messages).to contain_exactly( + be_a(Sheetah::Messaging::Message) & have_attributes( + severity: "ERROR", + code: "invalid_header", + code_data: sheet_headers[4].value, + scope: "COL", + scope_data: { col: sheet_headers[4].col } + ) ) end end @@ -120,16 +118,14 @@ def stub_specification(column_by_header) end it "considers the underlying column, not the header" do - expect(messenger.messages).to eq( - [ - Sheetah::Messaging::Message.new( - severity: "ERROR", - code: "duplicated_header", - code_data: sheet_headers[1].value, - scope: "COL", - scope_data: { col: sheet_headers[1].col } - ), - ] + expect(messenger.messages).to contain_exactly( + be_a(Sheetah::Messaging::Message) & have_attributes( + severity: "ERROR", + code: "duplicated_header", + code_data: sheet_headers[1].value, + scope: "COL", + scope_data: { col: sheet_headers[1].col } + ) ) end end diff --git a/spec/sheetah/messaging/config_spec.rb b/spec/sheetah/messaging/config_spec.rb new file mode 100644 index 0000000..8a56943 --- /dev/null +++ b/spec/sheetah/messaging/config_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "sheetah/messaging/config" + +RSpec.describe Sheetah::Messaging::Config do + describe "#validate_messages" do + it "may be false" do + config = described_class.new(validate_messages: false) + expect(config.validate_messages).to be(false) + end + + it "may be true" do + config = described_class.new(validate_messages: true) + expect(config.validate_messages).to be(true) + end + end + + describe "#validate_messages=" do + it "can become true" do + config = described_class.new(validate_messages: false) + config.validate_messages = true + expect(config.validate_messages).to be(true) + end + + it "can become false" do + config = described_class.new(validate_messages: true) + config.validate_messages = false + expect(config.validate_messages).to be(false) + end + end +end diff --git a/spec/sheetah/messaging/message_spec.rb b/spec/sheetah/messaging/message_spec.rb index d934b41..79e45d6 100644 --- a/spec/sheetah/messaging/message_spec.rb +++ b/spec/sheetah/messaging/message_spec.rb @@ -65,6 +65,22 @@ expect(message).not_to eq(other_message) end + describe "#to_h" do + it "returns the attributes as a hash" do + attrs = { + code: double, + code_data: double, + scope: double, + scope_data: double, + severity: double, + } + + message = described_class.new(**attrs) + + expect(message.to_h).to eq(attrs) + end + end + describe "#to_s" do let(:code) { "foo_is_bar" } let(:code_data) { nil } @@ -124,4 +140,147 @@ end end end + + describe "validations" do + it "is valid by default" do + msg = described_class.new(code: double, validatable: true) + + expect(msg.validate).to be_nil + end + + context "when customized" do + let(:msg_class) do + Class.new(described_class) do + def self.code + "foobar" + end + + validate_with do + row + + def validate_code_data(message) + message.code_data.is_a?(Hash) + end + end + end + end + + let(:msg) do + msg_class.new( + code: "foobar", + code_data: {}, + scope: "ROW", + scope_data: { row: 42 }, + validatable: true + ) + end + + it "may be valid" do + expect(msg.validate).to be_nil + end + + it "validates the code" do + msg.code = "qoifo" + + expect { msg.validate }.to raise_error( + Sheetah::Messaging::MessageValidations::InvalidMessage, + /^code / + ) + end + + it "validates the code data" do + msg.code_data = nil + + expect { msg.validate }.to raise_error( + Sheetah::Messaging::MessageValidations::InvalidMessage, + /^code_data / + ) + end + + it "validates the scope" do + msg.scope = "SHEET" + + expect { msg.validate }.to raise_error( + Sheetah::Messaging::MessageValidations::InvalidMessage, + /^scope / + ) + end + + it "validates the scope_data" do + msg.scope_data = nil + + expect { msg.validate }.to raise_error( + Sheetah::Messaging::MessageValidations::InvalidMessage, + /^scope_data / + ) + end + + it "validates multiple attributes at once" do + msg.code_data = nil + msg.scope_data = nil + + expect { msg.validate }.to raise_error( + Sheetah::Messaging::MessageValidations::InvalidMessage, + /^code_data, scope_data / + ) + end + + it "may ignore validations" do + msg = msg_class.new(validatable: false) + + expect(msg.validate).to be_nil + end + + describe "inheritance" do + let(:msg1_class) do + Class.new(msg_class) do + def self.code + "barbaz" + end + end + end + + let(:msg1) do + msg1_class.new( + code: "barbaz", + code_data: {}, + scope: "ROW", + scope_data: { row: 42 }, + validatable: true + ) + end + + it "may rely on a parent validator" do + expect(msg1.validate).to be_nil + + msg1.scope = "SHEET" + + expect { msg1.validate }.to raise_error( + Sheetah::Messaging::MessageValidations::InvalidMessage, + /^scope / + ) + end + end + end + end + + describe "initializations" do + before do + allow(described_class).to receive(:validate) + end + + describe "::new" do + it "will not validate after initialization" do + described_class.new(code: double, validatable: true) + expect(described_class).not_to have_received(:validate) + end + end + + describe "::new!" do + it "will validate after initialization" do + message = described_class.new!(code: double, validatable: true) + expect(described_class).to have_received(:validate).with(message) + end + end + end end diff --git a/spec/sheetah/messaging/messages/cleaned_string_spec.rb b/spec/sheetah/messaging/messages/cleaned_string_spec.rb new file mode 100644 index 0000000..7d8a95b --- /dev/null +++ b/spec/sheetah/messaging/messages/cleaned_string_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/cleaned_string" + +RSpec.describe Sheetah::Messaging::Messages::CleanedString do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "cleaned_string", + code_data: nil, + scope: "CELL", + scope_data: { col: "FOO", row: 42 }, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messages/duplicated_header_spec.rb b/spec/sheetah/messaging/messages/duplicated_header_spec.rb new file mode 100644 index 0000000..89faad7 --- /dev/null +++ b/spec/sheetah/messaging/messages/duplicated_header_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/duplicated_header" + +RSpec.describe Sheetah::Messaging::Messages::DuplicatedHeader do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "duplicated_header", + code_data: "header_foo", + scope: "COL", + scope_data: { col: "FOO" }, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messages/invalid_header_spec.rb b/spec/sheetah/messaging/messages/invalid_header_spec.rb new file mode 100644 index 0000000..045579a --- /dev/null +++ b/spec/sheetah/messaging/messages/invalid_header_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/invalid_header" + +RSpec.describe Sheetah::Messaging::Messages::InvalidHeader do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "invalid_header", + code_data: "header_foo", + scope: "COL", + scope_data: { col: "FOO" }, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messages/missing_column_spec.rb b/spec/sheetah/messaging/messages/missing_column_spec.rb new file mode 100644 index 0000000..39d431a --- /dev/null +++ b/spec/sheetah/messaging/messages/missing_column_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/missing_column" + +RSpec.describe Sheetah::Messaging::Messages::MissingColumn do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "missing_column", + code_data: "header_foo", + scope: "SHEET", + scope_data: nil, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messages/must_be_array_spec.rb b/spec/sheetah/messaging/messages/must_be_array_spec.rb new file mode 100644 index 0000000..24202e3 --- /dev/null +++ b/spec/sheetah/messaging/messages/must_be_array_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/must_be_array" + +RSpec.describe Sheetah::Messaging::Messages::MustBeArray do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "must_be_array", + code_data: nil, + scope: "CELL", + scope_data: { col: "FOO", row: 42 }, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messages/must_be_boolsy_spec.rb b/spec/sheetah/messaging/messages/must_be_boolsy_spec.rb new file mode 100644 index 0000000..f46872f --- /dev/null +++ b/spec/sheetah/messaging/messages/must_be_boolsy_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/must_be_boolsy" + +RSpec.describe Sheetah::Messaging::Messages::MustBeBoolsy do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "must_be_boolsy", + code_data: { value: "foo" }, + scope: "CELL", + scope_data: { col: "FOO", row: 42 }, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messages/must_be_date_spec.rb b/spec/sheetah/messaging/messages/must_be_date_spec.rb new file mode 100644 index 0000000..3988c6c --- /dev/null +++ b/spec/sheetah/messaging/messages/must_be_date_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/must_be_date" + +RSpec.describe Sheetah::Messaging::Messages::MustBeDate do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "must_be_date", + code_data: { format: "foo" }, + scope: "CELL", + scope_data: { col: "FOO", row: 42 }, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messages/must_be_email_spec.rb b/spec/sheetah/messaging/messages/must_be_email_spec.rb new file mode 100644 index 0000000..6e4f814 --- /dev/null +++ b/spec/sheetah/messaging/messages/must_be_email_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/must_be_email" + +RSpec.describe Sheetah::Messaging::Messages::MustBeEmail do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "must_be_email", + code_data: { value: "foo" }, + scope: "CELL", + scope_data: { col: "FOO", row: 42 }, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messages/must_be_string_spec.rb b/spec/sheetah/messaging/messages/must_be_string_spec.rb new file mode 100644 index 0000000..bd31937 --- /dev/null +++ b/spec/sheetah/messaging/messages/must_be_string_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/must_be_string" + +RSpec.describe Sheetah::Messaging::Messages::MustBeString do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "must_be_string", + code_data: nil, + scope: "CELL", + scope_data: { col: "FOO", row: 42 }, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messages/must_exist_spec.rb b/spec/sheetah/messaging/messages/must_exist_spec.rb new file mode 100644 index 0000000..b85afcf --- /dev/null +++ b/spec/sheetah/messaging/messages/must_exist_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/must_exist" + +RSpec.describe Sheetah::Messaging::Messages::MustExist do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "must_exist", + code_data: nil, + scope: "CELL", + scope_data: { col: "FOO", row: 42 }, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messages/no_applicable_backend_spec.rb b/spec/sheetah/messaging/messages/no_applicable_backend_spec.rb new file mode 100644 index 0000000..1998080 --- /dev/null +++ b/spec/sheetah/messaging/messages/no_applicable_backend_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/no_applicable_backend" + +RSpec.describe Sheetah::Messaging::Messages::NoApplicableBackend do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "no_applicable_backend", + code_data: nil, + scope: "SHEET", + scope_data: nil, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messages/sheet_error_spec.rb b/spec/sheetah/messaging/messages/sheet_error_spec.rb new file mode 100644 index 0000000..8ebff7b --- /dev/null +++ b/spec/sheetah/messaging/messages/sheet_error_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "sheetah/messaging/messages/sheet_error" + +RSpec.describe Sheetah::Messaging::Messages::SheetError do + it "has a default code" do + expect(described_class.new).to have_attributes(code: described_class::CODE) + end + + it "may be valid" do + msg = described_class.new( + code: "sheet_error", + code_data: nil, + scope: "SHEET", + scope_data: nil, + validatable: true + ) + + expect(msg.validate).to be_nil + end +end diff --git a/spec/sheetah/messaging/messenger_spec.rb b/spec/sheetah/messaging/messenger_spec.rb index fad393d..f883e9f 100644 --- a/spec/sheetah/messaging/messenger_spec.rb +++ b/spec/sheetah/messaging/messenger_spec.rb @@ -354,15 +354,19 @@ def stub_scope_col!(receiver, *args, &block) let(:code) { double } let(:code_data) { double } + let(:message) do + Sheetah::Messaging::Message.new(code: code, code_data: code_data) + end + let(:messenger) { described_class.new(scope: scope, scope_data: scope_data) } describe "#warn" do it "returns the receiver" do - expect(messenger.warn(code, code_data)).to be(messenger) + expect(messenger.warn(message)).to be(messenger) end it "adds the code & code_data as a warning" do - messenger.warn(code, code_data) + messenger.warn(message) expect(messenger.messages).to contain_exactly( Sheetah::Messaging::Message.new( @@ -374,29 +378,15 @@ def stub_scope_col!(receiver, *args, &block) ) ) end - - it "may do without code_data" do - messenger.warn(code) - - expect(messenger.messages).to contain_exactly( - Sheetah::Messaging::Message.new( - code: code, - code_data: nil, - scope: scope, - scope_data: scope_data, - severity: severities::WARN - ) - ) - end end describe "#error" do it "returns the receiver" do - expect(messenger.error(code, code_data)).to be(messenger) + expect(messenger.error(message)).to be(messenger) end it "adds the code & code_data as an error" do - messenger.error(code, code_data) + messenger.error(message) expect(messenger.messages).to contain_exactly( Sheetah::Messaging::Message.new( @@ -408,20 +398,6 @@ def stub_scope_col!(receiver, *args, &block) ) ) end - - it "may do without code_data" do - messenger.error(code) - - expect(messenger.messages).to contain_exactly( - Sheetah::Messaging::Message.new( - code: code, - code_data: nil, - scope: scope, - scope_data: scope_data, - severity: severities::ERROR - ) - ) - end end end end diff --git a/spec/sheetah/messaging_spec.rb b/spec/sheetah/messaging_spec.rb new file mode 100644 index 0000000..8126e03 --- /dev/null +++ b/spec/sheetah/messaging_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "sheetah/messaging" + +RSpec.describe Sheetah::Messaging do + around do |example| + config = described_class.config + example.run + described_class.config = config + end + + describe "::config" do + it "reads a global, frozen instance" do + expect(described_class.config).to be_a(described_class::Config) & be_frozen + end + end + + describe "::config=" do + it "writes a global instance" do + described_class.config = (config = double) + expect(described_class.config).to eq(config) + end + end + + describe "::configure" do + let(:old) { instance_double(described_class::Config, dup: new) } + let(:new) { instance_double(described_class::Config) } + + before do + described_class.config = old + end + + it "modifies a copy of the global instance" do + expect do |b| + described_class.configure(&b) + end.to yield_with_args(new) + + expect(described_class.config).to be(new) + end + end +end diff --git a/spec/sheetah/sheet_processor_spec.rb b/spec/sheetah/sheet_processor_spec.rb index 75f4b9e..76ed79a 100644 --- a/spec/sheetah/sheet_processor_spec.rb +++ b/spec/sheetah/sheet_processor_spec.rb @@ -98,11 +98,11 @@ def stub_sheet_open_ko(failure = double) context "when there is a sheet error" do let(:error) do - instance_double(Sheetah::Sheet::Error, msg_code: code) + instance_double(Sheetah::Sheet::Error, to_message: message) end - let(:code) do - double + let(:message) do + Sheetah::Messaging::Message.new(code: double, code_data: double) end before do @@ -115,8 +115,8 @@ def stub_sheet_open_ko(failure = double) result: Failure(), messages: [ Sheetah::Messaging::Message.new( - code: code, - code_data: nil, + code: message.code, + code_data: message.code_data, scope: "SHEET", scope_data: nil, severity: "ERROR" diff --git a/spec/sheetah/types/composites/array_spec.rb b/spec/sheetah/types/composites/array_spec.rb index 89b15e6..51e49c6 100644 --- a/spec/sheetah/types/composites/array_spec.rb +++ b/spec/sheetah/types/composites/array_spec.rb @@ -41,7 +41,9 @@ let(:value_is_array) { false } it "is a failure" do - expect { cast.call(value, messenger) }.to throw_symbol(:failure, "must_be_array") + expect do + cast.call(value, messenger) + end.to throw_symbol(:failure, Sheetah::Messaging::Messages::MustBeArray.new) end end end diff --git a/spec/sheetah/types/scalars/boolsy_cast_spec.rb b/spec/sheetah/types/scalars/boolsy_cast_spec.rb index 60b988b..d734523 100644 --- a/spec/sheetah/types/scalars/boolsy_cast_spec.rb +++ b/spec/sheetah/types/scalars/boolsy_cast_spec.rb @@ -42,8 +42,12 @@ def expect_falsy(value = self.value) end def expect_failure(value = self.value) - expect(messenger).to receive(:error).with("must_be_boolsy", value: value.inspect) - expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil) + expect do + cast.call(value, messenger) + end.to throw_symbol( + :failure, + Sheetah::Messaging::Messages::MustBeBoolsy.new(code_data: { value: value.inspect }) + ) end context "when the value is truthy" do diff --git a/spec/sheetah/types/scalars/date_string_cast_spec.rb b/spec/sheetah/types/scalars/date_string_cast_spec.rb index f46f8e7..15a8bc3 100644 --- a/spec/sheetah/types/scalars/date_string_cast_spec.rb +++ b/spec/sheetah/types/scalars/date_string_cast_spec.rb @@ -44,8 +44,12 @@ let(:accept_date) { false } it "fails with an error" do - expect(messenger).to receive(:error).with("must_be_date", format: default_fmt) - expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil) + expect do + cast.call(value, messenger) + end.to throw_symbol( + :failure, + Sheetah::Messaging::Messages::MustBeDate.new(code_data: { format: default_fmt }) + ) end end end @@ -73,8 +77,12 @@ end it "fails with an error" do - expect(messenger).to receive(:error).with("must_be_date", format: date_fmt) - expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil) + expect do + cast.call(value, messenger) + end.to throw_symbol( + :failure, + Sheetah::Messaging::Messages::MustBeDate.new(code_data: { format: date_fmt }) + ) end end @@ -84,8 +92,12 @@ end it "fails with an error" do - expect(messenger).to receive(:error).with("must_be_date", format: date_fmt) - expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil) + expect do + cast.call(value, messenger) + end.to throw_symbol( + :failure, + Sheetah::Messaging::Messages::MustBeDate.new(code_data: { format: date_fmt }) + ) end end end @@ -96,8 +108,12 @@ end it "fails with an error" do - expect(messenger).to receive(:error).with("must_be_date", format: default_fmt) - expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil) + expect do + cast.call(value, messenger) + end.to throw_symbol( + :failure, + Sheetah::Messaging::Messages::MustBeDate.new(code_data: { format: default_fmt }) + ) end end end diff --git a/spec/sheetah/types/scalars/email_cast_spec.rb b/spec/sheetah/types/scalars/email_cast_spec.rb index f6cb344..26f0161 100644 --- a/spec/sheetah/types/scalars/email_cast_spec.rb +++ b/spec/sheetah/types/scalars/email_cast_spec.rb @@ -41,8 +41,12 @@ let(:value_is_email) { false } it "adds an error message and throws :failure" do - expect(messenger).to receive(:error).with("must_be_email", value: value.inspect) - expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil) + expect do + cast.call(value, messenger) + end.to throw_symbol( + :failure, + Sheetah::Messaging::Messages::MustBeEmail.new(code_data: { value: value.inspect }) + ) end end end diff --git a/spec/sheetah/types/scalars/scalar_cast_spec.rb b/spec/sheetah/types/scalars/scalar_cast_spec.rb index 0dd1708..a69399c 100644 --- a/spec/sheetah/types/scalars/scalar_cast_spec.rb +++ b/spec/sheetah/types/scalars/scalar_cast_spec.rb @@ -31,7 +31,9 @@ it "halts with a failure and an appropriate error code" do expect do cast.call(nil, messenger) - end.to throw_symbol(:failure, "must_exist") + end.to throw_symbol( + :failure, Sheetah::Messaging::Messages::MustExist.new + ) end end end @@ -54,7 +56,8 @@ value = cast.call(string_with_garbage, messenger) expect(value).to eq(string_without_garbage) - expect(messenger).to have_received(:warn).with("cleaned_string") + expect(messenger).to have_received(:warn) + .with(Sheetah::Messaging::Messages::CleanedString.new) end end diff --git a/spec/sheetah/types/scalars/string_spec.rb b/spec/sheetah/types/scalars/string_spec.rb index ccdd491..bc901f4 100644 --- a/spec/sheetah/types/scalars/string_spec.rb +++ b/spec/sheetah/types/scalars/string_spec.rb @@ -46,7 +46,11 @@ let(:value_is_string) { false } it "is a failure" do - expect { cast.call(value, messenger) }.to throw_symbol(:failure, "must_be_string") + expect do + cast.call(value, messenger) + end.to throw_symbol( + :failure, Sheetah::Messaging::Messages::MustBeString.new + ) end end end diff --git a/spec/support.rb b/spec/support.rb index 3048425..3b0e1eb 100644 --- a/spec/support.rb +++ b/spec/support.rb @@ -4,3 +4,4 @@ require_relative "support/simplecov" require_relative "support/monadic_result" require_relative "support/fixtures" +require_relative "support/sheetah" diff --git a/spec/support/sheetah.rb b/spec/support/sheetah.rb new file mode 100644 index 0000000..014bbd5 --- /dev/null +++ b/spec/support/sheetah.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "sheetah/messaging" + +Sheetah::Messaging.configure do |config| + config.validate_messages = true +end