Commit Graph

3226 Commits

Author SHA1 Message Date
Yasuo Honda 1c2529b9a6
Merge pull request #50940 from skipkayhil/hm-use-as-test-case
Use ActiveSupport::TestCase for Tracker tests
2024-02-02 08:39:02 +09:00
cjilbert504 9d0a913763
Deprecate passing nil as model arg instead of raising ArgumentError (#50931) 2024-02-01 13:23:03 -08:00
Hartley McGuire 3a3ef7c1ed
Use ActiveSupport::TestCase for Tracker tests
Minitest::Test does not support all of the same options as
ActiveSupport::TestCase, such as running bin/test <filename>:<lineno>
and -n /regex/. Trying to use these options on this file would just run
all of the Minitest::Tests no matter what options were passed.

This commit fixes the ability to use those options by using
ActiveSupport::TestCase (like every other test in the repo).

Before:

```
$ bin/test test/template/dependency_tracker_test.rb:217
Running 59 tests in parallel using 8 processes
Run options: --seed 42725

.........................................................

Finished in 0.322759s, 176.6024 runs/s, 176.6024 assertions/s.
57 runs, 57 assertions, 0 failures, 0 errors, 0 skips
```

After:

```
$ bin/test test/template/dependency_tracker_test.rb:217
Running 59 tests in parallel using 8 processes
Run options: --seed 15213

...

Finished in 0.359162s, 8.3528 runs/s, 8.3528 assertions/s.
3 runs, 3 assertions, 0 failures, 0 errors, 0 skips
```
2024-02-01 15:45:00 -05:00
Petrik de Heus c15658c95c
Merge pull request #50896 from yin0110/fix/button_to_explanation_improve
Improve ActionView::Helpers::UrlHelper button_to explanation [ci-skip]
2024-01-28 15:50:10 +01:00
Yin fd64d16b32 [docs] improve button_to explanation 2024-01-28 22:17:07 +08:00
Rafael Mendonça França 68eade83c8
Merge pull request #50869 from skipkayhil/hm-test-all-ruby-trackers
Ensure all RubyTracker RenderParsers are tested
2024-01-25 19:05:42 -05:00
Sean Doyle 6e1c2f7fbf Mention Strict Locals in more documentation
Motivation / Background
---

Strict Locals support was introduced in [#45727][] and announced as part
of the [7.1 Release][]. There are several mentions across the Guides,
but support is rarely mentioned in the API documentation.

Detail
----

Mention the template short identifier (the pathname, in most cases) as
part of the `ArgumentError` message.

This commit adds two test cases to ensure support for splatting
additional arguments, and for forbidding block and positional arguments.

It also makes mention of strict locals in more places, and links to the
guides.

[#45727]: https://github.com/rails/rails/pull/45727
[7.1 Release]: https://edgeguides.rubyonrails.org/7_1_release_notes.html#allow-templates-to-set-strict-locals
2024-01-25 10:14:44 -05:00
Jean Boussier 6ee0041ed2 Refactor `Module#delegate` inside ActiveSupport::Delegation
This allow to support some extra private features without exposing
them in `Module#delegate`.
2024-01-25 11:51:00 +01:00
Hartley McGuire 0041af4c94
Ensure all RubyTracker RenderParsers are tested
Previously, only the PrismRenderParser or RipperRenderParser would be
tested depending on if the Prism gem is available. This meant that
PrismRenderParser was being tested on Ruby 3.3 and RipperRenderParser
was tested on Ruby < 3.3. Additionally, if someone were to add prism to
the rails/rails Gemfile because they wrote a tool that uses it then the
RipperRenderParser would end up completely untested.

This commit is a small refactor to enable testing both RenderParsers in
all Ruby versions so that the prism gem can be added to the Gemfile.
2024-01-24 19:16:59 -05:00
Petrik de Heus fe81d667a7
Merge pull request #50789 from p8/docs/relative-includes
Use relative includes of README's in documentation [ci-skip]
2024-01-21 18:30:07 +01:00
Jean Boussier 4816684c85
Merge pull request #50737 from skipkayhil/hm-further-optimize-tag-builder
Optimize TagBuilder tag generation
2024-01-18 19:19:27 +01:00
Petrik 8565f45100 Use relative includes of README's in documentation [ci-skip]
The Rails documentation uses the `:include:` directive to inline the
README of the framework into the main documentation page. As the
README's aren't in the root directory from where SDoc is run we need to
add the framework path to the include:

    # :include: activesupport/README.md

This results in a warning when installing the gems as generating the rdoc for the gem is run from the gem/framework root:

    Couldn't find file to include 'activesupport/README.rdoc' from lib/active_support.rb

The `:include:` RDoc directive supports includes relative to the current
file as well:

    # :include: ../README.md

This makes sure it works for the Rails API docs and the separate gems.

Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
2024-01-18 10:39:15 +01:00
Robert Fletcher 929f9fd3fe Fix threading issue with strict locals
Fixes #50774

When the server boots up, 2 threads hit the same `UnboundTemplate`
instance before it has set up `@templates`. Both threads get past the
`unless template = @templates[locals]` check because
`@templates[locals]` isn't set yet. However, with `@write_lock`, one
thread waits while the other one proceeds, setting `@templates` to a
frozen hash. The second thread then gets the write lock and tries to
modify `@templates` but it has been frozen.
2024-01-16 16:27:58 -08:00
Hartley McGuire 597b56cde0
Optimize TagBuilder tag generation
Currently there's about a 35% difference between tags generated using
the `TagBuilder` and tags generated by passing a positional argument to
`#tag`.

This commit optimizes `TagBuilder` to reduce that difference down to 13%.

The first change is to perform less hash allocations by not splatting
the options twice in the `TagBuilder` (one at the `tag.a` invocation,
and one at `tag_string`). The extra splat for `tag_string` was moved
into `method_missing` since that is the only other caller of this
private method.

The other change is to only escape the content in `tag_string` if it a
non-empty.

Additionally, a test was tweaked to ensure that passing `options` to a
`self_closing_element` is tested as it was previously not.

Benchmark:

```
require "action_view"
require "benchmark/ips"

class Foo
  include ActionView::Helpers
end

helpers = Foo.new

Benchmark.ips do |x|
  x.report("tag") { helpers.tag("a", href: "foo") }
  x.report("tag_builder") { helpers.tag.a(href: "foo") }
  x.compare!
end
```

Before:

```
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin22]
Warming up --------------------------------------
                 tag    67.180k i/100ms
         tag_builder    50.267k i/100ms
Calculating -------------------------------------
                 tag    673.064k (± 0.4%) i/s -      3.426M in   5.090520s
         tag_builder    504.971k (± 0.4%) i/s -      2.564M in   5.076842s

Comparison:
                 tag:   673063.7 i/s
         tag_builder:   504971.4 i/s - 1.33x  slower
```

After:

```
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin22]
Warming up --------------------------------------
                 tag    67.374k i/100ms
         tag_builder    59.702k i/100ms
Calculating -------------------------------------
                 tag    670.837k (± 0.4%) i/s -      3.369M in   5.021714s
         tag_builder    592.727k (± 1.3%) i/s -      2.985M in   5.037088s

Comparison:
                 tag:   670836.6 i/s
         tag_builder:   592726.7 i/s - 1.13x  slower
```

Co-authored-by: Sean Doyle <seanpdoyle@users.noreply.github.com>
2024-01-16 17:51:21 -05:00
Jean Boussier 946e46ebcc Modernize method missing implementations
`...` is both simpler an more correct since the keyword argument
separation.
2024-01-16 13:17:45 +01:00
Jean Boussier 6a9795d4d2
Merge pull request #50751 from skipkayhil/hm-av-silenceable-start
Add silenced? for Action View Start subscriber
2024-01-16 12:37:14 +01:00
Hartley McGuire d255ffd035
Add silenced? for Action View Start subscriber
Previously, the `render_template.action_view` and
`render_layout.action_view` events would always be handled as if they
had subscribers even if the log level of its subscribers result in
nothing being logged. For regular `LogSubscriber`s,
`subscribe_log_level` could be used to optimize these cases but the
Start subscriber is not a subclass of `LogSubscriber`.

This commit implements a `#subscribed?` method for the Start subscriber
so that it can also benefit from the `subscribe_log_level` optimization.
2024-01-15 15:46:24 -05:00
Sean Doyle a93d1e39cc Re-word #49856 CHANGELOG entry
Follow-up to [#49856][]

Replace shell word expansion with full English. In addition to that
change, elaborate on the changes with more detailed explanation.

[#49856]: https://github.com/rails/rails/pull/49856#discussion_r1451634684
2024-01-14 15:18:45 -05:00
Jonathan Hefner 91968e5a18 Do not mask NoMethodError for render_in in render_in
Follow-up to #50699.

This prevents a `NoMethodError` from being masked when the missing
method is itself named `render_in`.

Co-authored-by: Hartley McGuire <skipkayhil@gmail.com>
2024-01-10 17:26:27 -06:00
Jonathan Hefner 952a13b0ac Do not mask NoMethodError from within render_in
Follow-up to #50665.

Unconditionally converting `NoMethodError` to `ArgumentError` can mask a
legitimate `NoMethodError` from within the `render_in` method.  This
commit adds a check to prevent that.
2024-01-10 13:37:37 -06:00
Sean Doyle b3bb06a24a Raise `ArgumentError` if `:renderable` object does not respond to `#render_in`
When calling `render` with a `:renderable` argument, ensure that the
object responds to `#render_in`. If it doesn't, raise an
`ArgumentError`.

This commit also adjusts the `ArgumentError` that when a `:partial`
argument isn't Active Model compatible. Prior to this commit, the
message used `:` as a prefix to `to_partial_path`. This commit replaces
that with a `#` prefix to denote that it's expected to be an instance
method on the object.
2024-01-09 16:01:04 -05:00
Rafael Mendonça França 84f773f9d1
Merge pull request #50622 from seanpdoyle/document-render-in-examples
Document rendering `:renderable` and `#render_in`
2024-01-08 16:51:39 -05:00
Jonathan Hefner 3bbf21c343 Use verb form of "fallback"
"Fallback" is a noun, whereas "fall back" is a verb.
2024-01-07 17:27:23 -06:00
Jonathan Hefner 5cc2f8af3b Autolink AV::Helpers::SanitizeHelper#sanitize [ci-skip] 2024-01-07 17:27:23 -06:00
Jonathan Hefner e8656f8c28 Clean up AV::Helpers::SanitizeHelper#sanitize doc [ci-skip] 2024-01-07 17:27:23 -06:00
Jonathan Hefner d1411b2018 Split up code blocks for multi-file examples [ci-skip]
RDoc treats consecutive indented lines as a single code block.  For code
examples that span multiple files / languages, this confuses the syntax
highlighter and makes the examples harder to read.  Unfortunately, RDoc
doesn't provide syntax to prevent this, and it ignores multiple
consecutive blank lines.  However, by inserting an empty tag such as
`<code></code>`, we can force RDoc to recognize separate code blocks.
2024-01-07 17:27:23 -06:00
Jean Boussier c0b5052d92
Merge pull request #50609 from ricardotk002/use-array-intersect
Replace usage of `Array#?` with `Array#intersect?` for efficiency
2024-01-07 21:15:56 +01:00
Akhil G Krishnan 84840fbedb Document nonce option in stylesheet_link_tag api documentation 2024-01-07 09:37:24 +05:30
Jonathan Hefner 1743d7ad38 Point AV::Helpers::NumberHelper method docs to AS [ci-skip]
`ActionView::Helpers::NumberHelper` methods are thin wrappers over
`ActiveSupport::NumberHelper` methods, which are well documented.  Thus
this commit points `ActionView::Helpers::NumberHelper` method docs to
`ActiveSupport::NumberHelper` methods instead of duplicating those docs.
2024-01-06 18:07:26 -06:00
Sean Doyle 4117583e4b Document rendering `:renderable` and `#render_in`
Provide examples for rendering objects that respond to `render_in`. Also
highlight that the object can also define a `#format` method to control
how the rendered String should be treated.

Add test coverage for both Action View's and Action Pack's support for
`render` with `:renderable` options.
2024-01-06 17:57:02 -05:00
Ricardo Díaz de154095ed Replace usage of `Array#?` with `Array#intersect?` for efficiency
`Array#intersect?` was introduced in Ruby 3.1.0 and it's more efficient and
useful when the result of the intersection is not needed as the
following benchmarks show:

```
require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails", path: "./"
  # If you want to test against edge Rails replace the previous line with this:
  # gem "rails", github: "rails/rails", branch: "main"
  gem "benchmark-ips"
end

require "active_support"

SCENARIOS = [
  [(1..100).to_a, (90..200).to_a],    # Case 1
  [("a".."m").to_a, ("j".."z").to_a], # Case 2
  [(1..100).to_a, (101..200).to_a],   # Case 3
]

SCENARIOS.each_with_index do |values, n|
  puts
  puts " Case #{n + 1} ".center(80, "=")
  puts
  Benchmark.ips do |x|
    x.report("Array#?") { !(values[0] & values[1]).empty? }
    x.report("Array#intersect?")      { values[0].intersect?(values[1]) }
    x.compare!
  end
end
```

Results:

```
==================================== Case 1 ====================================

ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin21]
Warming up --------------------------------------
             Array#?    34.221k i/100ms
    Array#intersect?    62.035k i/100ms
Calculating -------------------------------------
             Array#?    343.119k (± 1.1%) i/s -      1.745M in   5.087078s
    Array#intersect?    615.394k (± 1.1%) i/s -      3.102M in   5.040838s

Comparison:
    Array#intersect?:   615393.7 i/s
             Array#?:   343119.4 i/s - 1.79x  slower

==================================== Case 2 ====================================

ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin21]
Warming up --------------------------------------
             Array#?   103.256k i/100ms
    Array#intersect?   185.104k i/100ms
Calculating -------------------------------------
             Array#?      1.039M (± 1.3%) i/s -      5.266M in   5.066847s
    Array#intersect?      1.873M (± 1.6%) i/s -      9.440M in   5.041740s

Comparison:
    Array#intersect?:  1872932.7 i/s
             Array#?:  1039482.4 i/s - 1.80x  slower

==================================== Case 3 ====================================

ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin21]
Warming up --------------------------------------
             Array#?    37.070k i/100ms
    Array#intersect?    41.438k i/100ms
Calculating -------------------------------------
             Array#?    370.902k (± 0.8%) i/s -      1.891M in   5.097584s
    Array#intersect?    409.902k (± 1.0%) i/s -      2.072M in   5.055185s

Comparison:
    Array#intersect?:   409901.8 i/s
             Array#?:   370902.3 i/s - 1.11x  slower
```
2024-01-05 15:15:30 -05:00
Rafael Mendonça França 8c4af05a38
Merge pull request #50591 from akhilgkrishnan/add-nonce-stylesheet-link-tag
Add the nonce: true option for stylesheet_link_tag helper
2024-01-05 10:29:13 -05:00
Rafael Mendonça França e7cf982700
Merge pull request #50588 from sato11/remove-actionview-renderer-render-template
Remove `ActionView::Renderer#render_template`
2024-01-05 10:06:20 -05:00
Jean Boussier 27140247c2 Cleanup `defined?` usage
Now that we dropped support for Ruby 2.7, we no longer
need to check if variables are defined before accessing them
to avoid the undefined variable warning.
2024-01-05 15:05:35 +01:00
Jean Boussier ef65e5fb32 Cleanup usage of ruby2_keywords
Now that we no longer support Ruby 2.7, many `ruby2_keyword` calls
can be eliminated.

The ones that are left could be eliminated but would end up substantially
slower or more compliacated so I left them for now.
2024-01-05 14:40:18 +01:00
Akhil G Krishnan 7fa6d15890 Add the nonce: true option for stylesheet_link_tag helper
This provides a shortcut for setting a Content Security Policy nonce on
a stylesheet_link_tag.

Co-authored-by: AJ Esler <ajesler@users.noreply.github.com>
2024-01-05 13:02:51 +05:30
Junichi Sato 2af3d84987
Remove `ActionView::Renderer#render_template`
This seems to be making a pair with `#render_partial` but in reality
it's no longer refered to from anywhere. Since it is marked with
nodoc, I propose to get rid of it.

As far as I can tell from my non-comprehensive research,
this method was introduced in the commit b735761,
became practically private in f984907 and unused in 1bc0a59.
2024-01-05 10:50:50 +09:00
Rafael Mendonça França 64c93600bd
Merge pull request #50584 from seanpdoyle/action-view-rendered-html-parser
Parse `ActionView::TestCase#rendered` as DocumentFragment
2024-01-04 17:38:53 -05:00
Petrik de Heus 0689b933c4
Merge pull request #50464 from vlado/round_mode_missing_mode
Document `round_mode` option in action view's number helper [ci skip]
2024-01-04 21:33:31 +01:00
Sean Doyle bec80373cd Parse `ActionView::TestCase#rendered` as DocumentFragment
To integrate with [rails-dom-testing][] and its selector assertions,
`ActionView::TestCase` [defines a `#document_root_element`
method][document_root_element] that parses the HTML into a fully valid
HTML document and returns the "root".

In the case of most Action View partials rendered with `render partial:
"..."`, the resulting document would be invalid, so its constituent
parts (its `<html>`, `<head>`, and `<body>` elements) are synthesized in
during the parsing process. This results in a document whose _contents_
are equivalent to the original HTML string, but whose structure is not.

To share a concrete example:

  ```ruby
  irb(main):002:0> rendered = "<h1>Hello world</h1><h2>Goodbye world</h2>"
  => "<h1>Hello world</h1><h2>Goodbye world</h2>"
  irb(main):003:0> root = Rails::Dom::Testing.html_document.parse(rendered).root
  =>
  #(Element:0x57080 {
  ...
  irb(main):004:0> rendered.to_s
  => "<h1>Hello world</h1><h2>Goodbye world</h2>"
  irb(main):005:0> root.to_s
  => "<html><head></head><body><h1>Hello world</h1><h2>Goodbye world</h2></body></html>"
  irb(main):006:0> rendered.to_s == root.to_s
  => false
  ```

Prior to this commit, the parsed HTML content returned from calling
`rendered.html` relied on the same mechanisms as
`#document_root_element`, and parsed the HTML fragment into a full
document, with a synthesized `<html>` element as its root. The
`rendered.html` value should reflect the content that was **rendered**
by the partial, and should not behave the same as
`#document_root_element`.

When the parsing class is changed from [Nokogiri::XML::Document][] to
[Nokogiri::XML::DocumentFragment][], the returned value reflects the
same **exact** content as what was rendered.

To elaborate on the previous example:

  ```ruby
  irb(main):007:0> fragment = Rails::Dom::Testing.html_document_fragment.parse(rendered)
  =>
  #(DocumentFragment:0x62ee4 {
  ...
  irb(main):008:0> fragment.to_s
  => "<h1>Hello world</h1><h2>Goodbye world</h2>"
  irb(main):009:0> rendered.to_s == fragment.to_s
  => true
  ```

This commit changes the default `rendered.html` behavior to rely on
`Nokogiri::XML::DocumentFragment` instead of `Nokogiri::XML::Document`.

[Nokogiri::XML::Document]: https://nokogiri.org/rdoc/Nokogiri/XML/Document.html
[Nokogiri::XML::DocumentFragment]: https://nokogiri.org/rdoc/Nokogiri/XML/DocumentFragment.html
[document_root_element]: https://github.com/rails/rails-dom-testing/blob/v2.2.0/lib/rails/dom/testing/assertions/selector_assertions.rb#L75
2024-01-04 14:49:51 -05:00
Sean Doyle cebbd1cf0a Rename `ActionView::TestCase::Behavior::{Content,RenderedViewContent}`
Closes #49818

Renames `ActionView::TestCase::Behavior::Content` to
`RenderedViewContent`, with the goal of making it more of an internal
implementation detail that's unlikely to collide with an
application-side `::Content` class.

The `RenderedView`-prefix mirrors the module's `RenderedViewsCollection`
class. Since the intention is to treat it as a private implementation
detail, `RenderedViewContent` is marked with `:nodoc:`.

Along with the rename, this commit also modifies the class inheritance,
replacing the `SimpleDelegator` superclass with `String`. [String.new][]
accepts a `String` positional argument in the same way as
`SimpleDelegator.new` accepts a delegate object positional argument.
Sharing the `String` superclass also makes it a good candidate for being
passed to [Capybara.string][] (and [Capybara::Node::Simple.new][]) like
the documentation suggests.

[Capybara.string]: https://github.com/teamcapybara/capybara/blob/3.39.2/lib/capybara.rb#L212-L242
[Capybara::Node::Simple.new]: https://github.com/teamcapybara/capybara/blob/3.39.2/lib/capybara/node/simple.rb#L23
[String.new]: https://ruby-doc.org/core/String.html#method-c-new
2024-01-04 14:35:28 -05:00
Rafael Mendonça França 4544b0d15b
Merge pull request #49943 from cjilbert504/handle-nil-form-model-object
Handle nil form_with model argument
2024-01-03 22:02:41 -05:00
Junichi Sato 994aeacddf
`#render_template_to_object` and `#render_partial_to_object` are private
Apparently it had not been clear what visibility they should assume
when they were initially created at https://github.com/rails/rails/pull/35265,
then a big refactoring took place at https://github.com/rails/rails/pull/38594
after which they remain intact.

So I think they can now become properly private as they are nodoc and
not refered to but from within the same class.
2024-01-04 10:50:08 +09:00
Rafael Mendonça França 8b4e92f4be
Point rubocop to ruby 3.1 2024-01-03 19:02:32 +00:00
Rafael Mendonça França 9d18dc8505
Remove all code to work with Ruby < 3.1 2024-01-03 19:02:31 +00:00
Rafael Mendonça França 664eb0dfc4
Merge pull request #50473 from seanpdoyle/action-text-content-pattern-matching
Delegate `ActionText::Content#deconstruct` to Nokogiri
2024-01-03 12:31:22 -05:00
David Heinemeier Hansson 8397eb24da
Remove rollup and test machinery for rails-ujs (#50535)
This leaves only the final compiled targets in place.
2024-01-02 16:49:36 +01:00
Hartley McGuire 90da071bb6
bundle update rubocop --conservative (#50515)
Also perform two autocorrects with `bundle exec rubocop -A`:

- fixes a new case of [`Style/RedundantReturn`][1]
- fixes a new case of [`Performance/StringInclude`][2]

[1]: 146b1c2e33
[2]: 3158bbb9f6

Co-authored-by: David Heinemeier Hansson <david@basecamp.com>
2024-01-02 12:49:36 +01:00
Jean Boussier 6ba2fdb2fe Bump the required Ruby version to 3.1.0
Until now, Rails only droped compatibility with older
rubies on new majors, but I propose to change this policy
because it causes us to either keep compatibility with long
EOLed rubies or to bump the Rails major more often, and to
drop multiple Ruby versions at once when we bump the major.

In my opinion it's a bad alignments of incentives. And we'd
be much better to just drop support in new minors whenever they
go EOL (so 3 years).

Also Ruby being an upstream dependency, it's not even
a semver violation AFAICT.

Since Rails 7.2 isn't planned before a few months, we
can already drop Ruby 3.0 as it will be EOL in March.
2023-12-31 08:54:03 +01:00
Sean Doyle 2e15010d56 Delegate `ActionText::Content#deconstruct` to Nokogiri
Since `ActionText::Content` wraps an `ActionText::Fragment`, and
`ActionText::Fragment` wraps a `Nokogiri::XML::DocumentFragment`, then
`ActionText::Content` should be able to rely on the newer Ruby pattern
matching introduced by [nokogiri@1.16.0][] (mainly the
[DocumentFragment#deconstruct][] method):

```ruby
content = ActionText::Content.new <<~HTML
  <h1>Hello, world</h1>

  <div>The body</div>
HTML

content => [h1, div]

assert_pattern { h1 => { content: "Hello, world" } }
assert_pattern { div => { content: "The body" } }
```

The implementation change relies on delegating from `Content` to
`Fragment`, and from `Fragment` to `DocumentFragment#elements` (to
deliberately exclude text nodes).

[nokogiri@1.16.0]: https://nokogiri.org/CHANGELOG.html?h=pattern
[DocumentFragment#deconstruct]: https://nokogiri.org/rdoc/Nokogiri/XML/DocumentFragment.html?h=deconstruct#method-i-deconstruct
2023-12-28 09:19:18 -05:00