From 8bf6e86ffb60b520cebc2a16b789e2c492854da8 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 30 Aug 2026 09:15:59 +0200 Subject: [PATCH] Implemented the "tagging()" method for the the AnalyticsEvent+Tagging extension in the Infrastructure package. --- Packages/Infrastructure/README.md | 4 +- .../Extensions/AnalyticsEvent+Tagging.swift | 24 +++++++++ .../TrailingSlashRedirectMiddleware.swift | 2 +- .../AnalyticsEventTaggingTests.swift | 49 +++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 Packages/Infrastructure/Sources/Public/Extensions/AnalyticsEvent+Tagging.swift create mode 100644 Packages/Infrastructure/Tests/Cases/Public/Extensions/AnalyticsEventTaggingTests.swift diff --git a/Packages/Infrastructure/README.md b/Packages/Infrastructure/README.md index 50a01f5..2181cf2 100644 --- a/Packages/Infrastructure/README.md +++ b/Packages/Infrastructure/README.md @@ -9,7 +9,7 @@ The shared [Hummingbird](https://github.com/hummingbird-project/hummingbird) too | Pages and assets | `Page`, `Asset`, `AssetExtension`, `FingerprintAssets` | | Link previews | `SocialCard`, its `Image` and `Style`, and the `Tag` meta tags it derives | | Structured data | `StructuredData`, the `Node`, `Property`, and `Value` types of its schema.org graph, the open `Name` and `Kind` vocabularies, and the site-wide initializer building the `Organization`/`WebSite` pair | -| Analytics | `Analytics`, the `Event`s a page reports, the tracker script `Attribute`s it derives, and the `origin` its preconnect hint targets | +| Analytics | `Analytics`, the `Event`s a page reports (with `tagging()` to apply one to a tag), the tracker script `Attribute`s it derives, and the `origin` its preconnect hint targets | | Responses | `CachedHTMLResponse`, `LocalizedHTMLCollectionResponse` | | Contexts | `LocalizedRequestContext` | | Constants | The `HTTPField.Name` header names, `Int.RateLimit` limits, and `String.Security` header values the middlewares default to | @@ -29,7 +29,7 @@ Sources/ ├── Public/ public API │ ├── Builders/ RouteCollectionBuilder │ ├── Enumerations/ AssetExtension -│ ├── Extensions/ addController, plus the default header names, rate limits, and header values +│ ├── Extensions/ addController and Analytics.Event tagging, plus the default header names, rate limits, and header values │ ├── Methods/ FingerprintAssets │ ├── Middlewares/ the seven HTTP middlewares │ ├── Protocols/ Asset, LocalizedRequestContext, Page, RouterController diff --git a/Packages/Infrastructure/Sources/Public/Extensions/AnalyticsEvent+Tagging.swift b/Packages/Infrastructure/Sources/Public/Extensions/AnalyticsEvent+Tagging.swift new file mode 100644 index 0000000..2605648 --- /dev/null +++ b/Packages/Infrastructure/Sources/Public/Extensions/AnalyticsEvent+Tagging.swift @@ -0,0 +1,24 @@ +import Elementary + +public extension Analytics.Event { + + // MARK: Methods + + /// The event's ``attributes`` as attributes of the tag reporting it. + /// + /// ```swift + /// a(.href(url)) { "Listen" } + /// .attributes(contentsOf: Analytics.Event(name: "playlist").tagging()) + /// ``` + /// + /// - Returns: one HTML attribute per event attribute, applied verbatim. + func tagging() -> [HTMLAttribute] { + attributes.map { + .custom( + name: $0.name, + value: $0.value + ) + } + } + +} diff --git a/Packages/Infrastructure/Sources/Public/Middlewares/TrailingSlashRedirectMiddleware.swift b/Packages/Infrastructure/Sources/Public/Middlewares/TrailingSlashRedirectMiddleware.swift index 4a77fcd..19b61e3 100644 --- a/Packages/Infrastructure/Sources/Public/Middlewares/TrailingSlashRedirectMiddleware.swift +++ b/Packages/Infrastructure/Sources/Public/Middlewares/TrailingSlashRedirectMiddleware.swift @@ -3,7 +3,7 @@ import Hummingbird /// Answers a request whose path carries a trailing slash with `301 Moved Permanently` to the same path without one. /// -/// The router matches `/about` and `/about/` alike, and `FileMiddleware` serves `/robots.txt/` as readily as `/robots.txt`, so without this +/// The router matches `/page` and `/page/` alike, and `FileMiddleware` serves `/robots.txt/` as readily as `/robots.txt`, so without this /// every address on the site answers under at least two URLs. A search engine treats those as separate pages competing with each other, and any link /// earned by one is not credited to the other. Redirecting collapses them onto the form the pages already name as canonical. /// diff --git a/Packages/Infrastructure/Tests/Cases/Public/Extensions/AnalyticsEventTaggingTests.swift b/Packages/Infrastructure/Tests/Cases/Public/Extensions/AnalyticsEventTaggingTests.swift new file mode 100644 index 0000000..174225c --- /dev/null +++ b/Packages/Infrastructure/Tests/Cases/Public/Extensions/AnalyticsEventTaggingTests.swift @@ -0,0 +1,49 @@ +import Elementary +import Testing + +@testable import Infrastructure + +@Suite( + "Analytics.Event+Tagging extension", + .tags(.extension) +) +struct AnalyticsEventTaggingTests { + + // MARK: Methods tests + + @Test + func `applies the event name to the tag`() { + let markup = a(.href("/")) { "Listen" } + .attributes(contentsOf: Analytics.Event(name: "instagram").tagging()) + + #expect(markup.render().contains(#"data-umami-event="instagram""#)) + } + + @Test + func `applies every event attribute to the tag`() { + let event = Analytics.Event( + name: "playlist", + properties: ["set": "avc-xi"] + ) + + let markup = button {} + .attributes(contentsOf: event.tagging()) + let rendered = markup.render() + + #expect(rendered.contains(#"data-umami-event="playlist""#)) + #expect(rendered.contains(#"data-umami-event-set="avc-xi""#)) + } + + @Test + func `returns one attribute per event attribute`() { + let event = Analytics.Event( + name: "playlist", + properties: ["set": "avc-xi"] + ) + + let attributes: [HTMLAttribute] = event.tagging() + + #expect(attributes.count == event.attributes.count) + } + +}