Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement concerns #19

Merged
merged 2 commits into from
Oct 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions lib/kangaru/concerns/attributes_concern.rb
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions lib/kangaru/concerns/concern.rb
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions sig/kangaru/concerns/attributes_concern.rbs
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions sig/kangaru/concerns/concern.rbs
Original file line number Diff line number Diff line change
@@ -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
97 changes: 97 additions & 0 deletions spec/kangaru/concerns/attributes_concern_spec.rb
Original file line number Diff line number Diff line change
@@ -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
81 changes: 81 additions & 0 deletions spec/kangaru/concerns/concern_spec.rb
Original file line number Diff line number Diff line change
@@ -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