70: Refactor the notification framework into transport-independent notification services

This commit is contained in:
2026-08-11 20:03:06 +02:00
parent b33db6da49
commit df504e29bf
25 changed files with 522 additions and 168 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-11
@@ -0,0 +1,54 @@
## Context
See `proposal.md` for motivation. The current notification framework combines an API package and three implementation packages under `com.r35157.libs.notification`; its generic message wrappers leak transport choices into consumers. The code currently has no external production consumer, but each implementation contains an inline compile-time usage example. The separate alarm-specific Pushover action is intentionally outside this module and must remain unchanged.
The repository temporarily hosts immature API and implementation artifacts together. Package ownership must nevertheless express a notification-service module that can later be extracted, with API types separated from transport implementations.
## Goals / Non-Goals
**Goals:**
- Establish a small stable API package containing only the bound service, addressed service, and destination abstraction.
- Make transport implementations consume the three existing String-backed ValueTags directly.
- Preserve the existing Pushover HTTP request and synchronous error behavior while enforcing message validation before transport work.
- Make SMTP's complete destination configuration immutable and bindable before injection into a consumer.
**Non-Goals:**
- Implementing Discord or SMTP delivery.
- Adding notification configuration, asynchronous delivery, retries, queues, persistence, or timeout policies.
- Refactoring the alarm-specific Pushover implementation or changing `NenjimHubImpl.startAutoRunProcesses()`.
- Adding or modifying automated tests.
## Decisions
### Separate API and transport implementation packages under the service owner
The API will live directly in `com.r35157.service.notification`, while Discord, Pushover, and SMTP remain in `impl.discord`, `impl.pushover`, and `impl.smtp` below it. This follows the repository's future module boundaries while keeping the current single-project layout. Retaining aliases in the old package was rejected because it would prolong the incompatible abstraction and violates the issue's clean migration requirement.
### Represent messages directly with the semantic ValueTag
All push contracts use `ΩNotificationMessageΩ` directly. Validation is performed at each concrete public push entry point before any delivery work. A replacement record, marker hierarchy, or transport generic was rejected because it would recreate the coupling the change removes.
### Keep addressed binding minimal and destination-only
`AddressedNotificationService<D>` captures a destination in its default `bind(D)` result. `SMTPDestination` captures both email address and subject and validates both in its compact constructor. `SMTPNotificationService` adds a convenience overload accepting these two values and delegates to the standard destination binding. Keeping the subject on each message was rejected because it would leave the service only partially configured when injected.
### Preserve transport maturity independently
Pushover keeps its current form encoding, endpoint, synchronous HTTP call, status handling, and `IOException` contract. Discord and SMTP validate message input first and then retain their `UnsupportedOperationException` behavior for otherwise valid messages. This prevents a refactor from silently expanding either transport's behavior.
## Risks / Trade-offs
- **[Breaking source migration]** All old names and packages disappear at once. → Migrate every production reference and inline example in the same change, then structurally search for old names.
- **[Validation duplicated across implementations]** Each implementation owns a small null/blank guard. → Prefer explicit local validation over adding an unnecessary public utility or runtime message wrapper; keep the exception type and condition identical.
- **[No automated regression coverage]** The issue explicitly excludes test changes. → Compile both source sets, run the normal assembly, and perform strict structural checks of packages, types, call sites, and generated-source boundaries.
## Migration Plan
1. Introduce the renamed API and transport implementations in the new package using the existing ValueTags.
2. Update all production imports and inline examples to the new contracts.
3. Remove the complete old package and every obsolete notifier/message type without aliases.
4. Compile and assemble from `.tjava`, validate OpenSpec strictly, and verify the source tree contains no stale API references.
Rollback consists of reverting the complete source and OpenSpec change together; the old and new APIs are intentionally not supported in parallel.
@@ -0,0 +1,25 @@
## Why
The existing notification API exposes transport-specific message types to consumers, so a fully configured notification sender cannot be substituted without changing consumer code. Before adding further transports, the framework needs a stable service API in which configuration is bound at composition time and consumers provide only a semantically tagged text message.
## What Changes
- **BREAKING** Relocate the notification API and implementations from `com.r35157.libs.notification` to `com.r35157.service.notification`, without compatibility aliases.
- **BREAKING** Replace `BoundNotifier` and `AddressedNotifier` with transport-independent `BoundNotificationService` and destination-only generic `AddressedNotificationService` contracts.
- **BREAKING** Remove the marker and transport-specific message wrappers in favor of `ΩNotificationMessageΩ` directly.
- Rename the Pushover, Discord, and SMTP implementations to notification-service terminology while preserving their current delivery status.
- Validate every notification message before delivery, and make SMTP binding capture both a validated email address and fixed subject.
## Capabilities
### New Capabilities
- `notification-services`: Defines the transport-independent notification API, binding and validation behavior, and the required behavior of the existing Pushover, Discord, and SMTP implementations.
### Modified Capabilities
None.
## Impact
The change replaces the public notification package and its API types, updates all production imports and inline examples, and removes obsolete message classes. Pushover's synchronous HTTP delivery remains functional; Discord webhook and SMTP delivery remain unimplemented. Alarm-specific Pushover code and Nenjim startup composition are unaffected.
@@ -0,0 +1,78 @@
## Purpose
Defines transport-independent notification services whose delivery configuration is bound before consumers submit semantically tagged text messages.
## ADDED Requirements
### Requirement: Transport-independent bound notification contract
The notification API SHALL provide a non-generic bound service under `com.r35157.service.notification` whose only delivery input is an `ΩNotificationMessageΩ` and whose synchronous failures use `IOException`. A consumer using an already bound service SHALL NOT need a transport-specific message type or an implementation-package dependency.
#### Scenario: Bound consumer sends a message
- **WHEN** a consumer receives a configured `BoundNotificationService`
- **THEN** it can call `push(message)` using only an `ΩNotificationMessageΩ`
#### Scenario: Delivery failure is reported synchronously
- **WHEN** a bound transport encounters a delivery failure
- **THEN** `push(message)` reports the failure through its `IOException` contract
### Requirement: Addressed services bind destination configuration
The notification API SHALL provide an `AddressedNotificationService` generic only over a `NotificationDestination` type. Binding a destination SHALL produce a `BoundNotificationService` that captures the destination so later calls require only an `ΩNotificationMessageΩ`.
#### Scenario: Destination is bound once
- **WHEN** a destination is passed to an addressed notification service's binding operation
- **THEN** the returned bound service sends subsequent messages to that captured destination without requiring it again
### Requirement: Notification message validation
Every public notification-service `push(...)` entry point SHALL reject a null, empty, or blank `ΩNotificationMessageΩ` with `IllegalArgumentException` before attempting any network or transport operation.
#### Scenario: Null message is rejected
- **WHEN** a caller pushes a null notification message
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
#### Scenario: Empty or blank message is rejected
- **WHEN** a caller pushes an empty or whitespace-only notification message
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
### Requirement: Pushover delivery remains functional
The Pushover implementation SHALL accept `ΩNotificationMessageΩ` directly, preserve its existing synchronous HTTP delivery behavior, and expose it as `PushoverNotificationService` implementing `BoundNotificationService`.
#### Scenario: Valid Pushover message is submitted
- **WHEN** a caller pushes a valid message through a configured Pushover notification service
- **THEN** the service submits the same message through the existing Pushover API request flow
#### Scenario: Pushover rejects delivery
- **WHEN** Pushover returns a non-successful HTTP response or communication fails
- **THEN** the service reports the delivery failure as an `IOException`
### Requirement: SMTP binding captures address and subject
An SMTP destination SHALL contain both an `ΩEmailAddressΩ` and a fixed `ΩEmailSubjectΩ`. SMTP SHALL support binding these two values into a `BoundNotificationService`, and SHALL reject a null, empty, or blank address or subject with `IllegalArgumentException` when the destination is created.
#### Scenario: SMTP service is fully bound
- **WHEN** a caller binds a valid email address and subject
- **THEN** the returned bound service requires only an `ΩNotificationMessageΩ` for each push
#### Scenario: Invalid SMTP binding is rejected
- **WHEN** a caller creates or requests an SMTP binding with a null, empty, or blank email address or subject
- **THEN** the operation throws `IllegalArgumentException`
#### Scenario: Valid SMTP delivery remains unimplemented
- **WHEN** a valid message is pushed through a valid SMTP binding
- **THEN** the service reports that SMTP delivery is not implemented
### Requirement: Discord delivery remains unimplemented
The Discord stub SHALL implement `BoundNotificationService`, accept `ΩNotificationMessageΩ` directly, and SHALL NOT implement webhook delivery as part of this capability.
#### Scenario: Valid Discord delivery remains unimplemented
- **WHEN** a valid message is pushed through the configured Discord service
- **THEN** the service reports that Discord delivery is not implemented without performing webhook delivery
### Requirement: Obsolete notification API is removed
The notification framework SHALL reside exclusively under `com.r35157.service.notification`. It SHALL NOT retain compatibility aliases under `com.r35157.libs.notification`, notifier-named public interfaces or implementations, the `NotificationMessage` marker, or transport-specific message wrapper types.
#### Scenario: Production code uses the new service API
- **WHEN** the production source tree is inspected after migration
- **THEN** notification framework code and consumers reference only the new package and service-oriented types
#### Scenario: Removed types are unavailable
- **WHEN** downstream code attempts to import an old notifier or message-wrapper type
- **THEN** that type is no longer part of the source or compiled API
@@ -0,0 +1,17 @@
## 1. Notification API Migration
- [x] 1.1 Add the documented `BoundNotificationService`, `AddressedNotificationService`, and `NotificationDestination` API under `com.r35157.service.notification`.
- [x] 1.2 Remove the old API package, marker interface, notifier contracts, and all compatibility aliases.
## 2. Transport Implementations
- [x] 2.1 Migrate and rename Pushover to `PushoverNotificationService`, accept and validate `ΩNotificationMessageΩ`, and preserve the existing delivery flow.
- [x] 2.2 Migrate and rename Discord to `DiscordNotificationService`, validate direct messages, and preserve its unimplemented delivery status.
- [x] 2.3 Migrate SMTP to `SMTPNotificationService` with a validated address-and-subject `SMTPDestination`, direct binding convenience, direct message validation, and unimplemented delivery status.
- [x] 2.4 Update every production call site and inline example, then confirm the old package and removed types have no remaining production references.
## 3. Verification
- [x] 3.1 Compile main and test source sets and complete the repository's normal assembly/build without changing tests.
- [x] 3.2 Strictly validate the OpenSpec change and run whitespace/diff checks.
- [x] 3.3 Review the complete diff for scope, generated-source changes, test changes, transport-independent consumer dependencies, and preservation of excluded services.
@@ -0,0 +1,80 @@
# notification-services Specification
## Purpose
Defines transport-independent notification services whose delivery configuration is bound before consumers submit semantically tagged text messages.
## Requirements
### Requirement: Transport-independent bound notification contract
The notification API SHALL provide a non-generic bound service under `com.r35157.service.notification` whose only delivery input is an `ΩNotificationMessageΩ` and whose synchronous failures use `IOException`. A consumer using an already bound service SHALL NOT need a transport-specific message type or an implementation-package dependency.
#### Scenario: Bound consumer sends a message
- **WHEN** a consumer receives a configured `BoundNotificationService`
- **THEN** it can call `push(message)` using only an `ΩNotificationMessageΩ`
#### Scenario: Delivery failure is reported synchronously
- **WHEN** a bound transport encounters a delivery failure
- **THEN** `push(message)` reports the failure through its `IOException` contract
### Requirement: Addressed services bind destination configuration
The notification API SHALL provide an `AddressedNotificationService` generic only over a `NotificationDestination` type. Binding a destination SHALL produce a `BoundNotificationService` that captures the destination so later calls require only an `ΩNotificationMessageΩ`.
#### Scenario: Destination is bound once
- **WHEN** a destination is passed to an addressed notification service's binding operation
- **THEN** the returned bound service sends subsequent messages to that captured destination without requiring it again
### Requirement: Notification message validation
Every public notification-service `push(...)` entry point SHALL reject a null, empty, or blank `ΩNotificationMessageΩ` with `IllegalArgumentException` before attempting any network or transport operation.
#### Scenario: Null message is rejected
- **WHEN** a caller pushes a null notification message
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
#### Scenario: Empty or blank message is rejected
- **WHEN** a caller pushes an empty or whitespace-only notification message
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
### Requirement: Pushover delivery remains functional
The Pushover implementation SHALL accept `ΩNotificationMessageΩ` directly, preserve its existing synchronous HTTP delivery behavior, and expose it as `PushoverNotificationService` implementing `BoundNotificationService`.
#### Scenario: Valid Pushover message is submitted
- **WHEN** a caller pushes a valid message through a configured Pushover notification service
- **THEN** the service submits the same message through the existing Pushover API request flow
#### Scenario: Pushover rejects delivery
- **WHEN** Pushover returns a non-successful HTTP response or communication fails
- **THEN** the service reports the delivery failure as an `IOException`
### Requirement: SMTP binding captures address and subject
An SMTP destination SHALL contain both an `ΩEmailAddressΩ` and a fixed `ΩEmailSubjectΩ`. SMTP SHALL support binding these two values into a `BoundNotificationService`, and SHALL reject a null, empty, or blank address or subject with `IllegalArgumentException` when the destination is created.
#### Scenario: SMTP service is fully bound
- **WHEN** a caller binds a valid email address and subject
- **THEN** the returned bound service requires only an `ΩNotificationMessageΩ` for each push
#### Scenario: Invalid SMTP binding is rejected
- **WHEN** a caller creates or requests an SMTP binding with a null, empty, or blank email address or subject
- **THEN** the operation throws `IllegalArgumentException`
#### Scenario: Valid SMTP delivery remains unimplemented
- **WHEN** a valid message is pushed through a valid SMTP binding
- **THEN** the service reports that SMTP delivery is not implemented
### Requirement: Discord delivery remains unimplemented
The Discord stub SHALL implement `BoundNotificationService`, accept `ΩNotificationMessageΩ` directly, and SHALL NOT implement webhook delivery as part of this capability.
#### Scenario: Valid Discord delivery remains unimplemented
- **WHEN** a valid message is pushed through the configured Discord service
- **THEN** the service reports that Discord delivery is not implemented without performing webhook delivery
### Requirement: Obsolete notification API is removed
The notification framework SHALL reside exclusively under `com.r35157.service.notification`. It SHALL NOT retain compatibility aliases under `com.r35157.libs.notification`, notifier-named public interfaces or implementations, the `NotificationMessage` marker, or transport-specific message wrapper types.
#### Scenario: Production code uses the new service API
- **WHEN** the production source tree is inspected after migration
- **THEN** notification framework code and consumers reference only the new package and service-oriented types
#### Scenario: Removed types are unavailable
- **WHEN** downstream code attempts to import an old notifier or message-wrapper type
- **THEN** that type is no longer part of the source or compiled API