mirror of
https://github.com/napnap75/multiarch-docker-images.git
synced 2026-11-11 00:22:12 +01:00
refactor(config): rename the period helper to frPeriod
The helper renders French period labels (Janvier 2026, T2 2026, S1 2026), so it carries the fr prefix like frDate. Config example, arch.md and user-stories.md updated. Configs calling period(...) must switch to frPeriod(...).
This commit is contained in:
+2
-2
@@ -173,13 +173,13 @@ A `config.jsonc` file kept alongside the binary, reloaded on restart. Three sect
|
||||
- `delete`: list of field names to unset (they become undefined again for later rules and template rendering)
|
||||
- `rules`: child rules, evaluated in order only if the parent `when` matched; nesting is recursive
|
||||
|
||||
The application registers two built-in helpers in the expression environment. `firstMatch(regex, texts?)` returns the first match of the regex in the document text (first capture group when present), or an empty string when no match is found (parsing an empty string with `date()` is then a rule error and routes the document to `pending/`); the optional second argument is an expression evaluated to the text(s) to search — a single page (e.g. `pages[0]`, the first page), a slice (e.g. `pages[0:3]`, pages 1 to 3, expr's standard slice syntax, end exclusive), or any list of strings — and when omitted the whole document is searched (e.g. `firstMatch(`[0-9]{2}/[0-9]{2}/[0-9]{4}`, pages[0])`). `period(value, unit, shift, layout?)` renders a period instead of a raw date: `value` is a native date (e.g. `emissionDate`), `unit` is `month` | `quarter` | `half` | `year`, `shift` is `-1` (previous period), `0` (period containing the date) or `+1` (next period) — e.g. a rent receipt issued mid-month uses the containing month, a service-charge call issued at the end of a quarter uses the next quarter, a bank statement uses the previous month. It returns a French string: `Janvier 2026`, `T2 2026`, `S1 2026` or `2026`. The optional `layout` argument is a standard Go reference-date layout applied to the period's first day, with month names rendered in French by the application (default layouts: `January 2006` for `month`, `2006` for `year`); it is ignored for `quarter` and `half`, whose formats (`T<quarter> <year>`, `S<half> <year>`) are fixed. A missing or invalid date is a rule error and routes the document to `pending/`. Parsing strings into native dates uses expr's standard `date(value, layout)` builtin (Go date layout, e.g. `date(`15/01/2026`, `02/01/2006`)`) — the format is carried by the parsing rule, so it can differ per document type.
|
||||
The application registers three built-in helpers in the expression environment. `firstMatch(regex, texts?)` returns the first match of the regex in the document text (first capture group when present), or an empty string when no match is found (parsing an empty string with `date()` is then a rule error and routes the document to `pending/`); the optional second argument is an expression evaluated to the text(s) to search — a single page (e.g. `pages[0]`, the first page), a slice (e.g. `pages[0:3]`, pages 1 to 3, expr's standard slice syntax, end exclusive), or any list of strings — and when omitted the whole document is searched (e.g. `firstMatch(`[0-9]{2}/[0-9]{2}/[0-9]{4}`, pages[0])`). `frPeriod(value, unit, shift, layout?)` renders a period instead of a raw date: `value` is a native date (e.g. `emissionDate`), `unit` is `month` | `quarter` | `half` | `year`, `shift` is `-1` (previous period), `0` (period containing the date) or `+1` (next period) — e.g. a rent receipt issued mid-month uses the containing month, a service-charge call issued at the end of a quarter uses the next quarter, a bank statement uses the previous month. It returns a French string: `Janvier 2026`, `T2 2026`, `S1 2026` or `2026`. The optional `layout` argument is a standard Go reference-date layout applied to the period's first day, with month names rendered in French by the application (default layouts: `January 2006` for `month`, `2006` for `year`); it is ignored for `quarter` and `half`, whose formats (`T<quarter> <year>`, `S<half> <year>`) are fixed. A missing or invalid date is a rule error and routes the document to `pending/`. Parsing strings into native dates uses expr's standard `date(value, layout)` builtin (Go date layout, e.g. `date(`15/01/2026`, `02/01/2006`)`) — the format is carried by the parsing rule, so it can differ per document type.
|
||||
|
||||
`frDate(text)` extracts and parses a French natural-language date — the layout Go cannot express, since `date(value, layout)` only knows English month names. It matches `le 19 janvier 2026` (day and leading `le` optional: `Janvier 2026` gives the first of the month; `1er` and abbreviated month names like `févr.` are accepted; accents are optional) and returns a native date — e.g. `frDate(firstMatch(`(?:le )?\d{1,2} \S+ \d{4}`, pages[0]))`, or `frDate(pages[0])` when the first French-looking date of the page is the right one. A text containing no French date, or an impossible date (31 février), is a rule error and routes the document to pending/.
|
||||
|
||||
3. `templates`: named naming templates (e.g. `bank_statement: {emittingCompany}/{emissionDate.Format(`2006-01-02`)}_{documentType}_{bankAccount}.pdf`). A template is a literal string containing `{...}` placeholders: the inside of each placeholder is an `expr-lang/expr` expression evaluated in the document context (fields, helpers, builtins — e.g. `{emissionDate.Format(`2006-01-02`)}` or `{upper(emittingCompany)}`) and replaced by its result; the static text between placeholders stays literal. Unset optional fields evaluate to an empty string; a missing mandatory field or an evaluation error renders the result invalid and routes the file to `pending/`. All placeholder expressions are compiled at config load; "/" must not appear in the result (it is the S3 key delimiter). The template is chosen by the rules themselves: `template` is a field declared in the `fields` section like any other and set with `set` (e.g. `set template = `bank_statement`` when `emittingCompany in [`BRED`, `Fortunéo`] and documentType == `Relevé de compte``). If no rule sets `template`, the special `default` template is used. A `template` value with no matching entry renders the result invalid and routes the file to `pending/`.
|
||||
|
||||
Validation on load: JSON schema, regex compilation, `expr` expression compilation (`when`, `eval` and template placeholder expressions) against a typed context (`Document{Pages, Fields}`) with the `firstMatch` and `period` helpers, regex precompilation and caching. Full rejection if invalid, no partial processing.
|
||||
Validation on load: JSON schema, regex compilation, `expr` expression compilation (`when`, `eval` and template placeholder expressions) against a typed context (`Document{Pages, Fields}`) with the `firstMatch`, `frDate` and `frPeriod` helpers, regex precompilation and caching. Full rejection if invalid, no partial processing.
|
||||
|
||||
## 6. Processing Pipeline
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
// 2026"; the day is optional and "1er" / abbreviated months are
|
||||
// accepted) into a native date, for documents whose date is not
|
||||
// written in a Go layout format.
|
||||
// period(value, unit, shift, layout?) renders a period instead of a
|
||||
// frPeriod(value, unit, shift, layout?) renders a period instead of a
|
||||
// raw date: unit = `month`|`quarter`|`half`|`year`, shift = -1/0/+1
|
||||
// for the previous / containing / next period (rent receipt: 0,
|
||||
// end-of-quarter charges call: +1, bank statement: -1). Returns a
|
||||
@@ -187,7 +187,7 @@
|
||||
// ==========================================================================
|
||||
"templates": {
|
||||
// A bank statement covers the month before its emission date.
|
||||
"bank_statement": "{emittingCompany}/{period(emissionDate, `month`, -1)}_{documentType}_{bankAccount}.pdf",
|
||||
"bank_statement": "{emittingCompany}/{frPeriod(emissionDate, `month`, -1)}_{documentType}_{bankAccount}.pdf",
|
||||
"tax_notice_housing": "Impots/{emissionDate.Format(`2006-01-02`)}_{documentType}_{address}.pdf",
|
||||
"tax_notice_income": "Impots/{emissionDate.Format(`2006-01-02`)}_{documentType}.pdf",
|
||||
"invoice": "Factures/{emissionDate.Format(`2006-01-02`)}_{emittingCompany}_{reference}.pdf",
|
||||
|
||||
@@ -189,7 +189,7 @@ func CompileExpr(src string) (*vm.Program, error) {
|
||||
func exprOptions() []expr.Option {
|
||||
return []expr.Option{
|
||||
expr.Function("firstMatch", FirstMatch),
|
||||
expr.Function("period", Period),
|
||||
expr.Function("frPeriod", FrPeriod),
|
||||
expr.Function("frDate", FrenchDate),
|
||||
}
|
||||
}
|
||||
@@ -472,14 +472,14 @@ func FrenchDate(args ...any) (any, error) {
|
||||
var frenchMonths = []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
|
||||
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"}
|
||||
|
||||
// Period is the period(value, unit, shift, layout?) helper (arch.md §5).
|
||||
func Period(args ...any) (any, error) {
|
||||
// FrPeriod is the frPeriod(value, unit, shift, layout?) helper (arch.md §5).
|
||||
func FrPeriod(args ...any) (any, error) {
|
||||
if len(args) < 3 {
|
||||
return "", fmt.Errorf("period: value, unit and shift required")
|
||||
return "", fmt.Errorf("frPeriod: value, unit and shift required")
|
||||
}
|
||||
d, ok := args[0].(time.Time)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("period: value must be a native date")
|
||||
return "", fmt.Errorf("frPeriod: value must be a native date")
|
||||
}
|
||||
unit, _ := args[1].(string)
|
||||
shift, _ := toInt(args[2])
|
||||
@@ -533,7 +533,7 @@ func Period(args ...any) (any, error) {
|
||||
}
|
||||
return time.Date(y, 1, 1, 0, 0, 0, 0, time.UTC).Format(layout), nil
|
||||
default:
|
||||
return "", fmt.Errorf("period: unknown unit %q (want month|quarter|half|year)", unit)
|
||||
return "", fmt.Errorf("frPeriod: unknown unit %q (want month|quarter|half|year)", unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ so that no document is ever processed against a partially understood configurati
|
||||
Acceptance criteria:
|
||||
|
||||
- Given a syntactically valid `config.jsonc` mounted in the container, when the application starts, then the file is parsed with comments allowed and validated against the JSON schema generated from the configuration structs.
|
||||
- Given the schema is valid, when the application validates further, then every regex compiles, every `expr` expression (`when`, `eval`, template placeholders) compiles against the typed context `Document{Pages, Fields}` with the `firstMatch` and `period` helpers, and every compiled expression is cached for later use.
|
||||
- Given the schema is valid, when the application validates further, then every regex compiles, every `expr` expression (`when`, `eval`, template placeholders) compiles against the typed context `Document{Pages, Fields}` with the `firstMatch`, `frDate` and `frPeriod` helpers, and every compiled expression is cached for later use.
|
||||
- Given any validation error (unknown key, bad type, non-compiling regex or expression, unknown field referenced in `set`/`delete`), when the application starts, then it exits with a non-zero code, an error message pinpointing the file location of the first error, and processes nothing. Cross-references are also checked: every template name used in the rules must exist in `templates`.
|
||||
- Given the configuration is valid, when a field declaration references a type, then the declared type is one of `text`, `number`, `date`, `boolean`, `enum` (schema-enforced).
|
||||
- Given the configuration is valid, when the application starts, then the effective configuration (fields, rules, templates) is logged in a condensed form for traceability.
|
||||
@@ -131,7 +131,7 @@ Acceptance criteria:
|
||||
|
||||
- Given a document's pages, when evaluation runs, then rules are evaluated in declaration order: a rule's `when` expression runs first; `set`, `delete`, and child rules run only if `when` returned true.
|
||||
- Given a rule's `set` entry with a constant `value`, when the rule fires, then the field is set to that constant.
|
||||
- Given a `set` entry with an `eval` expression, when the rule fires, then the expression is evaluated in the same context (pages, current fields, `firstMatch`, `period`, expr builtins) and the result is assigned.
|
||||
- Given a `set` entry with an `eval` expression, when the rule fires, then the expression is evaluated in the same context (pages, current fields, `firstMatch`, `frDate`, `frPeriod`, expr builtins) and the result is assigned.
|
||||
- Given a field assignment whose value does not match the declared field type (raw string to a `date`, non-numeric to a `number`), when it executes, then it is a rule error and the document is routed to `pending/`.
|
||||
- Given `firstMatch` finds no match, when its result is used, then it returns an empty string (parsing an empty string with `date()` is a rule error).
|
||||
- Given `delete` is executed, when the rule fires, then the named fields become undefined again for later rules and template rendering.
|
||||
|
||||
Reference in New Issue
Block a user