brut 0.0.20 → 0.0.21

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/lib/brut/back_end/seed_data.rb +19 -2
  4. data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
  5. data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
  6. data/lib/brut/back_end/sidekiq.rb +2 -1
  7. data/lib/brut/back_end/validator.rb +5 -1
  8. data/lib/brut/back_end.rb +4 -2
  9. data/lib/brut/cli.rb +4 -3
  10. data/lib/brut/factory_bot.rb +0 -5
  11. data/lib/brut/framework/app.rb +70 -5
  12. data/lib/brut/framework/config.rb +5 -3
  13. data/lib/brut/framework/container.rb +3 -2
  14. data/lib/brut/framework/errors.rb +12 -4
  15. data/lib/brut/framework/mcp.rb +62 -1
  16. data/lib/brut/framework/project_environment.rb +6 -2
  17. data/lib/brut/framework.rb +1 -1
  18. data/lib/brut/front_end/component.rb +35 -12
  19. data/lib/brut/front_end/components/constraint_violations.rb +1 -1
  20. data/lib/brut/front_end/components/form_tag.rb +1 -1
  21. data/lib/brut/front_end/components/inputs/csrf_token.rb +1 -1
  22. data/lib/brut/front_end/components/inputs/text_field.rb +1 -1
  23. data/lib/brut/front_end/components/time_tag.rb +1 -1
  24. data/lib/brut/front_end/layout.rb +16 -0
  25. data/lib/brut/front_end/page.rb +51 -26
  26. data/lib/brut/front_end/routing.rb +5 -1
  27. data/lib/brut/front_end.rb +4 -13
  28. data/lib/brut/i18n/base_methods.rb +37 -3
  29. data/lib/brut/i18n/for_back_end.rb +3 -0
  30. data/lib/brut/i18n/for_cli.rb +3 -0
  31. data/lib/brut/i18n/http_accept_language.rb +47 -0
  32. data/lib/brut/instrumentation/open_telemetry.rb +25 -0
  33. data/lib/brut/instrumentation.rb +3 -5
  34. data/lib/brut/sinatra_helpers.rb +1 -0
  35. data/lib/brut/spec_support/component_support.rb +18 -4
  36. data/lib/brut/spec_support/e2e_support.rb +1 -1
  37. data/lib/brut/spec_support/general_support.rb +3 -0
  38. data/lib/brut/spec_support/handler_support.rb +6 -1
  39. data/lib/brut/spec_support/matcher.rb +1 -0
  40. data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
  41. data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
  42. data/lib/brut/spec_support/matchers/have_i18n_string.rb +2 -5
  43. data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
  44. data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
  45. data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
  46. data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
  47. data/lib/brut/spec_support.rb +1 -1
  48. data/lib/brut/version.rb +1 -1
  49. data/lib/brut.rb +5 -4
  50. metadata +2 -9
  51. data/doc-src/architecture.md +0 -102
  52. data/doc-src/assets.md +0 -98
  53. data/doc-src/forms.md +0 -214
  54. data/doc-src/handlers.md +0 -83
  55. data/doc-src/javascript.md +0 -265
  56. data/doc-src/keyword-injection.md +0 -183
  57. data/doc-src/pages.md +0 -210
  58. data/doc-src/route-hooks.md +0 -59
@@ -1,46 +1,44 @@
1
- # A page backs a web page. A page renders everything in the browser window. Technically, it is exactly like a component except that
2
- # it can have a layout.
1
+ # A Page backs a web page, which handles rendering everything in a browser window when a URL is requested.
2
+ # Technically, a page is identical to a {Brut::FrontEnd::Component}, except that a page has a layout.
3
+ # A {Brut::FrontEnd::Layout} is common HTML that surrounds your page's HTML.
4
+ # Your page is a Phlex component, but instead of implementing `view_template`, you
5
+ # implement {#page_template} to ensure the layout is used.
3
6
  #
4
- # When subclassing this to create a page, your initializer's signature will determine what data
5
- # is required for your page to work. It can be anything, just keep in mind that any component your page uses may
6
- # require additional data.
7
+ # To create a page, after defining a route, subclass this class (or, more likely, your app's `AppPage`) and
8
+ # provide an initializer that accepts keyword arguments. The names of these arguments will be used to locate the
9
+ # values that Brut will pass in when creating your page object.
7
10
  #
8
- # If your page does not override {#render} (which, generally, it won't), an ERB file is expected to exist alongside it in the
9
- # app. For example, if you have a page named `Auth::LoginPage`, it would expected to be in
10
- # `app/src/front_end/pages/auth/login_page.rb`. Thus, Brut will also expect
11
- # `app/src/front_end/pages/auth/login_page.html.erb` to exist as well. That ERB file is used with an instance of your
12
- # pages's class to render the page's HTML.
11
+ # Consult Brut's documentation on keyword injection to know what values you may use and how values are located.
13
12
  #
14
13
  # @see Brut::FrontEnd::Component
14
+ # @see Brut::FrontEnd::Layout
15
15
  class Brut::FrontEnd::Page < Brut::FrontEnd::Component
16
16
  include Brut::FrontEnd::HandlingResults
17
17
 
18
- # Returns the name of the layout for this page. This string is used to find an ERB file in `app/src/front_end/layouts`. Every page
19
- # must have a layout. If you wish to render a page with no layout, create an empty layout in your app and use that.
18
+ # Returns the name of the layout for this page. This string is used to find a class named
19
+ # `«camelized-layout»Layout` in your app. The default value is "default", meaning that the class
20
+ # `DefaultLayout` will be used.
20
21
  #
21
- # Note that the layout can be dynamic. It is requested when {#render} is called, so you can override this
22
+ # Note that the layout can be dynamic. It is requested when {#page_template} is called, so you can override this
22
23
  # method and use any ivar set in your constructor to change what layout is used.
23
24
  #
25
+ # If your page does not need a layout, you have two options:
26
+ #
27
+ # * Create your own blank layout named, e.g. `BlankLayout` and have this method return `"blank"`.
28
+ # * Implement `view_template` instead of `page_template`, thus overriding this class' implementation that uses
29
+ # layouts.
30
+ #
24
31
  # @return [String] The name of the layout. May not be `nil`.
25
32
  def layout = "default"
26
33
 
27
- # Called after the page is created, but before {#render} is called. This allows you to do any pre-flight checks and potentially
34
+ # Called after the page is created, but before {#page_template} is called. This allows you to do any pre-flight checks and potentially
28
35
  # redirect the user or produce an error.
29
36
  #
30
- # @return [URI|Brut::FrontEnd::HttpStatus|Object] If you return a `URI` (mostly likely by returning the result of calling {Brut::FrontEnd::HandlingResults#redirect_to}), the user is redirected and {#render} is never called. If you return a {Brut::FrontEnd::HttpStatus} (mostly likely by returning the result of calling {Brut::FrontEnd::HandlingResults#http_status}), {#render} is skipped and that status is returned with no content. If anything else is returned, {#render} is called as normal.
37
+ # @return [URI|Brut::FrontEnd::HttpStatus|Object] If you return a `URI` (mostly likely by returning the result of calling {Brut::FrontEnd::HandlingResults#redirect_to}), the user is redirected and no HTML is generated. If you return a {Brut::FrontEnd::HttpStatus} (mostly likely by returning the result of calling {Brut::FrontEnd::HandlingResults#http_status}), HTML generation is skipped and that status is returned with no content. If anything else is returned, HTML is generated normal.
31
38
  def before_render = nil
32
39
 
33
- def with_layout(&block)
34
- layout_class = Module.const_get(
35
- layout_class = RichString.new([
36
- self.layout,
37
- "layout"
38
- ].join("_")).camelize
39
- )
40
- render layout_class.new(page_name:,&block)
41
- end
42
-
43
-
40
+ # Core method of this class. Do not override. This handles the use of {#before_render} and is what Brut
41
+ # calls to possibly render the page.
44
42
  def handle!
45
43
  case before_render
46
44
  in URI => uri
@@ -52,6 +50,15 @@ class Brut::FrontEnd::Page < Brut::FrontEnd::Component
52
50
  end
53
51
  end
54
52
 
53
+ # Override this method to produce your page's HTML. You are intended to call Phlex
54
+ # methods here. Anything you can do inside the Phlex-standard `view_template` method, you can
55
+ # do here. The only difference is that this will all be rendered in the context of your configured
56
+ # {#layout}.
57
+ def page_template = abstract_method!
58
+
59
+ # Phlex's API to produce markup. Do not override this or you will lose your layout.
60
+ # This implementation locates the configured layout, renders it, and renders {#page_template}
61
+ # inside.
55
62
  def view_template
56
63
  with_layout do
57
64
  page_template
@@ -68,6 +75,24 @@ class Brut::FrontEnd::Page < Brut::FrontEnd::Component
68
75
  # @!visibility private
69
76
  def component_name = raise Brut::Framework::Errors::Bug,"#{self.class} is not a component"
70
77
 
78
+ private
79
+
80
+ # Locates the layout class and uses it to render itself, along
81
+ # with the block given.
82
+ #
83
+ # @!visibility private
84
+ def with_layout(&block)
85
+ layout_class = Module.const_get(
86
+ layout_class = RichString.new([
87
+ self.layout,
88
+ "layout"
89
+ ].join("_")).camelize
90
+ )
91
+ render layout_class.new(page_name:,&block)
92
+ end
93
+
94
+
95
+
71
96
  end
72
97
 
73
98
  # Holds pages included with the Brut framework
@@ -218,7 +218,11 @@ private
218
218
  joined_path = joined_path + "#" + URI.encode_uri_component(anchor)
219
219
  end
220
220
  uri = URI(joined_path)
221
- uri.query = URI.encode_www_form(query_string_params)
221
+ query_string = URI.encode_www_form(query_string_params)
222
+ if query_string.to_s.strip != ""
223
+ uri.query = query_string
224
+ end
225
+
222
226
  uri.extend(Phlex::SGML::SafeObject)
223
227
  end
224
228
 
@@ -1,16 +1,7 @@
1
- # In Brut, the _front end_ is considered anything that interacts directly with a web browser or HTTP. This includes rendering HTML,
2
- # managing JavaScript and CSS, and processing form submissions. It contrasts to {Brut::BackEnd}, which handles the business logic
3
- # and database.
4
- #
5
- # You {Brut::App} defines pages, forms, and actions. A page is backed by a subclass of {Brut::FrontEnd::Page}, which provides
6
- # dynamic data for rendering. A page can reference {Brut::FrontEnd::Component} subclasses to allow functional decomposition of front
7
- # end logic and markup, as well as re-use. Both pages and components have ERB files that describe the HTML to be rendered.
8
- #
9
- # A {Brut::FrontEnd::Form} subclass defines a form that a browser will submit to your app. That
10
- # submission is processed by a {Brut::FrontEnd::Handler} subclass. Handlers can also respond to other HTTP requests.
11
- #
12
- # In addition to responding to requests, you can subclass {Brut::FrontEnd::RouteHook} or {Brut::FrontEnd::Middleware} to perform
13
- # further manipulation of the request.
1
+ # In Brut, the _front end_ is considered anything that interacts directly
2
+ # with a web browser or HTTP. This includes rendering HTML, managing
3
+ # JavaScript and CSS, and processing form submissions. It contrasts to
4
+ # {Brut::BackEnd}, which handles the business logic and database.
14
5
  #
15
6
  # The entire front-end is based on Rack, so you should be able to achieve anything you need to.
16
7
  module Brut::FrontEnd
@@ -23,12 +23,12 @@ module Brut::I18n::BaseMethods
23
23
  # to `t("email.required", field: "E-mail address")` would generate `"E-mail address is required"`.
24
24
  #
25
25
  # @param [String,Symbol,Array<String>,Array<Symbol>] key used to create one or more keys to be translated.
26
- # This value's behavior is designed to a balance predictabilitiy in what actual key is chosen
26
+ # This value's behavior is designed to balance predictabilitiy in what actual key is chosen
27
27
  # but without needless repetition on a page. If this value is provided, and is an array, the values
28
28
  # are joined with "." to form a key. If the value is not an array, that value is used directly.
29
29
  # Given this key, two values are checked for a translation: the key itself and
30
30
  # the key inside "general.". If this value is *not* provided, it is expected
31
- # taht the `**rest` hash includes page: or component:. See that parameter and the example.
31
+ # that the `**rest` hash includes page: or component:. See that parameter and the example.
32
32
  #
33
33
  # @param [Hash] rest values to use for interpolation of the key's translation. If `key` is omitted,
34
34
  # this hash should have a value for either `page:` or `component:` (not both). If
@@ -39,6 +39,16 @@ module Brut::I18n::BaseMethods
39
39
  # Note that if the page– or component–specific key is not found, this will check
40
40
  # `general.«page: value»`.
41
41
  # @option interpolated_values [Numeric] count Special interpolation to control pluralization.
42
+ # @yield Nothing is yielded if a block is given, however the value returned is used for the `%{block}`
43
+ # interpolation value.
44
+ # @yieldreturn [String] The value to use for the `%{block}` interpolation value. There is some nuance to
45
+ # how this works. The value returned is given to `capture`, and that value
46
+ # is given to `safe`. Outside of an HTML-rendering context, these methods
47
+ # simply pass through the contents of the block. In an HTML-rendering
48
+ # context, however, these methods are assumed to be from
49
+ # [`Phlex::HTML`](https://phlex.fun). `capture` will create a new Phlex
50
+ # context and capture any HTML built inside the block. That HTML is assumed
51
+ # to be safe, thus `safe` is called to communicate this to Phlex.
42
52
  #
43
53
  # @raise [I18n::MissingTranslation] if no translation is found
44
54
  # @raise [I18n::MissingInterpolationArgument] if interpolation arguments are missing, or if the key
@@ -89,7 +99,7 @@ module Brut::I18n::BaseMethods
89
99
  # t(page: :new_widget) # => Create new Widget
90
100
  # # in your code for WidgetsPage
91
101
  # t(page: :new_widget) # => Create New
92
- # # in your code for SomeOtherEPage
102
+ # # in your code for SomeOtherPage
93
103
  # t(page: :new_widget) # => Make a New Widget
94
104
  #
95
105
  # @example Using page: with an array
@@ -106,6 +116,30 @@ module Brut::I18n::BaseMethods
106
116
  # }
107
117
  # # in your code for HomePage
108
118
  # t(page: [ :captions, :new ]) # => New Widgets
119
+ #
120
+ # @example Using a block with Phlex
121
+ # # in your translations file
122
+ # en: {
123
+ # greeting: "Hello there %{name}, you may %{block}",
124
+ # }
125
+ # # Inside a component where
126
+ # # Brut::I18n::ForHTML has been included
127
+ # def view_template
128
+ # h1 do
129
+ # raw(t(:greeting), name: user.name) do
130
+ # a(href: "https://support.example.com") do
131
+ # "contact support"
132
+ # end
133
+ # end
134
+ # end
135
+ # end
136
+ # # This will produce this HTML, assuming user.name is "Pat":
137
+ # <h1>
138
+ # Hell there Pat, you may
139
+ # <a href="https://support.example.com">
140
+ # contact support
141
+ # </a>
142
+ # </h1>
109
143
  def t(key=:look_in_rest,**rest,&block)
110
144
  if key == :look_in_rest
111
145
 
@@ -1,3 +1,6 @@
1
+ # Use this to access translations in any back-end code.
2
+ # This implementation does support blocks yielded to {#t}, however
3
+ # their values are not necessarily HTML-escaped.
1
4
  module Brut::I18n::ForBackEnd
2
5
  include Brut::I18n::BaseMethods
3
6
  def safe(string) = string
@@ -1,3 +1,6 @@
1
+ # Use this to access translations in any CLI.
2
+ # This implementation does support blocks yielded to {#t}, however
3
+ # their values are not necessarily HTML-escaped.
1
4
  module Brut::I18n::ForCLI
2
5
  include Brut::I18n::BaseMethods
3
6
  def safe(string) = string
@@ -1,8 +1,19 @@
1
+ # Manages the value for the HTTP
2
+ # [Accept-Language](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language)
3
+ # header. Generally, you would not interact with this class directly, however it is used
4
+ # by Brut to make a guess as to which Locale a browser is reporting.
1
5
  class Brut::I18n::HTTPAcceptLanguage
6
+ # A locale with the weight (value for q=) it was given in the Accept-Language header
2
7
  WeightedLocale = Data.define(:locale, :q) do
8
+ # Returns the primary locale for whatever locale
9
+ # this is holding. For example, the primary locale
10
+ # of "en-US" is "en".
3
11
  def primary_locale = self.locale.gsub(/\-.*$/,"")
12
+
13
+ # True if this locale is a primary locale
4
14
  def primary? = self.primary_locale == self.locale
5
15
 
16
+ # Return a new WeightedLocale that is the primary locale.
6
17
  def primary_only
7
18
  self.class.new(locale: self.primary_locale, q: self.q)
8
19
  end
@@ -12,6 +23,11 @@ class Brut::I18n::HTTPAcceptLanguage
12
23
  end
13
24
  end
14
25
 
26
+ # Parse the value stored in the session.
27
+ #
28
+ # @param [String] session_value the value stored in the session.
29
+ # @return [Brut::I18n::HTTPAcceptLanguage] a usable object. If the provided value
30
+ # is blank, #{Brut::I18n::HTTPAcceptLanguage::AlwaysEnglish} is returned.
15
31
  def self.from_session(session_value)
16
32
  values = session_value.to_s.split(/,/).map { |value|
17
33
  locale,q = value.split(/;/)
@@ -24,6 +40,19 @@ class Brut::I18n::HTTPAcceptLanguage
24
40
  end
25
41
  end
26
42
 
43
+ # Parse the value provided by the browser via
44
+ # {Brut::FrontEnd::Handlers::LocaleDetectionHandler} via
45
+ # the `brut-locale-detection` custom element (which
46
+ # uses `Intl.DateTimeFormat().resolvedOptions()` to determine
47
+ # the locale).
48
+ #
49
+ # Because this value is not in the same format as the Accept-Language
50
+ # header, it's `q` is assumed to be 1.
51
+ #
52
+ # @param [String] value the value provided by the brower.
53
+ #
54
+ # @return [Brut::I18n::HTTPAcceptLanguage] a usable object. If the provided value
55
+ # is blank, #{Brut::I18n::HTTPAcceptLanguage::AlwaysEnglish} is returned.
27
56
  def self.from_browser(value)
28
57
  value = value.to_s.strip
29
58
  if value == ""
@@ -33,6 +62,10 @@ class Brut::I18n::HTTPAcceptLanguage
33
62
  end
34
63
  end
35
64
 
65
+ # Parse from the HTTP Accept-Language header.
66
+ #
67
+ # @return [Brut::I18n::HTTPAcceptLanguage] a usable object. If the provided value
68
+ # is blank, #{Brut::I18n::HTTPAcceptLanguage::AlwaysEnglish} is returned.
36
69
  def self.from_header(header_value)
37
70
  header_value = header_value.to_s.strip
38
71
  if header_value == "*" || header_value == ""
@@ -50,14 +83,28 @@ class Brut::I18n::HTTPAcceptLanguage
50
83
  end
51
84
  end
52
85
 
86
+ # Ordered list of locales, from highest-weighted to lowest.
53
87
  attr_reader :weighted_locales
88
+ # @param [Array<Brut::I18n::HTTPAcceptLanguage::WeightedLocale>] weighted_locales locales to use. They do not
89
+ # need to be ordered
54
90
  def initialize(weighted_locales)
55
91
  @weighted_locales = weighted_locales.sort_by(&:q).reverse
56
92
  end
93
+
94
+ # True if the values inside this object represent known locales, and not a guess based on missing information.
95
+ # In general, this returns true if the values came from the Accept-Language header, or from the browser.
57
96
  def known? = true
97
+
98
+ # Serialize for storage in the session
99
+ #
100
+ # @return [String] a string that can be stored in the session and later deserialized via {.from_session}.
58
101
  def for_session = @weighted_locales.map { |weighted_locale| "#{weighted_locale.locale};#{weighted_locale.q}" }.join(",")
59
102
  def to_s = self.for_session
60
103
 
104
+ # A subclass that represents the use of English and only English. This is
105
+ # used when attempts to determine the locale fail. Instances of this class
106
+ # are considered "unknown" ({#known?} returns false), which allows Brut
107
+ # to replace this with a known value later on.
61
108
  class AlwaysEnglish < Brut::I18n::HTTPAcceptLanguage
62
109
  def initialize
63
110
  super([ WeightedLocale.new(locale: "en", q: 1) ])
@@ -1,3 +1,7 @@
1
+ # Class to interact with the OpenTelemetry standard in a simpler way than
2
+ # the provided Ruby gem does. In general, you should use this class
3
+ # via `Brut.container.instrumentation`, and you should *not* use the
4
+ # OpenTelemetry ruby library directly. You probably wouldn't want to, anyway.
1
5
  class Brut::Instrumentation::OpenTelemetry
2
6
  # Create a span around the given block of code.
3
7
  #
@@ -39,11 +43,32 @@ class Brut::Instrumentation::OpenTelemetry
39
43
  timestamp:)
40
44
  end
41
45
 
46
+ # Record an exception. In general, use this only if:
47
+ #
48
+ # * You need to have the parent span record this particular exception
49
+ # * You are not going to re-raise the exception.
50
+ #
51
+ # Otherwise, look at {#record_and_reraise_exception!}.
52
+ #
53
+ # @param [Exception] ex the exception to record.
54
+ # @param [Hash] attributes any attributes to attach that will show up in your OTel provider
42
55
  def record_exception(ex,attributes=nil)
43
56
  current_span = OpenTelemetry::Trace.current_span
44
57
  current_span.record_exception(ex,attributes: NormalizedAttributes.new(nil,attributes).to_h)
45
58
  end
46
59
 
60
+ # Record an exception and re-raise it. This is useful if you want
61
+ # the exception recorded as part of the parent span, but still plan
62
+ # to let it raise. Don't do this for every exception you intend to raise.
63
+ # @param [Exception] ex the exception to record.
64
+ # @param [Hash] attributes any attributes to attach that will show up in your OTel provider
65
+ # @raise [Exception] the exception passed in.
66
+ def record_and_reraise_exception(ex,attributes=nil)
67
+ reecord_exception(ex,attributes)
68
+ raise ex
69
+ end
70
+
71
+
47
72
  # Adds attributes to the span, converting the hash or keyword arguments to strings. This will use
48
73
  # the app's Otel prefix for all attributes, so you do not have to prefix them.
49
74
  # If you need to set standard attributes, you should use {Brut::Instrumentation::OpenTelemetry::Span#add_prefixed_attributes} instead.
@@ -1,10 +1,8 @@
1
+ # Namespace for instrumentation setup and support. Brut strives to provide useful
2
+ # instrumentation by default.
3
+ #
1
4
  module Brut::Instrumentation
2
5
  autoload(:OpenTelemetry,"brut/instrumentation/open_telemetry")
3
6
  autoload(:LoggerSpanExporter,"brut/instrumentation/logger_span_exporter")
4
-
5
- # Convenience method to add attributes to create a span without accessing the instrumentation instance directly.
6
- def span(name,**attributes,&block)
7
- Brut.container.instrumentation.span(name,**attributes,&block)
8
- end
9
7
  end
10
8
 
@@ -9,6 +9,7 @@ module Brut::SinatraHelpers
9
9
  sinatra_app.path("/__brut/locale_detection",method: :post)
10
10
  sinatra_app.path("/__brut/instrumentation",method: :get)
11
11
  sinatra_app.set :host_authorization, permitted_hosts: Brut.container.permitted_hosts
12
+ sinatra_app.set :show_exceptions, false
12
13
  end
13
14
 
14
15
  # @private
@@ -10,9 +10,17 @@ module Brut::SpecSupport::ComponentSupport
10
10
  include Brut::SpecSupport::ClockSupport
11
11
  include Brut::I18n::BaseMethods
12
12
 
13
- # Render a component into its text representation. This mimics what happens when a component is used
14
- # inside a template. You typically don't want this, but should use {#render_and_parse}, since that will
15
- # parse the HTML.
13
+ # Render a component or page into its text representation. This mimics what happens when Brut renders
14
+ # the page or component. Note that pages don't always return Strings, for example if `before_render`
15
+ # returns a redirect.
16
+ #
17
+ # When testing a component, call {#render_and_parse} instead of this. When testing a page that will
18
+ # always render HTML, again call {#render_and_parse}.
19
+ #
20
+ # When using this, there are some matchers that can help assert what the page has done:
21
+ #
22
+ # * `have_redirected_to` to check that the page redirected elsewhere, instead of rendering.
23
+ # * `have_returned_http_status` to check that the page returned an HTTP status instead of rendering.
16
24
  def render(component,&block)
17
25
  if component.kind_of?(Brut::FrontEnd::Page)
18
26
  if !block.nil?
@@ -30,7 +38,13 @@ module Brut::SpecSupport::ComponentSupport
30
38
  end
31
39
  end
32
40
 
33
- # Render a component and parse it into a Nokogiri Node for examination.
41
+ # Render a component or page and parse it into a Nokogiri Node for examination. There are several matchers
42
+ # you can use with the return value of this method:
43
+ #
44
+ # * `have_html_attribute` to check if a node has a value for an HTML attribute.
45
+ # * `have_i18n_string` to check if the text of a node is exactly an i18n string you have set up.
46
+ # * `have_link_to` to check that a node contains a link to a page or page routing
47
+ #
34
48
  #
35
49
  # @example
36
50
  #
@@ -1,4 +1,4 @@
1
1
  # Convienience methods for writing E2E tests
2
2
  module Brut::SpecSupport::E2eSupport
3
- include Brut::I18n::BaseMethods
3
+ include Brut::I18n::ForBackEnd
4
4
  end
@@ -5,6 +5,9 @@ module Brut::SpecSupport::GeneralSupport
5
5
  end
6
6
 
7
7
  module ClassMethods
8
+ # Used to indicate that a test does need to be written, but that
9
+ # its implementation can wait until a given date before causing
10
+ # `bin/ci` to fail the test suite.
8
11
  def implementation_is_needed(check_again_at:)
9
12
  check_again_at = if check_again_at.kind_of?(Time)
10
13
  check_again_at
@@ -2,7 +2,12 @@ require_relative "flash_support"
2
2
  require_relative "clock_support"
3
3
  require_relative "session_support"
4
4
 
5
- # Convienience methods for testing handlers.
5
+ # Convienience methods for testing handlers. When testing handlers, the following matchers may be useful:
6
+ #
7
+ #
8
+ # * `have_redirected_to` to check that the handler redirected to a give URI
9
+ # * `have_rendered` to check that the handler rendered a specific page
10
+ # * `have_returned_http_status` to check that the handler returned an HTTP status
6
11
  module Brut::SpecSupport::HandlerSupport
7
12
  include Brut::SpecSupport::FlashSupport
8
13
  include Brut::SpecSupport::ClockSupport
@@ -10,4 +10,5 @@ require_relative "matchers/have_i18n_string"
10
10
  require_relative "matchers/have_redirected_to"
11
11
  require_relative "matchers/have_rendered"
12
12
  require_relative "matchers/have_returned_http_status"
13
+ require_relative "matchers/have_returned_rack_response"
13
14
  require_relative "matchers/have_link_to"
@@ -1,3 +1,4 @@
1
+ # E2E
1
2
  RSpec::Matchers.define :be_page_for do |klass|
2
3
  match do |page|
3
4
  meta = page.locator("meta[name='class']")
@@ -1,3 +1,4 @@
1
+ # Component/Page
1
2
  RSpec::Matchers.define :have_html_attribute do |attribute|
2
3
  if attribute.kind_of?(Hash)
3
4
  if attribute.keys.length != 1
@@ -1,9 +1,6 @@
1
+ # Component/Page
1
2
  RSpec::Matchers.define :have_i18n_string do |key,**args|
2
- include Brut::I18n::ForHTML
3
-
4
- # XXX: Figure out how to not have to do this
5
- def safe(x) = x
6
- def capture(&block) = block.()
3
+ include Brut::I18n::ForBackEnd
7
4
 
8
5
  match do |nokogiri_node|
9
6
 
@@ -1,3 +1,4 @@
1
+ # Component/Page
1
2
  RSpec::Matchers.define :have_link_to do |page_klass,**args|
2
3
  match do |node|
3
4
  node.css("a[href='#{page_klass.routing(**args)}']").any?
@@ -1,3 +1,4 @@
1
+ # Page
1
2
  RSpec::Matchers.define :have_redirected_to do |page_or_uri,**page_params|
2
3
  match do |result|
3
4
  if page_or_uri.kind_of?(URI)
@@ -1,3 +1,4 @@
1
+ # Handler
1
2
  RSpec::Matchers.define :have_rendered do |component_or_page|
2
3
  match do |result|
3
4
  result.class.ancestors.include?(component_or_page)
@@ -0,0 +1,44 @@
1
+ # Handler
2
+ RSpec::Matchers.define :have_returned_rack_response do |http_status: :any, headers: :any, body: :any|
3
+ match do |result|
4
+ case result
5
+ in [ response_status, response_headers, response_body ]
6
+ http_status_match = http_status == :any || response_status == http_status
7
+ headers_match = headers == :any || response_headers == headers
8
+ body_match = body == :any || response_body == body
9
+
10
+ http_status_match && headers_match && body_match
11
+ else
12
+ false
13
+ end
14
+ end
15
+
16
+ failure_message do |result|
17
+ case result
18
+ in [ response_status, response_headers, response_body ]
19
+ http_status_match = http_status == :any || response_status == http_status
20
+ headers_match = headers == :any || response_headers == headers
21
+ body_match = body == :any || response_body == body
22
+ errors = [
23
+ http_status_match ? nil : "HTTP status #{response_status} did not match #{http_status}",
24
+ headers_match ? nil : "Headers #{response_headers} did not match #{headers}",
25
+ body_match ? nil : "Body #{response_body} did not match #{body}",
26
+ ].compact.join(", ")
27
+ else
28
+ if result.kind_of?(Array)
29
+ "Response was a #{result.class} of length #{result.length}, which could not be interpreted as a Rack response."
30
+ else
31
+ "Response was a #{result.class}, which could not be interpreted as a Rack response."
32
+ end
33
+ end
34
+ end
35
+ failure_message_when_negated do |result|
36
+ case result
37
+ in [ response_status, response_headers, response_body ]
38
+ "Response was a Rack response and/or array of size 3"
39
+ else
40
+ "failure_message_when_negated encounterd a code-path for a non-Rack response, which should not have happened when have_returned_rack_response was negated"
41
+ end
42
+ end
43
+
44
+ end
@@ -1,7 +1,7 @@
1
1
  module Brut
2
2
  # Spec Support holds various matchers and helpers useful when writing tests with RSpec.
3
3
  # Note that this module and it's contents aren't loaded by default when you `require "brut"`.
4
- # Your app's `spec_helper.rb` should require these properly.
4
+ # Your app's `spec_helper.rb` should require these explicitly.
5
5
  module SpecSupport
6
6
  end
7
7
  end
data/lib/brut/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Brut
2
2
  # @!visibility private
3
- VERSION = "0.0.20"
3
+ VERSION = "0.0.21"
4
4
  end
data/lib/brut.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  require_relative "brut/framework"
2
2
 
3
- # Brut is a way to make web apps with Ruby. It focuses on web standards, object-orientation, and other fundamentals. Brut seeks to
4
- # minimize abstractions where possible.
3
+ # Brut is a way to make web apps with Ruby. It focuses on web standards, object-orientation, and other
4
+ # fundamentals. Brut seeks to minimize abstractions where possible.
5
5
  #
6
- # Brut encourages the use of the browser's technology and encourages you to build a web app based on good practices that are set up by
7
- # default. Brut may not look easy, but it aims to be simple. It attempts to minimize dependencies and complexity, while leveraging
6
+ # Brut encourages the use of the browser's technology and encourages you to build a web app based
7
+ # on good practices that are set up by default. Brut may not look easy, but it aims to be simple.
8
+ # It attempts to minimize dependencies and complexity, while leveraging
8
9
  # common tested Ruby libraries related to web development.
9
10
  #
10
11
  # Have fun!
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.20
4
+ version: 0.0.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Bryant Copeland
@@ -406,14 +406,6 @@ files:
406
406
  - bin/rake
407
407
  - bin/setup
408
408
  - brut.gemspec
409
- - doc-src/architecture.md
410
- - doc-src/assets.md
411
- - doc-src/forms.md
412
- - doc-src/handlers.md
413
- - doc-src/javascript.md
414
- - doc-src/keyword-injection.md
415
- - doc-src/pages.md
416
- - doc-src/route-hooks.md
417
409
  - docker-compose.dx.yml
418
410
  - docs-todo.md
419
411
  - dx/build
@@ -553,6 +545,7 @@ files:
553
545
  - lib/brut/spec_support/matchers/have_redirected_to.rb
554
546
  - lib/brut/spec_support/matchers/have_rendered.rb
555
547
  - lib/brut/spec_support/matchers/have_returned_http_status.rb
548
+ - lib/brut/spec_support/matchers/have_returned_rack_response.rb
556
549
  - lib/brut/spec_support/rspec_setup.rb
557
550
  - lib/brut/spec_support/session_support.rb
558
551
  - lib/brut/version.rb