From fb119f1720beb8618ce891de4e551b2a2a14d474 Mon Sep 17 00:00:00 2001 From: Chris Welham <71787007+apexatoll@users.noreply.github.com> Date: Sun, 1 Oct 2023 22:36:05 +0100 Subject: [PATCH 1/2] Create base concern --- lib/kangaru/concerns/concern.rb | 33 +++++++++++ sig/kangaru/concerns/concern.rbs | 15 +++++ spec/kangaru/concerns/concern_spec.rb | 81 +++++++++++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 lib/kangaru/concerns/concern.rb create mode 100644 sig/kangaru/concerns/concern.rbs create mode 100644 spec/kangaru/concerns/concern_spec.rb diff --git a/lib/kangaru/concerns/concern.rb b/lib/kangaru/concerns/concern.rb new file mode 100644 index 0000000..56f83c9 --- /dev/null +++ b/lib/kangaru/concerns/concern.rb @@ -0,0 +1,33 @@ +module Kangaru + module Concerns + module Concern + def append_features(base) + super + evaluate_concern_blocks!(base) + end + + def class_methods(&) + if const_defined?(:ClassMethods) + const_get(:ClassMethods) + else + const_set(:ClassMethods, Module.new) + end.module_eval(&) + end + + def included(base = nil, &block) + super base if base + return if block.nil? + + @included = block + end + + private + + def evaluate_concern_blocks!(base) + base.extend(const_get(:ClassMethods)) if const_defined?(:ClassMethods) + + base.class_eval(&@included) if instance_variable_defined?(:@included) + end + end + end +end diff --git a/sig/kangaru/concerns/concern.rbs b/sig/kangaru/concerns/concern.rbs new file mode 100644 index 0000000..88cf8dc --- /dev/null +++ b/sig/kangaru/concerns/concern.rbs @@ -0,0 +1,15 @@ +module Kangaru + module Concerns + module Concern : Module + @included: untyped + + def append_features: (untyped) -> void + + def class_methods: { -> void } -> void + + def included: (?untyped?) ?{ -> void } -> void + + def evaluate_concern_blocks!: (untyped) -> void + end + end +end diff --git a/spec/kangaru/concerns/concern_spec.rb b/spec/kangaru/concerns/concern_spec.rb new file mode 100644 index 0000000..f74a242 --- /dev/null +++ b/spec/kangaru/concerns/concern_spec.rb @@ -0,0 +1,81 @@ +RSpec.describe Kangaru::Concerns::Concern do + subject(:model_class) do + Class.new { include Concern } + end + + describe ".included" do + subject(:include_concern) { model_class.include(concern) } + + let(:model_class) do + Class.new do + def self.some_static_method = nil + end + end + + let(:concern) do + Module.new do + extend Kangaru::Concerns::Concern + + included do + some_static_method + + @some_variable = true + end + end + end + + before do + allow(model_class).to receive(:some_static_method) + end + + after do + model_class.remove_instance_variable(:@some_variable) + end + + it "runs the block" do + include_concern + expect(model_class).to have_received(:some_static_method).once + end + + it "is scoped to the model class" do + expect { include_concern } + .to change { model_class.instance_variable_get(:@some_variable) } + .from(nil) + .to(true) + end + end + + describe ".class_methods" do + let(:concern) do + Module.new do + extend Kangaru::Concerns::Concern + + class_methods do + attr_reader :some_ivar + + def some_method = nil + end + end + end + + let(:concern_ivar) { :concern_ivar } + + let(:model_class_ivar) { :model_class_ivar } + + before do + stub_const "Concern", concern + stub_const "Model", model_class + + Concern.instance_variable_set(:@some_ivar, concern_ivar) + Model.instance_variable_set(:@some_ivar, model_class_ivar) + end + + it "sets the class methods" do + expect(Model).to respond_to(:some_method, :some_ivar) + end + + it "scopes instance variables to the model class" do + expect(Model.some_ivar).to eq(model_class_ivar) + end + end +end From af90f3f4971db032ca375e397ebc48f3353799db Mon Sep 17 00:00:00 2001 From: Chris Welham <71787007+apexatoll@users.noreply.github.com> Date: Sun, 1 Oct 2023 23:02:41 +0100 Subject: [PATCH 2/2] Create attributes concern --- lib/kangaru/concerns/attributes_concern.rb | 21 ++++ sig/kangaru/concerns/attributes_concern.rbs | 17 ++++ .../concerns/attributes_concern_spec.rb | 97 +++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 lib/kangaru/concerns/attributes_concern.rb create mode 100644 sig/kangaru/concerns/attributes_concern.rbs create mode 100644 spec/kangaru/concerns/attributes_concern_spec.rb diff --git a/lib/kangaru/concerns/attributes_concern.rb b/lib/kangaru/concerns/attributes_concern.rb new file mode 100644 index 0000000..c2e3c2d --- /dev/null +++ b/lib/kangaru/concerns/attributes_concern.rb @@ -0,0 +1,21 @@ +module Kangaru + module Concerns + module AttributesConcern + extend Concern + + class_methods do + def attributes + instance_methods.grep(/\w=$/).map do |attribute| + attribute.to_s.delete_suffix("=").to_sym + end + end + end + + def initialize(**attributes) + attributes.slice(*self.class.attributes).each do |attr, value| + instance_variable_set(:"@#{attr}", value) + end + end + end + end +end diff --git a/sig/kangaru/concerns/attributes_concern.rbs b/sig/kangaru/concerns/attributes_concern.rbs new file mode 100644 index 0000000..5a60a82 --- /dev/null +++ b/sig/kangaru/concerns/attributes_concern.rbs @@ -0,0 +1,17 @@ +module Kangaru + module Concerns + module AttributesConcern + extend Concern + + module ClassMethods + def attributes: -> Array[Symbol] + end + + extend ClassMethods + + def instance_methods: -> Array[Symbol] + + def initialize: (**untyped) -> void + end + end +end diff --git a/spec/kangaru/concerns/attributes_concern_spec.rb b/spec/kangaru/concerns/attributes_concern_spec.rb new file mode 100644 index 0000000..0e996ca --- /dev/null +++ b/spec/kangaru/concerns/attributes_concern_spec.rb @@ -0,0 +1,97 @@ +RSpec.describe Kangaru::Concerns::AttributesConcern do + subject(:model) { model_class.new(**attributes) } + + let(:model_class) do + Class.new { include Kangaru::Concerns::AttributesConcern } + end + + describe "#initialize" do + context "when concern has not defined any attr_accessors" do + context "and no attributes are given" do + let(:attributes) { {} } + + it "does not raise any errors" do + expect { model }.not_to raise_error + end + + it "does not set any instance variables" do + expect { model }.not_to change { model_class.instance_variables } + end + end + + context "and attributes are given" do + let(:attributes) { { foo: "foo", bar: "bar" } } + + it "does not raise any errors" do + expect { model }.not_to raise_error + end + + it "does not set the attributes" do + expect(model).not_to respond_to(*attributes.keys) + end + end + end + + context "when concern has defined attr_accessors" do + let(:model_class) do + Class.new do + include Kangaru::Concerns::AttributesConcern + + attr_accessor :foo, :bar, :baz + end + end + + context "and no attributes are given" do + let(:attributes) { {} } + + it "does not raise any errors" do + expect { model }.not_to raise_error + end + end + + context "and attributes are given" do + let(:attributes) { { foo: "foo", bar: "bar" } } + + it "does not raise any errors" do + expect { model }.not_to raise_error + end + + it "sets the attributes" do + expect(model).to have_attributes(**attributes) + end + end + end + end + + describe ".attributes" do + subject(:attributes) { model_class.attributes } + + context "when no attr_accessors are set" do + it "does not raise any errors" do + expect { attributes }.not_to raise_error + end + + it "returns an empty array" do + expect(attributes).to be_empty + end + end + + context "when attr_accessors are set" do + let(:model_class) do + Class.new do + include Kangaru::Concerns::AttributesConcern + + attr_accessor :foo, :bar, :baz + end + end + + it "does not raise any errors" do + expect { attributes }.not_to raise_error + end + + it "returns the expected attributes" do + expect(attributes).to contain_exactly(:foo, :bar, :baz) + end + end + end +end