diff --git a/.github/workflows/spec.yml b/.github/workflows/spec.yml index e1cb5fd..c630648 100644 --- a/.github/workflows/spec.yml +++ b/.github/workflows/spec.yml @@ -19,10 +19,6 @@ jobs: - name: Install dependencies run: bundle install - name: Test - if: ${{ contains(matrix.ruby, 'truffleruby') }} - run: bundle exec rspec --exclude-pattern "**/*_pattern_matching_spec.rb" - - name: Test - if: ${{ !contains(matrix.ruby, 'truffleruby') }} run: bundle exec rspec - name: Coveralls uses: coverallsapp/github-action@v1.1.2 diff --git a/lib/fear/struct.rb b/lib/fear/struct.rb deleted file mode 100644 index 0a5c0df..0000000 --- a/lib/fear/struct.rb +++ /dev/null @@ -1,235 +0,0 @@ -# frozen_string_literal: true - -module Fear - # Structs are like regular classes and good for modeling immutable data. - # - # A minimal struct requires just a list of attributes: - # - # User = Fear::Struct.with_attributes(:id, :email, :admin) - # john = User.new(id: 2, email: 'john@example.com', admin: false) - # - # john.email #=> 'john@example.com' - # - # Instead of `.with_attributes` factory method you can use classic inheritance: - # - # class User < Fear::Struct - # attribute :id - # attribute :email - # attribute :admin - # end - # - # Since structs are immutable, you are not allowed to reassign their attributes - # - # john.email = "john.doe@example.com" #=> raises NoMethodError - # - # Two structs of the same type with the same attributes are equal - # - # john1 = User.new(id: 2, email: 'john@example.com', admin: false) - # john2 = User.new(id: 2, admin: false, email: 'john@example.com') - # john1 == john2 #=> true - # - # You can create a shallow copy of a +Struct+ by using copy method optionally changing its attributes. - # - # john = User.new(id: 2, email: 'john@example.com', admin: false) - # admin_john = john.copy(admin: true) - # - # john.admin #=> false - # admin_john.admin #=> true - # - class Struct - include PatternMatch.mixin - - @attributes = [].freeze - - class << self - # @param base [Fear::Struct] - # @api private - def inherited(base) - base.instance_variable_set(:@attributes, attributes) - end - - # Defines attribute - # - # @param name [Symbol] - # @return [Symbol] attribute name - # - # @example - # class User < Fear::Struct - # attribute :id - # attribute :email - # end - # - def attribute(name) - name.to_sym.tap do |symbolized_name| - @attributes << symbolized_name - attr_reader symbolized_name - end - end - - # Members of this struct - # - # @return [] - def attributes - @attributes.dup - end - - # Creates new struct with given attributes - # @param members [] - # @return [Fear::Struct] - # - # @example - # User = Fear::Struct.with_attributes(:id, :email, :admin) do - # def admin? - # @admin - # end - # end - # - def with_attributes(*members, &block) - members = members - block = block - - Class.new(self) do - members.each { |member| attribute(member) } - class_eval(&block) if block - end - end - end - - # @param attributes [{Symbol => any}] - def initialize(**attributes) - _check_missing_attributes!(attributes) - _check_unknown_attributes!(attributes) - - @values = members.each_with_object([]) do |name, values| - attributes.fetch(name).tap do |value| - _set_attribute(name, value) - values << value - end - end - end - - # Creates a shallow copy of this struct optionally changing the attributes arguments. - # @param attributes [{Symbol => any}] - # - # @example - # User = Fear::Struct.new(:id, :email, :admin) - # john = User.new(id: 2, email: 'john@example.com', admin: false) - # john.admin #=> false - # admin_john = john.copy(admin: true) - # admin_john.admin #=> true - # - def copy(**attributes) - self.class.new(**to_h.merge(attributes)) - end - - # Returns the struct attributes as an array of symbols - # @return [] - # - # @example - # User = Fear::Struct.new(:id, :email, :admin) - # john = User.new(email: 'john@example.com', admin: false, id: 2) - # john.attributes #=> [:id, :email, :admin] - # - def members - self.class.attributes - end - - # Returns the values for this struct as an Array. - # @return [Array] - # - # @example - # User = Fear::Struct.new(:id, :email, :admin) - # john = User.new(email: 'john@example.com', admin: false, id: 2) - # john.to_a #=> [2, 'john@example.com', false] - # - def to_a - @values.dup - end - - # @overload to_h() - # Returns a Hash containing the names and values for the struct's attributes - # @return [{Symbol => any}] - # - # @overload to_h(&block) - # Applies block to pairs of name name and value and use them to construct hash - # @yieldparam pair [] yields pair of name name and value - # @return [{Symbol => any}] - # - # @example - # User = Fear::Struct.new(:id, :email, :admin) - # john = User.new(email: 'john@example.com', admin: false, id: 2) - # john.to_h #=> {id: 2, email: 'john@example.com', admin: false} - # john.to_h do |key, value| - # [key.to_s, value] - # end #=> {'id' => 2, 'email' => 'john@example.com', 'admin' => false} - # - def to_h(&block) - pairs = members.zip(@values) - if block_given? - Hash[pairs.map(&block)] - else - Hash[pairs] - end - end - - # @param other [any] - # @return [Boolean] - def ==(other) - other.is_a?(other.class) && to_h == other.to_h - end - - INSPECT_TEMPLATE = "<#Fear::Struct %{class_name} %{attributes}>" - private_constant :INSPECT_TEMPLATE - - # @return [String] - # - # @example - # User = Fear::Struct.with_attributes(:id, :email) - # user = User.new(id: 2, email: 'john@exmaple.com') - # user.inspect #=> "<#Fear::Struct User id=2, email=>'john@exmaple.com'>" - # - def inspect - attributes = to_h.map { |key, value| "#{key}=#{value.inspect}" }.join(", ") - - format(INSPECT_TEMPLATE, class_name: self.class.name, attributes: attributes) - end - alias to_s inspect - - MISSING_KEYWORDS_ERROR = "missing keywords: %{keywords}" - private_constant :MISSING_KEYWORDS_ERROR - - private def _check_missing_attributes!(provided_attributes) - missing_attributes = members - provided_attributes.keys - - unless missing_attributes.empty? - raise ArgumentError, format(MISSING_KEYWORDS_ERROR, keywords: missing_attributes.join(", ")) - end - end - - UNKNOWN_KEYWORDS_ERROR = "unknown keywords: %{keywords}" - private_constant :UNKNOWN_KEYWORDS_ERROR - - private def _check_unknown_attributes!(provided_attributes) - unknown_attributes = provided_attributes.keys - members - - unless unknown_attributes.empty? - raise ArgumentError, format(UNKNOWN_KEYWORDS_ERROR, keywords: unknown_attributes.join(", ")) - end - end - - # @return [void] - private def _set_attribute(name, value) - instance_variable_set(:"@#{name}", value) - end - - # @param keys [Hash, nil] - # @return [Hash] - def deconstruct_keys(keys) - if keys - to_h.slice(*(self.class.attributes & keys)) - else - to_h - end - end - end -end diff --git a/spec/struct_pattern_matching_spec.rb b/spec/struct_pattern_matching_spec.rb deleted file mode 100644 index d5eb5b4..0000000 --- a/spec/struct_pattern_matching_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Fear::Struct do - describe "pattern matching" do - subject do - case struct - in Fear::Struct(a: 42) - "a = 42" - in Fear::Struct(a: 43, **rest) - "a = 43, #{rest}" - in Fear::Struct(a:) - "a = #{a}" - end - end - - let(:struct_class) { described_class.with_attributes(:a, :b) } - - context "when match single value" do - let(:struct) { struct_class.new(b: 43, a: 42) } - - it { is_expected.to eq("a = 42") } - end - - context "when match single value and capture the rest" do - let(:struct) { struct_class.new(b: 42, a: 43) } - - it { is_expected.to eq("a = 43, {:b=>42}") } - end - - context "when capture a value" do - let(:struct) { struct_class.new(b: 45, a: 44) } - - it { is_expected.to eq("a = 44") } - end - end -end diff --git a/spec/struct_spec.rb b/spec/struct_spec.rb deleted file mode 100644 index 28d19b3..0000000 --- a/spec/struct_spec.rb +++ /dev/null @@ -1,194 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Fear::Struct do - describe ".with_attributes" do - context "same arguments" do - subject { struct_class.new(a: 42, b: 43) } - - let(:struct_class) { described_class.with_attributes(:a, :b) } - - it { is_expected.to have_attributes(a: 42, b: 43) } - end - - context "string arguments" do - subject { -> { struct_class.new("a" => 42, "b" => 43) } } - - let(:struct_class) { described_class.with_attributes(:a, :b) } - - it { is_expected.to raise_error(ArgumentError) } - end - - context "extra argument" do - subject { -> { struct_class.new(a: 42, b: 41, c: 43, d: 44) } } - - let(:struct_class) { described_class.with_attributes(:a, :b) } - - it { is_expected.to raise_error(ArgumentError, "unknown keywords: c, d") } - end - - context "missing argument" do - subject { -> { struct_class.new } } - - let(:struct_class) { described_class.with_attributes(:a, :b) } - - it { is_expected.to raise_error(ArgumentError, "missing keywords: a, b") } - end - - context "inheritance" do - let(:parent_struct) { described_class.with_attributes(:a, :b) } - - it "does not change parent attributes" do - expect do - parent_struct.with_attributes(:c) - end.not_to change { parent_struct.attributes }.from([:a, :b]) - end - - it "extends parent attributes" do - child_struct = parent_struct.with_attributes(:c) - expect(child_struct.attributes).to eq([:a, :b, :c]) - end - end - - context "with block" do - subject { struct_class.new(a: 42, b: 43).a_plus_b } - - let(:struct_class) do - described_class.with_attributes(:a, :b) do - def a_plus_b - a + b - end - end - end - - it "evaluates block in context of struct" do - is_expected.to eq(85) - end - end - end - - describe "#==" do - context "with members" do - let(:struct_class) { described_class.with_attributes(:a, :b) } - - context "same class and members" do - subject { struct_class.new(a: 42, b: 43) == struct_class.new(a: 42, b: 43) } # rubocop: disable Lint/BinaryOperatorWithIdenticalOperands - - it { is_expected.to eq(true) } - end - - context "same class and different members" do - subject { struct_class.new(a: 42, b: 43) == struct_class.new(a: 42, b: 0) } - - it { is_expected.to eq(false) } - end - - context "different class and same members" do - subject { struct_class.new(a: 42, b: 43) == struct_class_1.new(a: 42, b: 43) } - - let(:struct_class_1) { described_class.with_attributes(:a, :b) } - - it { is_expected.to eq(true) } - end - - context "different class and different members" do - subject { struct_class.new(a: 42, b: 43) == struct_class.new(a: 42, b: 0) } - - let(:struct_class_1) { described_class.with_attributes(:a, :b) } - - it { is_expected.to eq(false) } - end - end - end - - describe "#members" do - let(:struct) { struct_class.new(b: 43, a: 42) } - let(:struct_class) { described_class.with_attributes(:a, :b) } - - it "returns members in the order they defined" do - expect(struct.members).to eq([:a, :b]) - end - - it "is immutable" do - expect { struct.members << :c }.not_to change { struct.members }.from([:a, :b]) - end - end - - describe "#to_a" do - let(:struct) { struct_class.new(b: 43, a: 42) } - let(:struct_class) { described_class.with_attributes(:a, :b) } - - it "returns members values in the order they defined" do - expect(struct.to_a).to eq([42, 43]) - end - - it "is immutable" do - expect { struct.to_a << 44 }.not_to change { struct.to_a }.from([42, 43]) - end - end - - describe "#to_h" do - let(:struct_class) { described_class.with_attributes(:a, :b) } - - context "without block" do - let(:struct) { struct_class.new(b: 43, a: 42) } - - it "returns a Hash containing the names and values for the structs members" do - expect(struct.to_h).to eq(a: 42, b: 43) - end - - it "is immutable" do - expect { struct.to_h.merge(c: 44) }.not_to change { struct.to_h }.from(a: 42, b: 43) - end - end - - context "with block" do - subject do - struct.to_h do |key, value| - [key.upcase, value / 2] - end - end - let(:struct) { struct_class.new(b: 2, a: 4) } - - it "returns a Hash containing the names and values for the structs members" do - is_expected.to eq(A: 2, B: 1) - end - end - end - - describe "#copy" do - let(:struct_class) { described_class.with_attributes(:a, :b) } - let(:struct) { struct_class.new(b: 43, a: 42) } - - context "attributes given" do - subject { struct.copy(b: 44) } - - it { is_expected.to eq(struct_class.new(a: 42, b: 44)) } - end - - context "string attributes" do - subject { -> { struct.copy("a" => 44) } } - - it { is_expected.to raise_error(ArgumentError) } - end - - context "no attributes given" do - subject { struct.copy == struct } - - it { is_expected.to eq(true) } - end - end - - describe "#inspect" do - subject { StrInspect.new(a: 2, b: nil).inspect } - StrInspect = Fear::Struct.with_attributes(:a, :b) - - it { is_expected.to eq("<#Fear::Struct StrInspect a=2, b=nil>") } - end - - describe "#inspect" do - subject { StrToS.new(a: 2, b: nil).inspect } - StrToS = Fear::Struct.with_attributes(:a, :b) - - it { is_expected.to eq("<#Fear::Struct StrToS a=2, b=nil>") } - end -end