diff --git a/.rubocop.yml b/.rubocop.yml index 69d9d99..1d8d19f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -61,6 +61,9 @@ RSpec/DescribedClass: SkipBlocks: true RSpec/ExpectChange: EnforcedStyle: block +RSpec/FilePath: + Exclude: + - spec/kangaru/components/**/*.rb RSpec/LetSetup: Enabled: false RSpec/MultipleMemoizedHelpers: diff --git a/lib/kangaru.rb b/lib/kangaru.rb index a51f4e3..56cd64c 100644 --- a/lib/kangaru.rb +++ b/lib/kangaru.rb @@ -10,6 +10,10 @@ require "yaml" module Kangaru + COLLAPSED_DIRS = [ + "#{__dir__}/kangaru/components" + ].freeze + DEFAULT_ENV = :runtime INFLECTIONS = { @@ -20,6 +24,7 @@ module Kangaru warn_on_extra_files: false ).tap do |loader| loader.inflector.inflect(INFLECTIONS) + loader.collapse(COLLAPSED_DIRS) loader.setup end diff --git a/lib/kangaru/components/component.rb b/lib/kangaru/components/component.rb new file mode 100644 index 0000000..6e9e5c0 --- /dev/null +++ b/lib/kangaru/components/component.rb @@ -0,0 +1,19 @@ +module Kangaru + class Component + using Patches::Inflections + using Patches::Source + + def render + Renderer.new(view_file).render(binding) + end + + private + + def view_file + dir = File.dirname(self.class.source) + name = File.basename(self.class.source, ".rb") + + Pathname.new(dir).join("#{name}.erb") + end + end +end diff --git a/lib/kangaru/controller.rb b/lib/kangaru/controller.rb index 29dccb0..566f5d1 100644 --- a/lib/kangaru/controller.rb +++ b/lib/kangaru/controller.rb @@ -4,7 +4,7 @@ class Controller def initialize(command) @command = command - @renderer = Renderer.new(command) + @renderer = Renderer.new(view_path) end def execute @@ -25,5 +25,14 @@ def self.const_missing(const) end private_class_method :const_missing + + private + + def view_path + Kangaru.application.view_path( + controller: command.controller, + action: command.action.to_s + ) + end end end diff --git a/lib/kangaru/patches/source.rb b/lib/kangaru/patches/source.rb new file mode 100644 index 0000000..d95d7ed --- /dev/null +++ b/lib/kangaru/patches/source.rb @@ -0,0 +1,11 @@ +module Kangaru + module Patches + module Source + refine Module do + def source + Object.const_source_location(name || raise)&.first || raise + end + end + end + end +end diff --git a/lib/kangaru/renderer.rb b/lib/kangaru/renderer.rb index 1b8be09..7d5aeea 100644 --- a/lib/kangaru/renderer.rb +++ b/lib/kangaru/renderer.rb @@ -1,24 +1,15 @@ module Kangaru class Renderer - attr_reader :command + attr_reader :path - def initialize(command) - @command = command + def initialize(path) + @path = path end def render(binding) - return unless view_path.exist? + return unless path.exist? - ERB.new(view_path.read, trim_mode: "-").run(binding) - end - - private - - def view_path - Kangaru.application.view_path( - controller: command.controller, - action: command.action.to_s - ) + ERB.new(path.read, trim_mode: "-").run(binding) end end end diff --git a/sig/kangaru.rbs b/sig/kangaru.rbs index a850ba9..b524d01 100644 --- a/sig/kangaru.rbs +++ b/sig/kangaru.rbs @@ -1,5 +1,7 @@ module Kangaru VERSION: String + + COLLAPSED_DIRS: Array[String] DEFAULT_ENV: Symbol INFLECTIONS: Hash[String, String] diff --git a/sig/kangaru/components/component.rbs b/sig/kangaru/components/component.rbs new file mode 100644 index 0000000..b8dcfbf --- /dev/null +++ b/sig/kangaru/components/component.rbs @@ -0,0 +1,9 @@ +module Kangaru + class Component + def render: -> void + + private + + def view_file: -> Pathname + end +end diff --git a/sig/kangaru/controller.rbs b/sig/kangaru/controller.rbs index 554a69e..8be0671 100644 --- a/sig/kangaru/controller.rbs +++ b/sig/kangaru/controller.rbs @@ -6,5 +6,9 @@ module Kangaru def initialize: (Command) -> void def execute: -> void + + private + + def view_path: -> Pathname end end diff --git a/sig/kangaru/patches/source.rbs b/sig/kangaru/patches/source.rbs new file mode 100644 index 0000000..db4b371 --- /dev/null +++ b/sig/kangaru/patches/source.rbs @@ -0,0 +1,9 @@ +module Kangaru + module Patches + module Source : Module + class ::Module + def source: -> String + end + end + end +end diff --git a/sig/kangaru/renderer.rbs b/sig/kangaru/renderer.rbs index ebab000..cb23c21 100644 --- a/sig/kangaru/renderer.rbs +++ b/sig/kangaru/renderer.rbs @@ -1,13 +1,9 @@ module Kangaru class Renderer - attr_reader command: Command + attr_reader path: Pathname - def initialize: (Command) -> void + def initialize: (Pathname) -> void def render: (Binding) -> void - - private - - def view_path: -> Pathname end end diff --git a/spec/kangaru/components/component_spec.rb b/spec/kangaru/components/component_spec.rb new file mode 100644 index 0000000..9ba03b5 --- /dev/null +++ b/spec/kangaru/components/component_spec.rb @@ -0,0 +1,31 @@ +RSpec.describe Kangaru::Component do + subject(:component) { described_class.new } + + let(:renderer) { instance_spy(Kangaru::Renderer) } + + before do + allow(Kangaru::Renderer).to receive(:new).and_return(renderer) + end + + describe "#render" do + subject(:render) { component.render } + + let(:component_path) do + Object.const_source_location(described_class.name).first + end + + let(:view_path) do + Pathname.new(component_path.gsub(/\.rb$/, ".erb")) + end + + it "infers the correct view file path" do + render + expect(Kangaru::Renderer).to have_received(:new).with(view_path).once + end + + it "renders the view file" do + render + expect(renderer).to have_received(:render).once + end + end +end diff --git a/spec/kangaru/controller_spec.rb b/spec/kangaru/controller_spec.rb index 08d2221..de67839 100644 --- a/spec/kangaru/controller_spec.rb +++ b/spec/kangaru/controller_spec.rb @@ -3,29 +3,29 @@ RSpec.describe Kangaru::Controller do subject(:controller) { described_class.new(command) } - let(:command) { instance_double(Kangaru::Command, action:) } + let(:command) do + instance_double(Kangaru::Command, controller: controller_name, action:) + end - let(:action) { :some_action } + let(:controller_name) { :some_controller } - let(:renderer) { instance_spy(Kangaru::Renderer) } + let(:action) { :some_action } - before do - allow(Kangaru::Renderer).to receive(:new).and_return(renderer) + let(:application) do + instance_spy(Kangaru::Application, namespace:, view_path:) end - describe "#initialize" do - it "sets the command" do - expect(controller.command).to eq(command) - end + let(:namespace) { Namespace } - it "instantiates a renderer" do - controller - expect(Kangaru::Renderer).to have_received(:new).with(command).once - end + let(:view_path) { instance_spy(Pathname) } - it "sets the renderer" do - expect(controller.renderer).to eq(renderer) - end + let(:renderer) { instance_spy(Kangaru::Renderer) } + + before do + stub_const "Namespace", Module.new + + allow(Kangaru).to receive(:application).and_return(application) + allow(Kangaru::Renderer).to receive(:new).and_return(renderer) end describe "#execute" do @@ -46,6 +46,11 @@ expect(controller).to have_received(action).once end + it "instantiates a renderer" do + execute + expect(Kangaru::Renderer).to have_received(:new).with(view_path).once + end + it "renders the command output" do execute expect(renderer).to have_received(:render).with(a_kind_of(Binding)).once @@ -57,16 +62,6 @@ let(:foobar) { Module.new } - let(:application) do - instance_spy(Kangaru::Application, namespace: Namespace) - end - - before do - stub_const "Namespace", Module.new - - allow(Kangaru).to receive(:application).and_return(application) - end - context "when specified const is not defined inside Controller" do context "and const not defined in application namespace" do it "raises an error" do diff --git a/spec/kangaru/patches/source_spec.rb b/spec/kangaru/patches/source_spec.rb new file mode 100644 index 0000000..7ef8ba7 --- /dev/null +++ b/spec/kangaru/patches/source_spec.rb @@ -0,0 +1,35 @@ +RSpec.describe Kangaru::Patches::Source do + using described_class + + describe ".source" do + subject(:source) { target.source } + + describe Module do + module SomeModule; end # rubocop:disable Lint,RSpec + + subject(:target) { SomeModule } + + it "returns a string" do + expect(source).to be_a(String) + end + + it "returns the expected source path" do + expect(source).to eq(__FILE__) + end + end + + describe Class do + class SomeClass; end # rubocop:disable Lint,RSpec + + subject(:target) { SomeClass } + + it "returns a string" do + expect(source).to be_a(String) + end + + it "returns the expected source path" do + expect(source).to eq(__FILE__) + end + end + end +end diff --git a/spec/kangaru/renderer_spec.rb b/spec/kangaru/renderer_spec.rb index 916a915..b013883 100644 --- a/spec/kangaru/renderer_spec.rb +++ b/spec/kangaru/renderer_spec.rb @@ -1,28 +1,21 @@ RSpec.describe Kangaru::Renderer do - subject(:renderer) { described_class.new(command) } + subject(:renderer) { described_class.new(path) } - let(:command) { instance_spy(Kangaru::Command) } - let(:application) { instance_spy(Kangaru::Application, view_path:) } - - let(:view_path) do - instance_spy(Pathname, read: view_file_contents, exist?: view_file_exists?) + let(:path) do + instance_spy(Pathname, read: view_contents, exist?: view_exists?) end - let(:view_file_contents) do + let(:view_contents) do <<~ERB Your name is <%= @name %>, you are <%= @age %> years old. ERB end - before do - allow(Kangaru).to receive(:application).and_return(application) - end - describe "#render" do subject(:render) { renderer.render(binding) } context "when view file does not exist" do - let(:view_file_exists?) { false } + let(:view_exists?) { false } it "does not output any text" do expect { render }.not_to output.to_stdout @@ -30,9 +23,9 @@ end context "when view file exists" do - let(:view_file_exists?) { true } + let(:view_exists?) { true } - context "when instance variables do not exist in binding" do + context "and instance variables do not exist in binding" do let(:expected_output) do <<~STRING Your name is , you are years old. @@ -44,7 +37,7 @@ end end - context "when instance variables exist in binding" do + context "and instance variables exist in binding" do around do |spec| instance_variable_set(:@name, "Foo Bar") instance_variable_set(:@age, 30)