Extract dry-view from rodakase

This commit is contained in:
Tim Riley 2016-03-27 21:37:00 +11:00
commit 45fee16221
32 changed files with 836 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@ -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

2
.rspec Normal file
View File

@ -0,0 +1,2 @@
--color
--require spec_helper

20
Gemfile Normal file
View File

@ -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

123
Gemfile.lock Normal file
View File

@ -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

10
LICENSE.md Normal file
View File

@ -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.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# dry-view
Data-oriented view rendering system.

30
dry-view.gemspec Normal file
View File

@ -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

1
lib/dry-view.rb Normal file
View File

@ -0,0 +1 @@
require 'dry/view'

2
lib/dry/view.rb Normal file
View File

@ -0,0 +1,2 @@
require 'dry/view/renderer'
require 'dry/view/layout'

122
lib/dry/view/layout.rb Normal file
View File

@ -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

30
lib/dry/view/null_part.rb Normal file
View File

@ -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

39
lib/dry/view/part.rb Normal file
View File

@ -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

68
lib/dry/view/renderer.rb Normal file
View File

@ -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

View File

@ -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

5
lib/dry/view/version.rb Normal file
View File

@ -0,0 +1,5 @@
module Dry
module View
VERSION = '0.1.0'.freeze
end
end

View File

@ -0,0 +1 @@
h1 Hello

View File

@ -0,0 +1,6 @@
doctype html
html
head
title == page.title
body
== yield

View File

@ -0,0 +1,3 @@
# <%= page.title %>
<%= yield %>

View File

@ -0,0 +1,2 @@
table
== yield

View File

@ -0,0 +1 @@
h1 Hello

View File

@ -0,0 +1,3 @@
ol
- tasks.each do |task|
li == task[:title]

View File

@ -0,0 +1,2 @@
h1 = header[:title]
p = user[:name]

View File

@ -0,0 +1,5 @@
h2 = subtitle
.users
== users.index_table do
== users.tbody

5
spec/fixtures/templates/users.txt.erb vendored Normal file
View File

@ -0,0 +1,5 @@
## <%= subtitle %>
<% users.each do |user| %>
* <%= user[:name] %> (<%= user[:email] %>)
<% end %>

View File

@ -0,0 +1,2 @@
tr
== yield

View File

@ -0,0 +1,5 @@
tbody
- users.each do |user|
== user.row do
td = user[:name]
td = user[:email]

View File

@ -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(
'<!DOCTYPE html><html><head><title>dry-view rocks!</title></head><body><h2>Users List</h2><div class="users"><table><tbody><tr><td>Jane</td><td>jane@doe.org</td></tr><tr><td>Joe</td><td>joe@doe.org</td></tr></tbody></table></div></body></html>'
)
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(
'<!DOCTYPE html><html><head><title>dry-view rocks!</title></head><body><ol><li>one</li><li>two</li></ol></body></html>'
)
end
end
end

20
spec/spec_helper.rb Normal file
View File

@ -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

55
spec/unit/layout_spec.rb Normal file
View File

@ -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(
'<!DOCTYPE html><html><head><title>Test</title></head><body><h1>User</h1><p>Jane</p></body></html>'
)
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

View File

@ -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

View File

@ -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('<h1>Hello</h1>')
end
it 'looks up shared template in current dir' do
expect(renderer.('_shared_hello', scope)).to eql('<h1>Hello</h1>')
end
it 'looks up shared template in upper dir' do
expect(renderer.chdir('greetings').('_shared_hello', scope)).to eql('<h1>Hello</h1>')
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

View File

@ -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