threedotslab/references/rules-domain.md

266 lines
6.4 KiB
Markdown

# Domain Rules (DOM-01..09)
## DOM-01: Private Entity Fields (CRITICAL)
ALL entity struct fields MUST be unexported (lowercase). Entities are "types with behavior," not data bags.
**Check:** Scan all structs in `domain/` for exported fields. Any uppercase field name is a violation.
**Correct:**
```go
type Hour struct {
hour time.Time
availability Availability
}
```
**Wrong:**
```go
type Hour struct {
Hour time.Time // VIOLATION: exported field
Availability Availability // VIOLATION: exported field
}
```
**Exception:** DB model structs in `adapters/` MAY have exported fields for serialization tags.
---
## DOM-02: Factory Constructors (WARNING)
Entities MUST be created through factory constructors, never by direct struct literal.
Pattern: `func New{Type}(args...) (*Type, error)`
The constructor:
- Validates all invariants
- Returns an error if validation fails
- Returns a pointer to the new entity
**Reference:**
```go
func NewAvailableHour(hour time.Time) (*Hour, error) {
if err := validateTime(hour); err != nil {
return nil, err
}
return &Hour{hour: hour, availability: Available}, nil
}
func NewTraining(uuid, userUUID, userName string, trainingTime time.Time) (*Training, error) {
if uuid == "" {
return nil, errors.New("empty training uuid")
}
if userUUID == "" {
return nil, errors.New("empty training user uuid")
}
// ... validate all fields
return &Training{uuid: uuid, userUUID: userUUID, userName: userName, time: trainingTime}, nil
}
```
---
## DOM-03: MustNew Panic Constructors (INFO)
For use in tests and initialization code, provide `MustNew{Type}` that panics on error.
```go
func MustNewFactory(fc FactoryConfig) Factory {
f, err := NewFactory(fc)
if err != nil {
panic(err)
}
return f
}
```
---
## DOM-04: UnmarshalFromDatabase (WARNING)
Entities MUST provide an `Unmarshal{Type}FromDatabase` function for reconstruction from persistence. This function:
- Bypasses normal validation (data was already valid when stored)
- Accepts all fields needed to reconstruct full state
- Is used ONLY by repository adapters
**Reference:**
```go
func UnmarshalHourFromDatabase(hour time.Time, availability Availability) *Hour {
return &Hour{hour: hour, availability: availability}
}
func UnmarshalTrainingFromDatabase(
uuid, userUUID, userName string,
trainingTime time.Time,
notes string,
canceled bool,
proposedNewTime time.Time,
moveProposedBy UserType,
) (*Training, error) {
return &Training{
uuid: uuid, userUUID: userUUID, userName: userName,
time: trainingTime, notes: notes, canceled: canceled,
proposedNewTime: proposedNewTime, moveProposedBy: moveProposedBy,
}, nil
}
```
---
## DOM-05: Value Objects as Structs (CRITICAL)
Value objects MUST be structs wrapping a private field, NOT raw strings, ints, or type aliases.
This ensures they cannot be constructed with arbitrary values — only through validated constructors or predefined constants.
**Correct:**
```go
type Availability struct {
a string // private — cannot be set directly
}
var (
Available = Availability{"available"}
NotAvailable = Availability{"not_available"}
TrainingScheduled = Availability{"training_scheduled"}
)
type UserType struct {
s string
}
var (
Trainer = UserType{"trainer"}
Attendee = UserType{"attendee"}
)
```
**Wrong:**
```go
type Availability string // VIOLATION: can be set to any string
const (
Available Availability = "available"
NotAvailable Availability = "not_available"
)
```
---
## DOM-06: IsZero Method (WARNING)
Value objects and factory structs SHOULD implement `IsZero() bool` to check for zero-value state.
```go
func (a Availability) IsZero() bool {
return a == Availability{}
}
func (f Factory) IsZero() bool {
return f == Factory{}
}
```
---
## DOM-07: Behavior Methods Use Domain Language (CRITICAL)
Entity methods MUST use domain-specific language, NOT generic CRUD terms.
| Forbidden | Use Instead |
|-----------|------------|
| `SetStatus`, `Update` | `ScheduleTraining`, `CancelTraining`, `MakeAvailable` |
| `Create` | `Schedule`, `Register`, `Place`, `Submit` |
| `Delete` | `Cancel`, `Archive`, `Revoke` |
| `Get` | Use query noun phrases |
**Reference:**
```go
func (h *Hour) ScheduleTraining() error {
if !h.IsAvailable() {
return ErrHourNotAvailable
}
h.availability = TrainingScheduled
return nil
}
func (h *Hour) CancelTraining() error { ... }
func (h *Hour) MakeAvailable() error { ... }
func (h *Hour) MakeNotAvailable() error { ... }
func (t *Training) ProposeReschedule(newTime time.Time, proposedBy UserType) error { ... }
func (t *Training) ApproveReschedule(approvedBy UserType) error { ... }
func (t *Training) RejectReschedule() error { ... }
```
---
## DOM-08: String Constructors Validate Input (WARNING)
When a value object can be constructed from a string, use `New{Type}FromString` with validation.
```go
func NewAvailabilityFromString(availabilityStr string) (Availability, error) {
switch availabilityStr {
case "available":
return Available, nil
case "not_available":
return NotAvailable, nil
case "training_scheduled":
return TrainingScheduled, nil
default:
return Availability{}, fmt.Errorf("unknown availability: %s", availabilityStr)
}
}
```
---
## DOM-09: Factory Struct for Complex Creation (INFO)
When entity creation requires configuration or external dependencies, use a Factory struct pattern.
```go
type FactoryConfig struct {
MaxWeeksInTheFutureToSet int
MinUtcHour int
MaxUtcHour int
}
func (c FactoryConfig) Validate() error {
var errs []error
if c.MaxWeeksInTheFutureToSet <= 0 {
errs = append(errs, errors.New("MaxWeeksInTheFutureToSet must be > 0"))
}
// ... more validations
return multierr.Combine(errs...)
}
type Factory struct {
fc FactoryConfig
}
func NewFactory(fc FactoryConfig) (Factory, error) {
if err := fc.Validate(); err != nil {
return Factory{}, err
}
return Factory{fc: fc}, nil
}
func MustNewFactory(fc FactoryConfig) Factory {
f, err := NewFactory(fc)
if err != nil {
panic(err)
}
return f
}
func (f Factory) IsZero() bool {
return f == Factory{}
}
func (f Factory) NewAvailableHour(hour time.Time) (*Hour, error) {
// uses f.fc for validation bounds
}
```