dry-view 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 [][gitter]
|
1
|
+
# dry-view
|
7
2
|
|
8
3
|
[][gem]
|
9
4
|
[][travis]
|
10
|
-
[]
|
11
|
-
[]
|
5
|
+
[][maint]
|
6
|
+
[][cov]
|
12
7
|
[][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
|