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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +18 -0
  3. data/.travis.yml +15 -10
  4. data/.yardopts +5 -0
  5. data/CHANGELOG.md +60 -1
  6. data/Gemfile +15 -5
  7. data/README.md +38 -13
  8. data/bin/setup +5 -0
  9. data/bin/setup_helpers.rb +27 -0
  10. data/dry-view.gemspec +8 -9
  11. data/lib/dry-view.rb +3 -1
  12. data/lib/dry/view.rb +503 -2
  13. data/lib/dry/view/context.rb +80 -0
  14. data/lib/dry/view/decorated_attributes.rb +81 -0
  15. data/lib/dry/view/exposure.rb +15 -2
  16. data/lib/dry/view/exposures.rb +15 -5
  17. data/lib/dry/view/part.rb +154 -61
  18. data/lib/dry/view/part_builder.rb +136 -0
  19. data/lib/dry/view/path.rb +22 -5
  20. data/lib/dry/view/render_environment.rb +62 -0
  21. data/lib/dry/view/render_environment_missing.rb +44 -0
  22. data/lib/dry/view/rendered.rb +55 -0
  23. data/lib/dry/view/renderer.rb +22 -19
  24. data/lib/dry/view/scope.rb +146 -14
  25. data/lib/dry/view/scope_builder.rb +98 -0
  26. data/lib/dry/view/tilt.rb +78 -0
  27. data/lib/dry/view/tilt/erb.rb +26 -0
  28. data/lib/dry/view/tilt/erbse.rb +21 -0
  29. data/lib/dry/view/tilt/haml.rb +26 -0
  30. data/lib/dry/view/version.rb +5 -2
  31. metadata +50 -88
  32. data/benchmarks/templates/button.html.erb +0 -1
  33. data/benchmarks/view.rb +0 -24
  34. data/lib/dry/view/controller.rb +0 -159
  35. data/lib/dry/view/decorator.rb +0 -45
  36. data/lib/dry/view/missing_renderer.rb +0 -15
  37. data/spec/fixtures/templates/_hello.html.slim +0 -1
  38. data/spec/fixtures/templates/controller_renderer_options.html.erb +0 -3
  39. data/spec/fixtures/templates/decorated_parts.html.slim +0 -4
  40. data/spec/fixtures/templates/edit.html.slim +0 -11
  41. data/spec/fixtures/templates/empty.html.slim +0 -1
  42. data/spec/fixtures/templates/greeting.html.slim +0 -2
  43. data/spec/fixtures/templates/hello.html.slim +0 -1
  44. data/spec/fixtures/templates/layouts/app.html.slim +0 -6
  45. data/spec/fixtures/templates/layouts/app.txt.erb +0 -3
  46. data/spec/fixtures/templates/parts_with_args.html.slim +0 -3
  47. data/spec/fixtures/templates/parts_with_args/_box.html.slim +0 -3
  48. data/spec/fixtures/templates/shared/_index_table.html.slim +0 -2
  49. data/spec/fixtures/templates/shared/_shared_hello.html.slim +0 -1
  50. data/spec/fixtures/templates/tasks.html.slim +0 -3
  51. data/spec/fixtures/templates/user.html.slim +0 -2
  52. data/spec/fixtures/templates/users.html.slim +0 -5
  53. data/spec/fixtures/templates/users.txt.erb +0 -3
  54. data/spec/fixtures/templates/users/_row.html.slim +0 -2
  55. data/spec/fixtures/templates/users/_tbody.html.slim +0 -5
  56. data/spec/fixtures/templates/users_with_count.html.slim +0 -5
  57. data/spec/fixtures/templates/users_with_count_inherit.html.slim +0 -6
  58. data/spec/fixtures/templates_override/_hello.html.slim +0 -1
  59. data/spec/fixtures/templates_override/users.html.slim +0 -5
  60. data/spec/integration/decorator_spec.rb +0 -80
  61. data/spec/integration/exposures_spec.rb +0 -392
  62. data/spec/integration/part/decorated_attributes_spec.rb +0 -193
  63. data/spec/integration/view_spec.rb +0 -133
  64. data/spec/spec_helper.rb +0 -46
  65. data/spec/unit/controller_spec.rb +0 -83
  66. data/spec/unit/decorator_spec.rb +0 -61
  67. data/spec/unit/exposure_spec.rb +0 -227
  68. data/spec/unit/exposures_spec.rb +0 -103
  69. data/spec/unit/part_spec.rb +0 -104
  70. data/spec/unit/renderer_spec.rb +0 -57
  71. data/spec/unit/scope_spec.rb +0 -53
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c234898af572e4ebd4b6717f0ba9519f2b4feda8a6d7b2133c00ee8cea2ac7a
4
- data.tar.gz: 6f086379dad341a18fbf4f68b3b6edde81cdcdf30e38c859aa5ba86556c56ae7
3
+ metadata.gz: 4d0cd65b1329feef3497caadc8f41521850546d9c60aa673d035215bcaaee922
4
+ data.tar.gz: 46e2e12a0d192dd2e7443c334bc62fdd86ff76f1aa308c1b1ac14a921e67565b
5
5
  SHA512:
6
- metadata.gz: 0af32817c536bd8beeb57d63e856aeb49400579ab084a586c7d8fcdad32052d91244cf4a450a5b10243915fa652140d3389fccbd2421a04d2479767d515e5bb5
7
- data.tar.gz: e6dbf82b4d90b57751a74047ffa8d9cda75986bb468a42553ab150f8c9f12458662329f0be859bc1701ce6d39e3ee1ee523abcc23d0ce928ccba106e3b9593f3
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
- after_success:
9
- # Send coverage report from the job #1 == current MRI release
10
- - '[ "${TRAVIS_JOB_NUMBER#*.}" = "1" ] && [ "$TRAVIS_BRANCH" = "master" ] && bundle exec codeclimate-test-reporter'
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.5.0
13
- - 2.4.2
14
- - 2.3.6
15
- - jruby-9.1.10.0
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 # options: [always|never|change] default: always
22
- on_failure: always # options: [always|never|change] default: always
23
- on_start: false # default: 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
@@ -0,0 +1,5 @@
1
+ --query '@api.text != "private"'
2
+ --markup-provider=redcarpet
3
+ --markup=markdown
4
+ --plugin junk
5
+ lib/**/*.rb
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.0...v0.5.1](https://github.com/dry-rb/dry-view/compare/v0.5.1...v0.5.2)
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 'rack', '>= 1.0.0', '<= 2.0.0'
13
- gem 'slim'
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
- [gitter]: https://gitter.im/dry-rb/chat
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)](https://codeclimate.com/github/dry-rb/dry-view/maintainability)
11
- [![Test Coverage](https://api.codeclimate.com/v1/badges/de81a8026a2e7f64e4df/test_coverage)](https://codeclimate.com/github/dry-rb/dry-view/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
- A simple, standalone view rendering system built around functional view
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](http://dry-rb.org/gems/dry-view)
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,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "setup_helpers"
4
+
5
+ Setup.execute "bundle"
@@ -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 = ["Piotr Solnica", "Tim Riley"]
10
- spec.email = ["piotr.solnica@gmail.com", "tim@icelab.com.au"]
11
- spec.summary = "Functional view rendering system"
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://github.com/dry-rb/dry-view"
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", "~> 1.7"
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
@@ -1 +1,3 @@
1
- require 'dry/view'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/view"
data/lib/dry/view.rb CHANGED
@@ -1,2 +1,503 @@
1
- require 'dry/view/renderer'
2
- require 'dry/view/controller'
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