dry-view 0.5.3 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +18 -0
- data/.travis.yml +15 -10
- data/.yardopts +5 -0
- data/CHANGELOG.md +60 -1
- data/Gemfile +15 -5
- data/README.md +38 -13
- data/bin/setup +5 -0
- data/bin/setup_helpers.rb +27 -0
- data/dry-view.gemspec +8 -9
- data/lib/dry-view.rb +3 -1
- data/lib/dry/view.rb +503 -2
- data/lib/dry/view/context.rb +80 -0
- data/lib/dry/view/decorated_attributes.rb +81 -0
- data/lib/dry/view/exposure.rb +15 -2
- data/lib/dry/view/exposures.rb +15 -5
- data/lib/dry/view/part.rb +154 -61
- data/lib/dry/view/part_builder.rb +136 -0
- data/lib/dry/view/path.rb +22 -5
- data/lib/dry/view/render_environment.rb +62 -0
- data/lib/dry/view/render_environment_missing.rb +44 -0
- data/lib/dry/view/rendered.rb +55 -0
- data/lib/dry/view/renderer.rb +22 -19
- data/lib/dry/view/scope.rb +146 -14
- data/lib/dry/view/scope_builder.rb +98 -0
- data/lib/dry/view/tilt.rb +78 -0
- data/lib/dry/view/tilt/erb.rb +26 -0
- data/lib/dry/view/tilt/erbse.rb +21 -0
- data/lib/dry/view/tilt/haml.rb +26 -0
- data/lib/dry/view/version.rb +5 -2
- metadata +50 -88
- data/benchmarks/templates/button.html.erb +0 -1
- data/benchmarks/view.rb +0 -24
- data/lib/dry/view/controller.rb +0 -159
- data/lib/dry/view/decorator.rb +0 -45
- data/lib/dry/view/missing_renderer.rb +0 -15
- data/spec/fixtures/templates/_hello.html.slim +0 -1
- data/spec/fixtures/templates/controller_renderer_options.html.erb +0 -3
- data/spec/fixtures/templates/decorated_parts.html.slim +0 -4
- data/spec/fixtures/templates/edit.html.slim +0 -11
- data/spec/fixtures/templates/empty.html.slim +0 -1
- data/spec/fixtures/templates/greeting.html.slim +0 -2
- data/spec/fixtures/templates/hello.html.slim +0 -1
- data/spec/fixtures/templates/layouts/app.html.slim +0 -6
- data/spec/fixtures/templates/layouts/app.txt.erb +0 -3
- data/spec/fixtures/templates/parts_with_args.html.slim +0 -3
- data/spec/fixtures/templates/parts_with_args/_box.html.slim +0 -3
- data/spec/fixtures/templates/shared/_index_table.html.slim +0 -2
- data/spec/fixtures/templates/shared/_shared_hello.html.slim +0 -1
- data/spec/fixtures/templates/tasks.html.slim +0 -3
- data/spec/fixtures/templates/user.html.slim +0 -2
- data/spec/fixtures/templates/users.html.slim +0 -5
- data/spec/fixtures/templates/users.txt.erb +0 -3
- data/spec/fixtures/templates/users/_row.html.slim +0 -2
- data/spec/fixtures/templates/users/_tbody.html.slim +0 -5
- data/spec/fixtures/templates/users_with_count.html.slim +0 -5
- data/spec/fixtures/templates/users_with_count_inherit.html.slim +0 -6
- data/spec/fixtures/templates_override/_hello.html.slim +0 -1
- data/spec/fixtures/templates_override/users.html.slim +0 -5
- data/spec/integration/decorator_spec.rb +0 -80
- data/spec/integration/exposures_spec.rb +0 -392
- data/spec/integration/part/decorated_attributes_spec.rb +0 -193
- data/spec/integration/view_spec.rb +0 -133
- data/spec/spec_helper.rb +0 -46
- data/spec/unit/controller_spec.rb +0 -83
- data/spec/unit/decorator_spec.rb +0 -61
- data/spec/unit/exposure_spec.rb +0 -227
- data/spec/unit/exposures_spec.rb +0 -103
- data/spec/unit/part_spec.rb +0 -104
- data/spec/unit/renderer_spec.rb +0 -57
- data/spec/unit/scope_spec.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d0cd65b1329feef3497caadc8f41521850546d9c60aa673d035215bcaaee922
|
4
|
+
data.tar.gz: 46e2e12a0d192dd2e7443c334bc62fdd86ff76f1aa308c1b1ac14a921e67565b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f235921f2631c60446690342c388cc624665f21a9a1e40e979efa285bfa8fd73c3a0f4bb453931e1fc25cdc5a6201d58f42a7a4a6275a96c2f5d641f0ec23961
|
7
|
+
data.tar.gz: 2cc2906563bcd30b21c1034d0ffb97c395e77898b8ab7d117cdf11aa52462c7a874e05ddb66a59c600c29a31c65f7e3808fae4f447df817cdedbc255e7d0c179
|
data/.codeclimate.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
version: "2"
|
2
|
+
plugins:
|
3
|
+
rubocop:
|
4
|
+
enabled: true
|
5
|
+
checks:
|
6
|
+
Rubocop/AllCops:
|
7
|
+
TargetRubyVersion: 2.2
|
8
|
+
Rubocop/Metrics/ClassLength:
|
9
|
+
Max: 150
|
10
|
+
Rubocop/Metrics/LineLength:
|
11
|
+
Max: 100
|
12
|
+
Rubocop/Metrics/MethodLength:
|
13
|
+
Max: 20
|
14
|
+
exclude_patterns:
|
15
|
+
- "benchmarks/"
|
16
|
+
- "bin/"
|
17
|
+
- "examples/"
|
18
|
+
- "spec/"
|
data/.travis.yml
CHANGED
@@ -3,21 +3,26 @@ cache: bundler
|
|
3
3
|
bundler_args: --without tools benchmarks
|
4
4
|
before_install:
|
5
5
|
- gem update --system
|
6
|
+
- gem install bundler
|
6
7
|
script:
|
7
8
|
- bundle exec rake
|
8
|
-
|
9
|
-
|
10
|
-
-
|
9
|
+
before_script:
|
10
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
11
|
+
- chmod +x ./cc-test-reporter
|
12
|
+
- ./cc-test-reporter before-build
|
13
|
+
after_script:
|
14
|
+
- "[ -d coverage ] && ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
|
11
15
|
rvm:
|
12
|
-
- 2.
|
13
|
-
- 2.
|
14
|
-
- 2.
|
15
|
-
-
|
16
|
+
- 2.6.0
|
17
|
+
- 2.5.3
|
18
|
+
- 2.4.5
|
19
|
+
- 2.3.8
|
20
|
+
- jruby-9.2.5.0
|
16
21
|
notifications:
|
17
22
|
email: false
|
18
23
|
webhooks:
|
19
24
|
urls:
|
20
25
|
- https://webhooks.gitter.im/e/19098b4253a72c9796db
|
21
|
-
on_success: change
|
22
|
-
on_failure: always
|
23
|
-
on_start: false
|
26
|
+
on_success: change # options: [always|never|change] default: always
|
27
|
+
on_failure: always # options: [always|never|change] default: always
|
28
|
+
on_start: false # default: false
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,60 @@
|
|
1
|
+
# 0.6.0 / 2019-01-30
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
- [BREAKING] `Dry::View#call` now returns a `Dry::View::Rendered` instance, which carries both the rendered output (accessible via `#to_s` or `#to_str`) as well as all of the view's locals, wrapped in their view parts (accessible via `#locals` or individually via `#[]`) (timriley in [#72][pr72])
|
6
|
+
- [BREAKING] Added `Dry::View::PartBuilder` (renamed from `Dry::View::Decorator`), which resolves part classes from a namespace configured via View's `part_namespace` setting. A custom part builder can be specified via a View's `part_builder` setting. (timriley in [#80][pr80])
|
7
|
+
- [BREAKING] Context classes can now declare decorated attributes just like part classes, via `.decorate` class-level API. Context classes are now required to inherit from `Dry::View::Context`. `Dry::View::Context` provides a `#with` method for creating copies of itself while preserving the rendering details needed for decorated attributes to work (timriley in [#89][pr89] and [#91][pr91])
|
8
|
+
- Customizable _scope_ objects, which work like view parts, but instead of encapsulating a single value, they encapsulate a whole template or partial and all of its locals. Scopes can be created via `#scope` method in templates, parts, as well as scope classes themselves. Scope classes are resolved via a View's `scope_builder` setting, which defaults to an instance of `Dry::View::ScopeBuilder`.
|
9
|
+
- Added `inflector` setting to View, which is used by the part and scope builders to resolve classes for a given part or scope name. Defaults to `Dry::Inflector.new` (timriley in [#80][pr80] and [#90][pr90])
|
10
|
+
- Exposures can be sent to the layout template when defined with `layout: true` option (GustavoCaso in [#87][pr87])
|
11
|
+
- Exposures can be left undecorated by a part when defined with `decorate: false` option (timriley in [#88][pr88])
|
12
|
+
- Part classes have access to the current template format via a private `#_format` method (timriley in [#118][pr118])
|
13
|
+
- Added "Tilt adapter" layer, to ensure a rendering engine compatible with dry-view's features is being used. Added adapters for "haml" and "erb" templates to ensure that "hamlit-block" and "erbse" are required and used as engines (unlike their more common counterparts, both of these engines support the implicit block capturing that is a central part of dry-view rendering behaviour) (timriley in [#106][pr106])
|
14
|
+
- Added `renderer_engine_mapping` setting to View, which allows an explicit engine class to be provided for the rendering of a given type of template (e.g. `config.renderer_engine_mapping = {erb: Tilt::ErubiTemplate}`) (timriley in [#106][pr106])
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
|
18
|
+
- [BREAKING] `Dry::View::Controller` renamed to `Dry::View` (timriley in [#115][pr115])
|
19
|
+
- [BREAKING] `Dry::View` `context` setting renamed to `default_context` (GustavoCaso in [#86][pr86])
|
20
|
+
- Exposure values are wrapped in their view parts before being made available as exposure dependencies (timriley in [#80][pr80])
|
21
|
+
- Exposures can access current context object through `context:` block or method parameter (timriley in [#119][pr119])
|
22
|
+
- Improved performance due to caching various lookups (timriley and GustavoCaso in [#97][pr97])
|
23
|
+
- `Part#inspect` output simplified to include only name and value (timriley in [#98][pr98])
|
24
|
+
- Attribute decoration in `Part` now achieved via a prepended module, which means it is possible to decorate an attribute provided by an instance method directly on the part class, which wasn't possible with the previous `method_missing`-based approach (timriley in [#110][pr110])
|
25
|
+
- `Part` classes can be initialized with missing `name:` and `rendering:` values, which can be useful for unit testing Part methods that don't use any rendering facilities (timriley in [#116][pr116])
|
26
|
+
|
27
|
+
### Fixed
|
28
|
+
|
29
|
+
- Preserve renderer options when chdir-ing (timriley in [889ac7b](https://github.com/dry-rb/dry-view/commit/889ac7b))
|
30
|
+
|
31
|
+
[Compare v0.5.3...v0.6.0](https://github.com/dry-rb/dry-view/compare/v0.5.3...v0.6.0)
|
32
|
+
|
33
|
+
[pr72]: https://github.com/dry-rb/dry-view/pull/72
|
34
|
+
[pr80]: https://github.com/dry-rb/dry-view/pull/80
|
35
|
+
[pr86]: https://github.com/dry-rb/dry-view/pull/86
|
36
|
+
[pr87]: https://github.com/dry-rb/dry-view/pull/87
|
37
|
+
[pr88]: https://github.com/dry-rb/dry-view/pull/88
|
38
|
+
[pr89]: https://github.com/dry-rb/dry-view/pull/89
|
39
|
+
[pr90]: https://github.com/dry-rb/dry-view/pull/90
|
40
|
+
[pr91]: https://github.com/dry-rb/dry-view/pull/91
|
41
|
+
[pr97]: https://github.com/dry-rb/dry-view/pull/97
|
42
|
+
[pr98]: https://github.com/dry-rb/dry-view/pull/98
|
43
|
+
[pr106]: https://github.com/dry-rb/dry-view/pull/106
|
44
|
+
[pr110]: https://github.com/dry-rb/dry-view/pull/110
|
45
|
+
[pr115]: https://github.com/dry-rb/dry-view/pull/115
|
46
|
+
[pr116]: https://github.com/dry-rb/dry-view/pull/116
|
47
|
+
[pr118]: https://github.com/dry-rb/dry-view/pull/118
|
48
|
+
[pr119]: https://github.com/dry-rb/dry-view/pull/119
|
49
|
+
|
50
|
+
# 0.5.4 / 2019-01-06 [YANKED 2019-01-18]
|
51
|
+
|
52
|
+
This version was yanked due to the release accidentally containing a batch of breaking changes from master.
|
53
|
+
|
54
|
+
### Fixed
|
55
|
+
|
56
|
+
- Preserve renderer options when chdir-ing (timriley in [889ac7b](https://github.com/dry-rb/dry-view/commit/889ac7b))
|
57
|
+
|
1
58
|
# 0.5.3 / 2018-10-22
|
2
59
|
|
3
60
|
### Added
|
@@ -8,6 +65,8 @@
|
|
8
65
|
|
9
66
|
- Part objects wrap values more transparently, via added `#respond_to_missing?` (liseki in [#63][pr63])
|
10
67
|
|
68
|
+
[Compare v0.5.2...v0.5.3](https://github.com/dry-rb/dry-view/compare/v0.5.2...v0.5.3)
|
69
|
+
|
11
70
|
[pr62]: https://github.com/dry-rb/dry-view/pull/62
|
12
71
|
[pr63]: https://github.com/dry-rb/dry-view/pull/63
|
13
72
|
|
@@ -17,7 +76,7 @@
|
|
17
76
|
|
18
77
|
- Only truthy view part attributes are decorated (timriley)
|
19
78
|
|
20
|
-
[Compare v0.5.
|
79
|
+
[Compare v0.5.1...v0.5.2](https://github.com/dry-rb/dry-view/compare/v0.5.1...v0.5.2)
|
21
80
|
|
22
81
|
# 0.5.1 / 2018-02-20
|
23
82
|
|
data/Gemfile
CHANGED
@@ -2,21 +2,31 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
-
gem 'inflecto'
|
6
|
-
|
7
5
|
group :tools do
|
6
|
+
gem 'hotch'
|
8
7
|
gem 'pry-byebug', platform: :mri
|
9
8
|
end
|
10
9
|
|
11
10
|
group :test do
|
12
|
-
gem
|
13
|
-
|
11
|
+
gem "rack", ">= 2.0.6"
|
12
|
+
|
13
|
+
gem "erbse"
|
14
|
+
gem "erubi"
|
15
|
+
gem "hamlit"
|
16
|
+
gem "hamlit-block"
|
17
|
+
gem 'slim', "~> 4.0"
|
14
18
|
|
15
19
|
gem 'simplecov'
|
16
|
-
gem 'codeclimate-test-reporter'
|
17
20
|
end
|
18
21
|
|
19
22
|
group :benchmarks do
|
20
23
|
gem 'benchmark-ips'
|
21
24
|
gem 'actionview'
|
25
|
+
gem 'actionpack'
|
26
|
+
end
|
27
|
+
|
28
|
+
group :docs do
|
29
|
+
gem 'yard'
|
30
|
+
gem 'yard-junk'
|
31
|
+
gem 'redcarpet', platforms: :mri
|
22
32
|
end
|
data/README.md
CHANGED
@@ -1,21 +1,46 @@
|
|
1
|
-
|
2
|
-
[gem]: https://rubygems.org/gems/dry-view
|
3
|
-
[travis]: https://travis-ci.org/dry-rb/dry-view
|
4
|
-
[inch]: http://inch-ci.org/github/dry-rb/dry-view
|
5
|
-
|
6
|
-
# dry-view [![Join the Gitter chat](https://badges.gitter.im/Join%20Chat.svg)][gitter]
|
1
|
+
# dry-view
|
7
2
|
|
8
3
|
[![Gem Version](https://img.shields.io/gem/v/dry-view.svg)][gem]
|
9
4
|
[![Build Status](https://img.shields.io/travis/dry-rb/dry-view.svg)][travis]
|
10
|
-
[![Maintainability](https://api.codeclimate.com/v1/badges/de81a8026a2e7f64e4df/maintainability)]
|
11
|
-
[![Test Coverage](https://api.codeclimate.com/v1/badges/de81a8026a2e7f64e4df/test_coverage)]
|
5
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/de81a8026a2e7f64e4df/maintainability)][maint]
|
6
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/de81a8026a2e7f64e4df/test_coverage)][cov]
|
12
7
|
[![API Documentation Coverage](http://inch-ci.org/github/dry-rb/dry-view.svg)][inch]
|
13
8
|
|
14
|
-
|
15
|
-
|
16
|
-
controllers and templates. dry-view allows you to model your views as stateless
|
17
|
-
transformations, accepting user input and returning your rendered view.
|
9
|
+
dry-view is a complete, standalone view rendering system that gives you
|
10
|
+
everything you need to write well-factored view code.
|
18
11
|
|
19
12
|
## Links
|
20
13
|
|
21
|
-
* [Documentation]
|
14
|
+
* [Documentation][docs]
|
15
|
+
* [API documentation][api]
|
16
|
+
* [dry-rb website][website]
|
17
|
+
* [Support forum][support]
|
18
|
+
|
19
|
+
## Development
|
20
|
+
|
21
|
+
After checking out this repo, run `bin/setup` to setup the project.
|
22
|
+
|
23
|
+
Then, run `rake spec` to run the tests.
|
24
|
+
|
25
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
26
|
+
|
27
|
+
## Contributing
|
28
|
+
|
29
|
+
Bug reports and pull requests are welcome [on GitHub][repo]. For new feature
|
30
|
+
development, we recommend having a discussion [in the forum][support] before
|
31
|
+
beginning your work.
|
32
|
+
|
33
|
+
<!-- Links -->
|
34
|
+
[docs]: https://dry-rb.org/gems/dry-view
|
35
|
+
[api]: https://www.rubydoc.info/github/dry-rb/dry-view
|
36
|
+
[website]: https://dry-rb.org/
|
37
|
+
[support]: https://discourse.dry-rb.org/
|
38
|
+
[repo]: https://github.com/dry-rb/dry-view
|
39
|
+
|
40
|
+
<!-- Badge links -->
|
41
|
+
[gitter]: https://gitter.im/dry-rb/chat
|
42
|
+
[gem]: https://rubygems.org/gems/dry-view
|
43
|
+
[travis]: https://travis-ci.org/dry-rb/dry-view
|
44
|
+
[maint]: https://codeclimate.com/github/dry-rb/dry-view/maintainability
|
45
|
+
[cov]: https://codeclimate.com/github/dry-rb/dry-view/test_coverage
|
46
|
+
[inch]: http://inch-ci.org/github/dry-rb/dry-view
|
data/bin/setup
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module Setup
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def execute(cmd)
|
9
|
+
puts "Running #{cmd}"
|
10
|
+
|
11
|
+
status, out, err = nil
|
12
|
+
|
13
|
+
Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
|
14
|
+
_pid = wait_thr.pid
|
15
|
+
stdin.close
|
16
|
+
out = stdout.read
|
17
|
+
err = stderr.read
|
18
|
+
status = wait_thr.value
|
19
|
+
end
|
20
|
+
|
21
|
+
if !status.success?
|
22
|
+
puts "Failed to run #{cmd}"
|
23
|
+
puts err
|
24
|
+
exit 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/dry-view.gemspec
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# coding: utf-8
|
2
1
|
lib = File.expand_path('../lib', __FILE__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
3
|
require 'dry/view/version'
|
@@ -6,27 +5,27 @@ require 'dry/view/version'
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
6
|
spec.name = "dry-view"
|
8
7
|
spec.version = Dry::View::VERSION
|
9
|
-
spec.authors = ["
|
10
|
-
spec.email = ["
|
11
|
-
spec.summary = "
|
8
|
+
spec.authors = ["Tim Riley", "Piotr Solnica"]
|
9
|
+
spec.email = ["tim@icelab.com.au", "piotr.solnica@gmail.com"]
|
10
|
+
spec.summary = "A complete, standalone view rendering system that gives you everything you need to write well-factored view code"
|
12
11
|
spec.description = spec.summary
|
13
|
-
spec.homepage = "https://
|
12
|
+
spec.homepage = "https://dry-rb.org/gems/dry-view"
|
14
13
|
spec.license = "MIT"
|
15
14
|
|
16
|
-
spec.files = `git ls-files -z`.split("\x0")
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(benchmarks|examples|spec)/}) }
|
17
16
|
spec.bindir = "exe"
|
18
17
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
18
|
spec.require_paths = ["lib"]
|
21
19
|
|
22
20
|
spec.required_ruby_version = '>= 2.2.0'
|
23
21
|
|
24
|
-
spec.add_runtime_dependency "tilt", "~> 2.0"
|
22
|
+
spec.add_runtime_dependency "tilt", "~> 2.0", ">= 2.0.6"
|
25
23
|
spec.add_runtime_dependency "dry-core", "~> 0.2"
|
26
24
|
spec.add_runtime_dependency "dry-configurable", "~> 0.1"
|
27
25
|
spec.add_runtime_dependency "dry-equalizer", "~> 0.2"
|
26
|
+
spec.add_runtime_dependency "dry-inflector", "~> 0.1"
|
28
27
|
|
29
|
-
spec.add_development_dependency "bundler"
|
28
|
+
spec.add_development_dependency "bundler"
|
30
29
|
spec.add_development_dependency "rake", "~> 10.0"
|
31
30
|
spec.add_development_dependency "rspec", "~> 3.1"
|
32
31
|
end
|
data/lib/dry-view.rb
CHANGED
data/lib/dry/view.rb
CHANGED
@@ -1,2 +1,503 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/configurable"
|
4
|
+
require "dry/core/cache"
|
5
|
+
require "dry/equalizer"
|
6
|
+
require "dry/inflector"
|
7
|
+
|
8
|
+
require_relative "view/context"
|
9
|
+
require_relative "view/exposures"
|
10
|
+
require_relative "view/part_builder"
|
11
|
+
require_relative "view/path"
|
12
|
+
require_relative "view/render_environment"
|
13
|
+
require_relative "view/rendered"
|
14
|
+
require_relative "view/renderer"
|
15
|
+
require_relative "view/scope_builder"
|
16
|
+
|
17
|
+
# A collection of next-generation Ruby libraries, helping you to write clear,
|
18
|
+
# flexible, and more maintainable Ruby code. Each dry-rb gem fulfils a common
|
19
|
+
# task, and together they make a powerful platform for any kind of Ruby
|
20
|
+
# application.
|
21
|
+
module Dry
|
22
|
+
# A standalone, template-based view rendering system that offers everything
|
23
|
+
# you need to write well-factored view code.
|
24
|
+
#
|
25
|
+
# This represents a single view, holding the configuration and exposures
|
26
|
+
# necessary for rendering its template.
|
27
|
+
#
|
28
|
+
# @abstract Subclass this and provide your own configuration and exposures to
|
29
|
+
# define your own view (along with a custom `#initialize` if you wish to
|
30
|
+
# inject dependencies into your subclass)
|
31
|
+
#
|
32
|
+
# @see https://dry-rb.org/gems/dry-view/
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
class View
|
36
|
+
# @api private
|
37
|
+
UndefinedTemplateError = Class.new(StandardError)
|
38
|
+
|
39
|
+
# @api private
|
40
|
+
DEFAULT_RENDERER_OPTIONS = {default_encoding: "utf-8"}.freeze
|
41
|
+
|
42
|
+
include Dry::Equalizer(:config, :exposures)
|
43
|
+
|
44
|
+
extend Dry::Core::Cache
|
45
|
+
|
46
|
+
extend Dry::Configurable
|
47
|
+
|
48
|
+
# @!group Configuration
|
49
|
+
|
50
|
+
# @overload config.paths=(paths)
|
51
|
+
# Set an array of directories that will be searched for all templates
|
52
|
+
# (templates, partials, and layouts).
|
53
|
+
#
|
54
|
+
# These will be converted into Path objects and used for template lookup
|
55
|
+
# when rendering.
|
56
|
+
#
|
57
|
+
# This is a **required setting**.
|
58
|
+
#
|
59
|
+
# @param paths [String, Path, Array<String, Path>] the paths
|
60
|
+
#
|
61
|
+
# @api public
|
62
|
+
# @!scope class
|
63
|
+
setting :paths do |paths|
|
64
|
+
Array(paths).map { |path| Path[path] }
|
65
|
+
end
|
66
|
+
|
67
|
+
# @overload config.template=(name)
|
68
|
+
# Set the name of the template for rendering this view. Template name
|
69
|
+
# should be relative to the configured `paths`.
|
70
|
+
#
|
71
|
+
# This is a **required setting**.
|
72
|
+
#
|
73
|
+
# @param name [String] template name
|
74
|
+
# @api public
|
75
|
+
# @!scope class
|
76
|
+
setting :template
|
77
|
+
|
78
|
+
# @overload config.layout=(name)
|
79
|
+
# Set the name of the layout to render templates within. Layouts will be
|
80
|
+
# looked up within the configured `layouts_dir`, within the configured
|
81
|
+
# `paths`.
|
82
|
+
#
|
83
|
+
# A false or nil value will use no layout. Defaults to `nil`.
|
84
|
+
#
|
85
|
+
# @param name [String, FalseClass, nil] layout name, or false to indicate no layout
|
86
|
+
# @api public
|
87
|
+
# @!scope class
|
88
|
+
setting :layout, false
|
89
|
+
|
90
|
+
# @overload config.layouts_dir=(dir)
|
91
|
+
# Set the name of the directory (within the configured `paths`) holding
|
92
|
+
# the layouts. Defaults to `"layouts"`
|
93
|
+
#
|
94
|
+
# @param dir [String] directory name
|
95
|
+
# @api public
|
96
|
+
# @!scope class
|
97
|
+
setting :layouts_dir, "layouts"
|
98
|
+
|
99
|
+
# @overload config.scope=(scope_class)
|
100
|
+
# Set the scope class to use when rendering the view's template.
|
101
|
+
#
|
102
|
+
# Configuring a custom scope class allows you to provide extra behaviour
|
103
|
+
# (alongside exposures) to the template.
|
104
|
+
#
|
105
|
+
# @see https://dry-rb.org/gems/dry-view/scopes/
|
106
|
+
#
|
107
|
+
# @param scope_class [Class] scope class (inheriting from `Dry::View::Scope`)
|
108
|
+
# @api public
|
109
|
+
# @!scope class
|
110
|
+
setting :scope
|
111
|
+
|
112
|
+
# @overload config.default_context=(context)
|
113
|
+
# Set the default context object to use when rendering. This will be used
|
114
|
+
# unless another context object is applied at render-time to `View#call`
|
115
|
+
#
|
116
|
+
# Defaults to a frozen instance of `Dry::View::Context`.
|
117
|
+
#
|
118
|
+
# @see View#call
|
119
|
+
#
|
120
|
+
# @param context [Dry::View::Context] context object
|
121
|
+
# @api public
|
122
|
+
# @!scope class
|
123
|
+
setting :default_context, Context.new.freeze
|
124
|
+
|
125
|
+
# @overload config.default_format=(format)
|
126
|
+
# Set the default format to use when rendering.
|
127
|
+
#
|
128
|
+
# Defaults to `:html`.
|
129
|
+
#
|
130
|
+
# @param format [Symbol]
|
131
|
+
# @api public
|
132
|
+
# @!scope class
|
133
|
+
setting :default_format, :html
|
134
|
+
|
135
|
+
# @overload config.scope_namespace=(namespace)
|
136
|
+
# Set a namespace that will be searched when building scope classes.
|
137
|
+
#
|
138
|
+
# @param namespace [Module, Class]
|
139
|
+
#
|
140
|
+
# @see Scope
|
141
|
+
#
|
142
|
+
# @api public
|
143
|
+
# @!scope class
|
144
|
+
setting :part_namespace
|
145
|
+
|
146
|
+
# @overload config.part_builder=(part_builder)
|
147
|
+
# Set a custom part builder class
|
148
|
+
#
|
149
|
+
# @see https://dry-rb.org/gems/dry-view/parts/
|
150
|
+
#
|
151
|
+
# @param part_builder [Class]
|
152
|
+
# @api public
|
153
|
+
# @!scope class
|
154
|
+
setting :part_builder, PartBuilder
|
155
|
+
|
156
|
+
# @overload config.scope_namespace=(namespace)
|
157
|
+
# Set a namespace that will be searched when building scope classes.
|
158
|
+
#
|
159
|
+
# @param namespace [Module, Class]
|
160
|
+
#
|
161
|
+
# @see Scope
|
162
|
+
#
|
163
|
+
# @api public
|
164
|
+
# @!scope class
|
165
|
+
setting :scope_namespace
|
166
|
+
|
167
|
+
# @overload config.scope_builder=(scope_builder)
|
168
|
+
# Set a custom scope builder class
|
169
|
+
#
|
170
|
+
# @see https://dry-rb.org/gems/dry-view/scopes/
|
171
|
+
#
|
172
|
+
# @param scope_builder [Class]
|
173
|
+
# @api public
|
174
|
+
# @!scope class
|
175
|
+
setting :scope_builder, ScopeBuilder
|
176
|
+
|
177
|
+
# @overload config.inflector=(inflector)
|
178
|
+
# Set an inflector to provide to the part_builder and scope_builder.
|
179
|
+
#
|
180
|
+
# Defaults to `Dry::Inflector.new`.
|
181
|
+
#
|
182
|
+
# @param inflector
|
183
|
+
# @api public
|
184
|
+
# @!scope class
|
185
|
+
setting :inflector, Dry::Inflector.new
|
186
|
+
|
187
|
+
# @overload config.renderer_options=(options)
|
188
|
+
# A hash of options to pass to the template engine. Template engines are
|
189
|
+
# provided by Tilt; see Tilt's documentation for what options your
|
190
|
+
# template engine may support.
|
191
|
+
#
|
192
|
+
# Defaults to `{default_encoding: "utf-8"}`. Any options passed will be
|
193
|
+
# merged onto the defaults.
|
194
|
+
#
|
195
|
+
# @see https://github.com/rtomayko/tilt
|
196
|
+
#
|
197
|
+
# @param options [Hash] renderer options
|
198
|
+
# @api public
|
199
|
+
# @!scope class
|
200
|
+
setting :renderer_options, DEFAULT_RENDERER_OPTIONS do |options|
|
201
|
+
DEFAULT_RENDERER_OPTIONS.merge(options.to_h).freeze
|
202
|
+
end
|
203
|
+
|
204
|
+
# @overload config.renderer_engine_mapping=(mapping)
|
205
|
+
# A hash specifying the (Tilt-compatible) template engine class to use
|
206
|
+
# for a given format. Template engine detection is automatic based on
|
207
|
+
# format; use this setting only if you want to force a non-preferred
|
208
|
+
# engine.
|
209
|
+
#
|
210
|
+
# @example
|
211
|
+
# config.renderer_engine_mapping = {erb: Tilt::ErubiTemplate}
|
212
|
+
#
|
213
|
+
# @see https://github.com/rtomayko/tilt
|
214
|
+
#
|
215
|
+
# @param mapping [Hash<Symbol, Class>] engine mapping
|
216
|
+
# @api public
|
217
|
+
# @!scope class
|
218
|
+
setting :renderer_engine_mapping
|
219
|
+
|
220
|
+
# @!endgroup
|
221
|
+
|
222
|
+
# @api private
|
223
|
+
def self.inherited(klass)
|
224
|
+
super
|
225
|
+
exposures.each do |name, exposure|
|
226
|
+
klass.exposures.import(name, exposure)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# @!group Exposures
|
231
|
+
|
232
|
+
# @!macro [new] exposure_options
|
233
|
+
# @param options [Hash] the exposure's options
|
234
|
+
# @option options [Boolean] :layout expose this value to the layout (defaults to false)
|
235
|
+
# @option options [Boolean] :decorate decorate this value in a matching Part (defaults to true)
|
236
|
+
# @option options [Symbol, Class] :as an alternative name or class to use when finding a matching Part
|
237
|
+
|
238
|
+
# @overload expose(name, **options, &block)
|
239
|
+
# Define a value to be passed to the template. The return value of the
|
240
|
+
# block will be decorated by a matching Part and passed to the template.
|
241
|
+
#
|
242
|
+
# The block will be evaluated with the view instance as its `self`. The
|
243
|
+
# block's parameters will determine what it is given:
|
244
|
+
#
|
245
|
+
# - To receive other exposure values, provide positional parameters
|
246
|
+
# matching the exposure names. These exposures will already by decorated
|
247
|
+
# by their Parts.
|
248
|
+
# - To receive the view's input arguments (whatever is passed to
|
249
|
+
# `View#call`), provide matching keyword parameters. You can provide
|
250
|
+
# default values for these parameters to make the corresponding input
|
251
|
+
# keys optional
|
252
|
+
# - To receive the Context object, provide a `context:` keyword parameter
|
253
|
+
# - To receive the view's input arguments in their entirety, provide a
|
254
|
+
# keywords splat parameter (i.e. `**input`)
|
255
|
+
#
|
256
|
+
# @example Accessing input arguments
|
257
|
+
# expose :article do |slug:|
|
258
|
+
# article_repo.find_by_slug(slug)
|
259
|
+
# end
|
260
|
+
#
|
261
|
+
# @example Accessing other exposures
|
262
|
+
# expose :articles do
|
263
|
+
# article_repo.listing
|
264
|
+
# end
|
265
|
+
#
|
266
|
+
# expose :featured_articles do |articles|
|
267
|
+
# articles.select(&:featured?)
|
268
|
+
# end
|
269
|
+
#
|
270
|
+
# @param name [Symbol] name for the exposure
|
271
|
+
# @macro exposure_options
|
272
|
+
#
|
273
|
+
# @overload expose(name, **options)
|
274
|
+
# Define a value to be passed to the template, provided by an instance
|
275
|
+
# method matching the name. The method's return value will be decorated by
|
276
|
+
# a matching Part and passed to the template.
|
277
|
+
#
|
278
|
+
# The method's parameters will determine what it is given:
|
279
|
+
#
|
280
|
+
# - To receive other exposure values, provide positional parameters
|
281
|
+
# matching the exposure names. These exposures will already by decorated
|
282
|
+
# by their Parts.
|
283
|
+
# - To receive the view's input arguments (whatever is passed to
|
284
|
+
# `View#call`), provide matching keyword parameters. You can provide
|
285
|
+
# default values for these parameters to make the corresponding input
|
286
|
+
# keys optional
|
287
|
+
# - To receive the Context object, provide a `context:` keyword parameter
|
288
|
+
# - To receive the view's input arguments in their entirey, provide a
|
289
|
+
# keywords splat parameter (i.e. `**input`)
|
290
|
+
#
|
291
|
+
# @example Accessing input arguments
|
292
|
+
# expose :article
|
293
|
+
#
|
294
|
+
# def article(slug:)
|
295
|
+
# article_repo.find_by_slug(slug)
|
296
|
+
# end
|
297
|
+
#
|
298
|
+
# @example Accessing other exposures
|
299
|
+
# expose :articles
|
300
|
+
# expose :featured_articles
|
301
|
+
#
|
302
|
+
# def articles
|
303
|
+
# article_repo.listing
|
304
|
+
# end
|
305
|
+
#
|
306
|
+
# def featured_articles
|
307
|
+
# articles.select(&:featured?)
|
308
|
+
# end
|
309
|
+
#
|
310
|
+
# @param name [Symbol] name for the exposure
|
311
|
+
# @macro exposure_options
|
312
|
+
#
|
313
|
+
# @overload expose(name, **options)
|
314
|
+
# Define a single value to pass through from the input data (when there is
|
315
|
+
# no instance method matching the `name`). This value will be decorated by
|
316
|
+
# a matching Part and passed to the template.
|
317
|
+
#
|
318
|
+
# @param name [Symbol] name for the exposure
|
319
|
+
# @macro exposure_options
|
320
|
+
# @option options [Boolean] :default a default value to provide if there is no matching input data
|
321
|
+
#
|
322
|
+
# @overload expose(*names, **options)
|
323
|
+
# Define multiple values to pass through from the input data (when there
|
324
|
+
# is no instance methods matching their names). These values will be
|
325
|
+
# decorated by matching Parts and passed through to the template.
|
326
|
+
#
|
327
|
+
# The provided options will be applied to all the exposures.
|
328
|
+
#
|
329
|
+
# @param names [Symbol] names for the exposures
|
330
|
+
# @macro exposure_options
|
331
|
+
# @option options [Boolean] :default a default value to provide if there is no matching input data
|
332
|
+
#
|
333
|
+
# @see https://dry-rb.org/gems/dry-view/exposures/
|
334
|
+
#
|
335
|
+
# @api public
|
336
|
+
def self.expose(*names, **options, &block)
|
337
|
+
if names.length == 1
|
338
|
+
exposures.add(names.first, block, options)
|
339
|
+
else
|
340
|
+
names.each do |name|
|
341
|
+
exposures.add(name, options)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# @api public
|
347
|
+
def self.private_expose(*names, **options, &block)
|
348
|
+
expose(*names, **options, private: true, &block)
|
349
|
+
end
|
350
|
+
|
351
|
+
# Returns the defined exposures. These are unbound, since bound exposures
|
352
|
+
# are only created when initializing a View instance.
|
353
|
+
#
|
354
|
+
# @return [Exposures]
|
355
|
+
# @api private
|
356
|
+
def self.exposures
|
357
|
+
@exposures ||= Exposures.new
|
358
|
+
end
|
359
|
+
|
360
|
+
# @!endgroup
|
361
|
+
|
362
|
+
# @!group Render environment
|
363
|
+
|
364
|
+
# Returns a render environment for the view and the given options. This
|
365
|
+
# environment isn't chdir'ed into any particular directory.
|
366
|
+
#
|
367
|
+
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
368
|
+
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
369
|
+
#
|
370
|
+
# @see View.template_env render environment for the view's template
|
371
|
+
# @see View.layout_env render environment for the view's layout
|
372
|
+
#
|
373
|
+
# @return [RenderEnvironment]
|
374
|
+
# @api public
|
375
|
+
def self.render_env(format: config.default_format, context: config.default_context)
|
376
|
+
RenderEnvironment.prepare(renderer(format), config, context)
|
377
|
+
end
|
378
|
+
|
379
|
+
# @overload template_env(format: config.default_format, context: config.default_context)
|
380
|
+
# Returns a render environment for the view and the given options,
|
381
|
+
# chdir'ed into the view's template directory. This is the environment
|
382
|
+
# used when rendering the template, and is useful to to fetch
|
383
|
+
# independently when unit testing Parts and Scopes.
|
384
|
+
#
|
385
|
+
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
386
|
+
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
387
|
+
#
|
388
|
+
# @return [RenderEnvironment]
|
389
|
+
# @api public
|
390
|
+
def self.template_env(**args)
|
391
|
+
render_env(**args).chdir(config.template)
|
392
|
+
end
|
393
|
+
|
394
|
+
# @overload layout_env(format: config.default_format, context: config.default_context)
|
395
|
+
# Returns a render environment for the view and the given options,
|
396
|
+
# chdir'ed into the view's layout directory. This is the environment used
|
397
|
+
# when rendering the view's layout.
|
398
|
+
#
|
399
|
+
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
400
|
+
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
401
|
+
#
|
402
|
+
# @return [RenderEnvironment] @api public
|
403
|
+
def self.layout_env(**args)
|
404
|
+
render_env(**args).chdir(layout_path)
|
405
|
+
end
|
406
|
+
|
407
|
+
# Returns renderer for the view and provided format
|
408
|
+
#
|
409
|
+
# @api private
|
410
|
+
def self.renderer(format)
|
411
|
+
fetch_or_store(:renderer, config, format) {
|
412
|
+
Renderer.new(
|
413
|
+
config.paths,
|
414
|
+
format: format,
|
415
|
+
engine_mapping: config.renderer_engine_mapping,
|
416
|
+
**config.renderer_options,
|
417
|
+
)
|
418
|
+
}
|
419
|
+
end
|
420
|
+
|
421
|
+
# @api private
|
422
|
+
def self.layout_path
|
423
|
+
File.join(config.layouts_dir, config.layout)
|
424
|
+
end
|
425
|
+
|
426
|
+
# @!endgroup
|
427
|
+
|
428
|
+
# The view's bound exposures
|
429
|
+
#
|
430
|
+
# @return [Exposures]
|
431
|
+
# @api private
|
432
|
+
attr_reader :exposures
|
433
|
+
|
434
|
+
# Returns an instance of the view. This binds the defined exposures to the
|
435
|
+
# view instance.
|
436
|
+
#
|
437
|
+
# Subclasses can define their own `#initialize` to accept injected
|
438
|
+
# dependencies, but must call `super()` to ensure the standard view
|
439
|
+
# initialization can proceed.
|
440
|
+
#
|
441
|
+
# @api public
|
442
|
+
def initialize
|
443
|
+
@exposures = self.class.exposures.bind(self)
|
444
|
+
end
|
445
|
+
|
446
|
+
# The view's configuration
|
447
|
+
#
|
448
|
+
# @api private
|
449
|
+
def config
|
450
|
+
self.class.config
|
451
|
+
end
|
452
|
+
|
453
|
+
# Render the view
|
454
|
+
#
|
455
|
+
# @param format [Symbol] template format to use
|
456
|
+
# @param context [Context] context object to use
|
457
|
+
# @param input input data for preparing exposure values
|
458
|
+
#
|
459
|
+
# @return [Rendered] rendered view object
|
460
|
+
# @api public
|
461
|
+
def call(format: config.default_format, context: config.default_context, **input)
|
462
|
+
raise UndefinedTemplateError, "no +template+ configured" unless config.template
|
463
|
+
|
464
|
+
env = self.class.render_env(format: format, context: context)
|
465
|
+
template_env = self.class.template_env(format: format, context: context)
|
466
|
+
|
467
|
+
locals = locals(template_env, input)
|
468
|
+
output = env.template(config.template, template_env.scope(config.scope, locals))
|
469
|
+
|
470
|
+
if layout?
|
471
|
+
layout_env = self.class.layout_env(format: format, context: context)
|
472
|
+
output = layout_env.template(self.class.layout_path, layout_env.scope(config.scope, layout_locals(locals))) { output }
|
473
|
+
end
|
474
|
+
|
475
|
+
Rendered.new(output: output, locals: locals)
|
476
|
+
end
|
477
|
+
|
478
|
+
private
|
479
|
+
|
480
|
+
# @api private
|
481
|
+
def locals(render_env, input)
|
482
|
+
exposures.(context: render_env.context, **input) do |value, exposure|
|
483
|
+
if exposure.decorate? && value
|
484
|
+
render_env.part(exposure.name, value, **exposure.options)
|
485
|
+
else
|
486
|
+
value
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
# @api private
|
492
|
+
def layout_locals(locals)
|
493
|
+
locals.each_with_object({}) do |(key, value), layout_locals|
|
494
|
+
layout_locals[key] = value if exposures[key].for_layout?
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# @api private
|
499
|
+
def layout?
|
500
|
+
!!config.layout
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|