commit 45fee16221c77b8b265d295f70f0bde0aa115a23 Author: Tim Riley Date: Sun Mar 27 21:37:00 2016 +1100 Extract dry-view from rodakase diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e2698b --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +*.gem +*.rbc +/.config +/coverage/ +/InstalledFiles +/pkg/ +/spec/reports/ +/test/tmp/ +/test/version_tmp/ +/tmp/ + +## Documentation cache and generated files: +/.yardoc/ +/_yardoc/ +/doc/ +/rdoc/ + +## Environment normalisation: +/.bundle/ +/lib/bundler/man/ + +# For a library or gem, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +Gemfile.lock +.ruby-version +.rvmrc diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..83e16f8 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..f48d5c6 --- /dev/null +++ b/Gemfile @@ -0,0 +1,20 @@ +source 'https://rubygems.org' + +gemspec + +group :test do + gem 'byebug', platform: :mri + gem 'rack-test' + gem 'slim' + + gem 'codeclimate-test-reporter', platform: :rbx +end + +group :tools do + gem 'pry' +end + +group :benchmarks do + gem 'benchmark-ips' + gem 'actionview' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..566a567 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,123 @@ +PATH + remote: . + specs: + dry-view (0.1.0) + dry-configurable (~> 0.1) + dry-equalizer (~> 0.2) + inflecto (~> 0) + tilt (~> 2.0) + +GEM + remote: https://rubygems.org/ + specs: + actionview (4.2.6) + activesupport (= 4.2.6) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activesupport (4.2.6) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + addressable (2.4.0) + benchmark-ips (2.5.0) + builder (3.2.2) + byebug (8.2.2) + capybara (2.6.2) + addressable + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + codeclimate-test-reporter (0.5.0) + simplecov (>= 0.7.1, < 1.0.0) + coderay (1.1.1) + concurrent-ruby (1.0.1) + diff-lcs (1.2.5) + docile (1.1.5) + dry-configurable (0.1.4) + concurrent-ruby (~> 1.0) + dry-equalizer (0.2.0) + erubis (2.7.0) + i18n (0.7.0) + inflecto (0.0.2) + json (1.8.3) + loofah (2.0.3) + nokogiri (>= 1.5.9) + method_source (0.8.2) + mime-types (3.0) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0221) + mini_portile2 (2.0.0) + minitest (5.8.4) + nokogiri (1.6.7.2) + mini_portile2 (~> 2.0.0.rc2) + pry (0.10.3) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + rack (1.6.4) + rack-test (0.6.3) + rack (>= 1.0) + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.7) + activesupport (>= 4.2.0.beta, < 5.0) + nokogiri (~> 1.6.0) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + rake (10.5.0) + rspec (3.4.0) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) + rspec-core (3.4.4) + rspec-support (~> 3.4.0) + rspec-expectations (3.4.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.4.0) + rspec-mocks (3.4.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.4.0) + rspec-support (3.4.1) + simplecov (0.11.2) + docile (~> 1.1.0) + json (~> 1.8) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.0) + slim (3.0.6) + temple (~> 0.7.3) + tilt (>= 1.3.3, < 2.1) + slop (3.6.0) + temple (0.7.6) + thread_safe (0.3.5) + tilt (2.0.2) + tzinfo (1.2.2) + thread_safe (~> 0.1) + xpath (2.0.0) + nokogiri (~> 1.3) + +PLATFORMS + ruby + +DEPENDENCIES + actionview + benchmark-ips + bundler (~> 1.7) + byebug + capybara (~> 2.5) + codeclimate-test-reporter + dry-view! + pry + rack-test + rake (~> 10.0) + rspec (~> 3.1) + slim + +BUNDLED WITH + 1.11.2 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..27afa20 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,10 @@ +The MIT License (MIT) + +Copyright (c) 2015 Piotr Solnica + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a21b5e --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# dry-view + +Data-oriented view rendering system. diff --git a/dry-view.gemspec b/dry-view.gemspec new file mode 100644 index 0000000..5fb254d --- /dev/null +++ b/dry-view.gemspec @@ -0,0 +1,30 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'dry/view/version' + +Gem::Specification.new do |spec| + spec.name = "dry-view" + spec.version = Dry::View::VERSION + spec.authors = ["Piotr Solnica"] + spec.email = ["piotr.solnica@gmail.com"] + spec.summary = "Lightweight web application stack on top of Roda" + spec.description = spec.summary + spec.homepage = "https://github.com/dry-rb/dry-view" + spec.license = "MIT" + + spec.files = `git ls-files -z`.split("\x0") + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.require_paths = ["lib"] + + spec.add_runtime_dependency "inflecto", "~> 0" + spec.add_runtime_dependency "tilt", "~> 2.0" + spec.add_runtime_dependency "dry-configurable", "~> 0.1" + spec.add_runtime_dependency "dry-equalizer", "~> 0.2" + + spec.add_development_dependency "bundler", "~> 1.7" + spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "rspec", "~> 3.1" + spec.add_development_dependency "capybara", "~> 2.5" +end diff --git a/lib/dry-view.rb b/lib/dry-view.rb new file mode 100644 index 0000000..43a7de7 --- /dev/null +++ b/lib/dry-view.rb @@ -0,0 +1 @@ +require 'dry/view' diff --git a/lib/dry/view.rb b/lib/dry/view.rb new file mode 100644 index 0000000..acfe03d --- /dev/null +++ b/lib/dry/view.rb @@ -0,0 +1,2 @@ +require 'dry/view/renderer' +require 'dry/view/layout' diff --git a/lib/dry/view/layout.rb b/lib/dry/view/layout.rb new file mode 100644 index 0000000..550f61d --- /dev/null +++ b/lib/dry/view/layout.rb @@ -0,0 +1,122 @@ +require 'dry-configurable' +require 'dry-equalizer' +require 'inflecto' + +require 'dry/view/part' +require 'dry/view/value_part' +require 'dry/view/null_part' +require 'dry/view/renderer' + +module Dry + module View + class Layout + include Dry::Equalizer(:config) + + Scope = Struct.new(:page) + + DEFAULT_DIR = 'layouts'.freeze + + extend Dry::Configurable + + setting :root + setting :name + setting :template + setting :formats, { html: :erb } + setting :scope + + attr_reader :config, :scope, :layout_dir, :layout_path, :template_path, + :default_format + + def self.renderer(format = default_format) + unless config.formats.key?(format.to_sym) + raise ArgumentError, "format +#{format}+ is not configured" + end + + renderers[format] + end + + def self.renderers + @renderers ||= Hash.new do |h, key| + h[key.to_sym] = Renderer.new( + config.root, format: key, engine: config.formats[key.to_sym] + ) + end + end + + def self.default_format + config.formats.keys.first + end + + def initialize + @config = self.class.config + @default_format = self.class.default_format + @layout_dir = DEFAULT_DIR + @layout_path = "#{layout_dir}/#{config.name}" + @template_path = config.template + @scope = config.scope + end + + def call(options = {}) + renderer = self.class.renderer(options.fetch(:format, default_format)) + + renderer.(layout_path, layout_scope(options, renderer)) do + renderer.(template_path, template_scope(options, renderer)) + end + end + + def locals(options) + options.fetch(:locals, {}) + end + + def parts(locals, renderer) + return empty_part(template_path, renderer) unless locals.any? + + part_hash = locals.each_with_object({}) do |(key, value), result| + part = + case value + when Array + el_key = Inflecto.singularize(key).to_sym + + template_part( + key, renderer, + value.map { |element| template_part(el_key, renderer, element) } + ) + else + template_part(key, renderer, value) + end + + result[key] = part + end + + part(template_path, renderer, part_hash) + end + + private + + def layout_scope(options, renderer) + Scope.new(layout_part(:page, renderer, options.fetch(:scope, scope))) + end + + def template_scope(options, renderer) + parts(locals(options), renderer) + end + + def layout_part(name, renderer, value) + part(layout_dir, renderer, { name => value }) + end + + def template_part(name, renderer, value) + part(template_path, renderer, { name => value }) + end + + def part(dir, renderer, value = {}) + part_class = value.values[0] ? ValuePart : NullPart + part_class.new(renderer.chdir(dir), value) + end + + def empty_part(dir, renderer) + Part.new(renderer.chdir(dir)) + end + end + end +end diff --git a/lib/dry/view/null_part.rb b/lib/dry/view/null_part.rb new file mode 100644 index 0000000..4c09e71 --- /dev/null +++ b/lib/dry/view/null_part.rb @@ -0,0 +1,30 @@ +require 'dry-equalizer' +require 'dry/view/value_part' + +module Dry + module View + class NullPart < ValuePart + def [](key) + end + + def each(&block) + end + + def respond_to_missing?(*) + true + end + + private + + def method_missing(meth, *args, &block) + template_path = template?("#{meth}_missing") + + if template_path + render(template_path) + else + nil + end + end + end + end +end diff --git a/lib/dry/view/part.rb b/lib/dry/view/part.rb new file mode 100644 index 0000000..2f51cad --- /dev/null +++ b/lib/dry/view/part.rb @@ -0,0 +1,39 @@ +require 'dry-equalizer' + +module Dry + module View + class Part + include Dry::Equalizer(:renderer) + + attr_reader :renderer + + def initialize(renderer) + @renderer = renderer + end + + def render(path, &block) + renderer.render(path, self, &block) + end + + def template?(name) + renderer.lookup("_#{name}") + end + + def respond_to_missing?(meth, include_private = false) + super || template?(meth) + end + + private + + def method_missing(meth, *args, &block) + template_path = template?(meth) + + if template_path + render(template_path, &block) + else + super + end + end + end + end +end diff --git a/lib/dry/view/renderer.rb b/lib/dry/view/renderer.rb new file mode 100644 index 0000000..02bffdb --- /dev/null +++ b/lib/dry/view/renderer.rb @@ -0,0 +1,68 @@ +require 'tilt' +require 'dry-equalizer' + +module Dry + module View + class Renderer + include Dry::Equalizer(:dir, :root, :engine) + + TemplateNotFoundError = Class.new(StandardError) + + attr_reader :dir, :root, :format, :engine, :tilts + + def self.tilts + @__engines__ ||= {} + end + + def initialize(dir, options = {}) + @dir = dir + @root = options.fetch(:root, dir) + @format = options[:format] + @engine = options[:engine] + @tilts = self.class.tilts + end + + def call(template, scope, &block) + path = lookup(template) + + if path + render(path, scope, &block) + else + raise TemplateNotFoundError, "Template #{template} could not be looked up within #{root}" + end + end + + def render(path, scope, &block) + tilt(path).render(scope, &block) + end + + def tilt(path) + tilts.fetch(path) { tilts[path] = Tilt[engine].new(path) } + end + + def lookup(name) + template?(name) || template?("shared/#{name}") || !root? && chdir('..').lookup(name) + end + + def root? + dir == root + end + + def template?(name) + template_path = path(name) + + if File.exist?(template_path) + template_path + end + end + + def path(name) + dir.join("#{name}.#{format}.#{engine}") + end + + def chdir(dirname) + self.class.new(dir.join(dirname), engine: engine, format: format, root: root) + end + end + end +end diff --git a/lib/dry/view/value_part.rb b/lib/dry/view/value_part.rb new file mode 100644 index 0000000..8b9c480 --- /dev/null +++ b/lib/dry/view/value_part.rb @@ -0,0 +1,50 @@ +require 'dry-equalizer' +require 'dry/view/part' + +module Dry + module View + class ValuePart < Part + include Dry::Equalizer(:renderer, :_data, :_value) + + attr_reader :_data, :_value + + def initialize(renderer, data) + super(renderer) + @_data = data + @_value = data.values[0] + end + + def to_s + _value.to_s + end + + def [](key) + _value[key] + end + + def each(&block) + _value.each(&block) + end + + def respond_to_missing?(meth, include_private = false) + _data.key?(meth) || super + end + + private + + def method_missing(meth, *args, &block) + template_path = template?(meth) + + if template_path + render(template_path, &block) + elsif _data.key?(meth) + _data[meth] + elsif _value.respond_to?(meth) + _value.public_send(meth, *args, &block) + else + super + end + end + end + end +end diff --git a/lib/dry/view/version.rb b/lib/dry/view/version.rb new file mode 100644 index 0000000..c745860 --- /dev/null +++ b/lib/dry/view/version.rb @@ -0,0 +1,5 @@ +module Dry + module View + VERSION = '0.1.0'.freeze + end +end diff --git a/spec/fixtures/templates/hello.html.slim b/spec/fixtures/templates/hello.html.slim new file mode 100644 index 0000000..b315cd0 --- /dev/null +++ b/spec/fixtures/templates/hello.html.slim @@ -0,0 +1 @@ +h1 Hello diff --git a/spec/fixtures/templates/layouts/app.html.slim b/spec/fixtures/templates/layouts/app.html.slim new file mode 100644 index 0000000..97ccd6d --- /dev/null +++ b/spec/fixtures/templates/layouts/app.html.slim @@ -0,0 +1,6 @@ +doctype html +html + head + title == page.title + body + == yield diff --git a/spec/fixtures/templates/layouts/app.txt.erb b/spec/fixtures/templates/layouts/app.txt.erb new file mode 100644 index 0000000..6d0d030 --- /dev/null +++ b/spec/fixtures/templates/layouts/app.txt.erb @@ -0,0 +1,3 @@ +# <%= page.title %> + +<%= yield %> diff --git a/spec/fixtures/templates/shared/_index_table.html.slim b/spec/fixtures/templates/shared/_index_table.html.slim new file mode 100644 index 0000000..ede2d15 --- /dev/null +++ b/spec/fixtures/templates/shared/_index_table.html.slim @@ -0,0 +1,2 @@ +table + == yield diff --git a/spec/fixtures/templates/shared/_shared_hello.html.slim b/spec/fixtures/templates/shared/_shared_hello.html.slim new file mode 100644 index 0000000..b315cd0 --- /dev/null +++ b/spec/fixtures/templates/shared/_shared_hello.html.slim @@ -0,0 +1 @@ +h1 Hello diff --git a/spec/fixtures/templates/tasks.html.slim b/spec/fixtures/templates/tasks.html.slim new file mode 100644 index 0000000..e270cc6 --- /dev/null +++ b/spec/fixtures/templates/tasks.html.slim @@ -0,0 +1,3 @@ +ol + - tasks.each do |task| + li == task[:title] diff --git a/spec/fixtures/templates/user.html.slim b/spec/fixtures/templates/user.html.slim new file mode 100644 index 0000000..486e986 --- /dev/null +++ b/spec/fixtures/templates/user.html.slim @@ -0,0 +1,2 @@ +h1 = header[:title] +p = user[:name] diff --git a/spec/fixtures/templates/users.html.slim b/spec/fixtures/templates/users.html.slim new file mode 100644 index 0000000..b0e2793 --- /dev/null +++ b/spec/fixtures/templates/users.html.slim @@ -0,0 +1,5 @@ +h2 = subtitle + +.users + == users.index_table do + == users.tbody diff --git a/spec/fixtures/templates/users.txt.erb b/spec/fixtures/templates/users.txt.erb new file mode 100644 index 0000000..301f267 --- /dev/null +++ b/spec/fixtures/templates/users.txt.erb @@ -0,0 +1,5 @@ +## <%= subtitle %> + +<% users.each do |user| %> +* <%= user[:name] %> (<%= user[:email] %>) +<% end %> diff --git a/spec/fixtures/templates/users/_row.html.slim b/spec/fixtures/templates/users/_row.html.slim new file mode 100644 index 0000000..4adde7b --- /dev/null +++ b/spec/fixtures/templates/users/_row.html.slim @@ -0,0 +1,2 @@ +tr + == yield diff --git a/spec/fixtures/templates/users/_tbody.html.slim b/spec/fixtures/templates/users/_tbody.html.slim new file mode 100644 index 0000000..65c6690 --- /dev/null +++ b/spec/fixtures/templates/users/_tbody.html.slim @@ -0,0 +1,5 @@ +tbody + - users.each do |user| + == user.row do + td = user[:name] + td = user[:email] diff --git a/spec/integration/view_spec.rb b/spec/integration/view_spec.rb new file mode 100644 index 0000000..8499939 --- /dev/null +++ b/spec/integration/view_spec.rb @@ -0,0 +1,72 @@ +RSpec.describe 'dry-view' do + let(:view_class) do + klass = Class.new(Dry::View::Layout) + + klass.configure do |config| + config.root = SPEC_ROOT.join('fixtures/templates') + config.name = 'app' + config.template = 'users' + config.formats = {html: :slim, txt: :erb} + end + + klass + end + + let(:scope) do + Struct.new(:title).new('dry-view rocks!') + end + + it 'renders within a layout using provided scope' do + view = view_class.new + + users = [ + { name: 'Jane', email: 'jane@doe.org' }, + { name: 'Joe', email: 'joe@doe.org' } + ] + + expect(view.(scope: scope, locals: { subtitle: "Users List", users: users })).to eql( + 'dry-view rocks!

Users List

Janejane@doe.org
Joejoe@doe.org
' + ) + end + + it 'renders a view with an alternative format and engine' do + view = view_class.new + + users = [ + { name: 'Jane', email: 'jane@doe.org' }, + { name: 'Joe', email: 'joe@doe.org' } + ] + + expect(view.(scope: scope, locals: { subtitle: 'Users List', users: users }, format: 'txt').strip).to eql( + "# dry-view rocks!\n\n## Users List\n\n* Jane (jane@doe.org)\n* Joe (joe@doe.org)" + ) + end + + describe 'inheritance' do + let(:parent_view) do + klass = Class.new(Dry::View::Layout) + + klass.setting :root, SPEC_ROOT.join('fixtures/templates') + klass.setting :name, 'app' + klass.setting :formats, {html: :slim} + + klass + end + + let(:child_view) do + Class.new(parent_view) do + configure do |config| + config.template = 'tasks' + end + end + end + + it 'renders within a parent class layout using provided scope' do + view = child_view.new + + expect(view.(scope: scope, locals: { tasks: [{ title: 'one' }, { title: 'two' }] })).to eql( + 'dry-view rocks!
  1. one
  2. two
' + ) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..7808180 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,20 @@ +if RUBY_ENGINE == "rbx" + require "codeclimate-test-reporter" + CodeClimate::TestReporter.start +end + +begin + require 'byebug' +rescue LoadError; end + +SPEC_ROOT = Pathname(__FILE__).dirname + +require 'dry-view' +require 'slim' + +RSpec.configure do |config| + config.disable_monkey_patching! + + config.order = :random + Kernel.srand config.seed +end diff --git a/spec/unit/layout_spec.rb b/spec/unit/layout_spec.rb new file mode 100644 index 0000000..ac9bbb9 --- /dev/null +++ b/spec/unit/layout_spec.rb @@ -0,0 +1,55 @@ +RSpec.describe Dry::View::Layout do + subject(:layout) { layout_class.new } + + let(:layout_class) do + klass = Class.new(Dry::View::Layout) + + klass.configure do |config| + config.root = SPEC_ROOT.join('fixtures/templates') + config.name = 'app' + config.template = 'user' + config.formats = {html: :slim} + end + + klass + end + + let(:page) do + double(:page, title: 'Test') + end + + let(:options) do + { scope: page, locals: { user: { name: 'Jane' }, header: { title: 'User' } } } + end + + let(:renderer) do + layout.class.renderers[:html] + end + + describe '#call' do + it 'renders template within the layout' do + expect(layout.(options)).to eql( + 'Test

User

Jane

' + ) + end + end + + describe '#parts' do + it 'returns view parts' do + part = layout.parts({ user: { id: 1, name: 'Jane' } }, renderer) + + expect(part[:id]).to be(1) + expect(part[:name]).to eql('Jane') + end + + it 'builds null parts for nil values' do + part = layout.parts({ user: nil }, renderer) + + expect(part[:id]).to be_nil + end + + it 'returns empty part when no locals are passed' do + expect(layout.parts({}, renderer)).to be_instance_of(Dry::View::Part) + end + end +end diff --git a/spec/unit/null_part_spec.rb b/spec/unit/null_part_spec.rb new file mode 100644 index 0000000..3e40f84 --- /dev/null +++ b/spec/unit/null_part_spec.rb @@ -0,0 +1,39 @@ +require 'dry/view/null_part' + +RSpec.describe Dry::View::NullPart do + subject(:part) do + Dry::View::NullPart.new(renderer, data) + end + + let(:name) { :user } + let(:data) { { user: nil } } + + let(:renderer) { double(:renderer) } + + describe '#[]' do + it 'returns nil for any data value names' do + expect(part[:email]).to eql(nil) + end + end + + describe '#method_missing' do + it 'renders a template with the _missing suffix' do + expect(renderer).to receive(:lookup).with('_row_missing').and_return('_row_missing.slim') + expect(renderer).to receive(:render).with('_row_missing.slim', part) + + part.row + end + + it 'renders a _missing template within another when block is passed' do + block = proc { part.fields } + + expect(renderer).to receive(:lookup).with('_form_missing').and_return('form_missing.slim') + expect(renderer).to receive(:lookup).with('_fields_missing').and_return('fields_missing.slim') + + expect(renderer).to receive(:render).with('form_missing.slim', part, &block) + expect(renderer).to receive(:render).with('fields_missing.slim', part) + + part.form(block) + end + end +end diff --git a/spec/unit/renderer_spec.rb b/spec/unit/renderer_spec.rb new file mode 100644 index 0000000..89abd58 --- /dev/null +++ b/spec/unit/renderer_spec.rb @@ -0,0 +1,29 @@ +require 'dry/view/renderer' + +RSpec.describe Dry::View::Renderer do + subject(:renderer) do + Dry::View::Renderer.new(SPEC_ROOT.join('fixtures/templates'), format: 'html', engine: :slim) + end + + let(:scope) { double(:scope) } + + describe '#call' do + it 'renders template' do + expect(renderer.('hello', scope)).to eql('

Hello

') + end + + it 'looks up shared template in current dir' do + expect(renderer.('_shared_hello', scope)).to eql('

Hello

') + end + + it 'looks up shared template in upper dir' do + expect(renderer.chdir('greetings').('_shared_hello', scope)).to eql('

Hello

') + end + + it 'raises error when template was not found' do + expect { + renderer.('not_found', scope) + }.to raise_error(Dry::View::Renderer::TemplateNotFoundError, /not_found/) + end + end +end diff --git a/spec/unit/value_part_spec.rb b/spec/unit/value_part_spec.rb new file mode 100644 index 0000000..b63f2ca --- /dev/null +++ b/spec/unit/value_part_spec.rb @@ -0,0 +1,55 @@ +require 'dry/view/part' + +RSpec.describe Dry::View::ValuePart do + subject(:part) do + Dry::View::ValuePart.new(renderer, data) + end + + let(:name) { :user } + let(:data) { { user: { email: 'jane@doe.org' } } } + + let(:renderer) { double(:renderer) } + + describe '#[]' do + it 'gives access to data values' do + expect(part[:email]).to eql('jane@doe.org') + end + end + + describe '#render' do + it 'renders given template' do + expect(renderer).to receive(:render).with('row.slim', part) + + part.render('row.slim') + end + end + + describe '#template?' do + it 'asks renderer if there is a valid template for a given identifier' do + expect(renderer).to receive(:lookup).with('_row').and_return('row.slim') + + expect(part.template?('row')).to eql('row.slim') + end + end + + describe '#method_missing' do + it 'renders template' do + expect(renderer).to receive(:lookup).with('_row').and_return('_row.slim') + expect(renderer).to receive(:render).with('_row.slim', part) + + part.row + end + + it 'renders template within another when block is passed' do + block = proc { part.fields } + + expect(renderer).to receive(:lookup).with('_form').and_return('form.slim') + expect(renderer).to receive(:lookup).with('_fields').and_return('fields.slim') + + expect(renderer).to receive(:render).with('form.slim', part, &block) + expect(renderer).to receive(:render).with('fields.slim', part) + + part.form(block) + end + end +end