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