CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
286 lines
9.4 KiB
Go
286 lines
9.4 KiB
Go
// Package routingruleapi ist die HTTP-Bindung fuer den Routing-Regel-Editor
|
|
// (RTE-03). Nutzeridentitaet uebergangsweise ueber X-User-Id, gleiches
|
|
// Muster wie approvalapi (APR-01) vor der Core-JWT-Anbindung.
|
|
package routingruleapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingrule"
|
|
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingsim"
|
|
)
|
|
|
|
var allowedOperators = map[string]bool{"equals": true, "contains": true, "in": true}
|
|
var allowedActionTypes = map[string]bool{"route_to_process": true, "route_to_person": true}
|
|
|
|
type Server struct {
|
|
store *routingrule.Store
|
|
}
|
|
|
|
func NewServer(store *routingrule.Store) *Server {
|
|
return &Server{store: store}
|
|
}
|
|
|
|
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /routing-rules", s.requireUser(s.handleList))
|
|
mux.HandleFunc("POST /routing-rules", s.requireUser(s.handleCreate))
|
|
mux.HandleFunc("PUT /routing-rules/{id}", s.requireUser(s.handleUpdate))
|
|
mux.HandleFunc("PUT /routing-rules/{id}/priority", s.requireUser(s.handleSetPriority))
|
|
mux.HandleFunc("PUT /routing-rules/{id}/enabled", s.requireUser(s.handleSetEnabled))
|
|
mux.HandleFunc("POST /routing-rules/test", s.requireUser(s.handleTest))
|
|
mux.HandleFunc("POST /routing-rules/simulate", s.requireUser(s.handleSimulate))
|
|
}
|
|
|
|
func (s *Server) requireUser(next func(w http.ResponseWriter, r *http.Request, userID string)) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
userID := r.Header.Get("X-User-Id")
|
|
if userID == "" {
|
|
writeError(w, http.StatusUnauthorized, "X-User-Id fehlt")
|
|
return
|
|
}
|
|
next(w, r, userID)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleList(w http.ResponseWriter, r *http.Request, _ string) {
|
|
rules, err := s.store.ListAll(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "regeln konnten nicht geladen werden")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, rules)
|
|
}
|
|
|
|
type ruleInput struct {
|
|
Name string `json:"name"`
|
|
Priority int `json:"priority"`
|
|
Conditions []routingrule.Condition `json:"conditions"`
|
|
Actions []routingrule.Action `json:"actions"`
|
|
}
|
|
|
|
// validate lehnt strukturell ungueltige Bedingungen/Aktionen ab, bevor sie
|
|
// gespeichert werden (Pruefung 3: ungueltige Bedingungseingabe wird
|
|
// abgewiesen). Der Store selbst validiert das nicht (RTE-01-Scope), daher
|
|
// hier auf API-Ebene statt den bestehenden Store-Code zu veraendern.
|
|
func validate(in ruleInput) error {
|
|
if in.Name == "" {
|
|
return errors.New("name ist pflicht")
|
|
}
|
|
for _, c := range in.Conditions {
|
|
if c.Field == "" {
|
|
return errors.New("bedingung: feld ist pflicht")
|
|
}
|
|
if !allowedOperators[c.Operator] {
|
|
return errors.New("bedingung: unbekannter operator " + c.Operator)
|
|
}
|
|
if c.Value == nil {
|
|
return errors.New("bedingung: wert ist pflicht")
|
|
}
|
|
}
|
|
for _, a := range in.Actions {
|
|
if !allowedActionTypes[a.Type] {
|
|
return errors.New("aktion: unbekannter typ " + a.Type)
|
|
}
|
|
if a.Target == "" {
|
|
return errors.New("aktion: ziel ist pflicht")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request, _ string) {
|
|
var in ruleInput
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltige anfrage")
|
|
return
|
|
}
|
|
if err := validate(in); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
rule, err := s.store.Create(r.Context(), in.Name, in.Priority, in.Conditions, in.Actions)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "regel konnte nicht angelegt werden")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, rule)
|
|
}
|
|
|
|
func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request, _ string) {
|
|
id := r.PathValue("id")
|
|
var in ruleInput
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltige anfrage")
|
|
return
|
|
}
|
|
if err := validate(in); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
rule, err := s.store.Update(r.Context(), id, in.Conditions, in.Actions)
|
|
if err != nil {
|
|
if errors.Is(err, routingrule.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "regel nicht gefunden")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "regel konnte nicht aktualisiert werden")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, rule)
|
|
}
|
|
|
|
type priorityInput struct {
|
|
Priority int `json:"priority"`
|
|
}
|
|
|
|
func (s *Server) handleSetPriority(w http.ResponseWriter, r *http.Request, _ string) {
|
|
id := r.PathValue("id")
|
|
var in priorityInput
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltige anfrage")
|
|
return
|
|
}
|
|
if err := s.store.SetPriority(r.Context(), id, in.Priority); err != nil {
|
|
if errors.Is(err, routingrule.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "regel nicht gefunden")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "prioritaet konnte nicht gesetzt werden")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
type enabledInput struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
func (s *Server) handleSetEnabled(w http.ResponseWriter, r *http.Request, _ string) {
|
|
id := r.PathValue("id")
|
|
var in enabledInput
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltige anfrage")
|
|
return
|
|
}
|
|
if err := s.store.SetEnabled(r.Context(), id, in.Enabled); err != nil {
|
|
if errors.Is(err, routingrule.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "regel nicht gefunden")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "status konnte nicht gesetzt werden")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
type testInput struct {
|
|
Object map[string]any `json:"object"`
|
|
}
|
|
|
|
type testResult struct {
|
|
Matched bool `json:"matched"`
|
|
RuleID string `json:"rule_id,omitempty"`
|
|
RuleName string `json:"rule_name,omitempty"`
|
|
Actions []routingrule.Action `json:"actions,omitempty"`
|
|
}
|
|
|
|
// handleTest fuehrt eine Regel probeweise gegen ein Beispielobjekt aus
|
|
// (Pruefung 2: Testlauf vor Aktivierung), ohne eine echte Ausfuehrung
|
|
// (RTE-02, routing_decisions) anzustossen — reiner Lesevorgang.
|
|
func (s *Server) handleTest(w http.ResponseWriter, r *http.Request, _ string) {
|
|
var in testInput
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltige anfrage")
|
|
return
|
|
}
|
|
if in.Object == nil {
|
|
writeError(w, http.StatusBadRequest, "object ist pflicht")
|
|
return
|
|
}
|
|
rules, err := s.store.ListAll(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "regeln konnten nicht geladen werden")
|
|
return
|
|
}
|
|
// ListAll sortiert bereits nach Prioritaet absteigend; erste passende
|
|
// (aktivierte) Regel gewinnt (RTE-02-Prinzip), hier nur simuliert.
|
|
for _, rule := range rules {
|
|
if !rule.Enabled {
|
|
continue
|
|
}
|
|
if rule.Matches(in.Object) {
|
|
writeJSON(w, http.StatusOK, testResult{Matched: true, RuleID: rule.ID, RuleName: rule.Name, Actions: rule.Actions})
|
|
return
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, testResult{Matched: false})
|
|
}
|
|
|
|
type simulateCase struct {
|
|
Label string `json:"label"`
|
|
Object map[string]any `json:"object"`
|
|
ExpectedRuleID string `json:"expected_rule_id,omitempty"`
|
|
}
|
|
|
|
type simulateInput struct {
|
|
Corpus []simulateCase `json:"corpus"`
|
|
}
|
|
|
|
type simulateResult struct {
|
|
Label string `json:"label"`
|
|
ExpectedRuleID string `json:"expected_rule_id,omitempty"`
|
|
ActualRuleID string `json:"actual_rule_id,omitempty"`
|
|
ActualRuleName string `json:"actual_rule_name,omitempty"`
|
|
Actions []routingrule.Action `json:"actions,omitempty"`
|
|
Matched bool `json:"matched"`
|
|
AsExpected bool `json:"as_expected"`
|
|
}
|
|
|
|
// handleSimulate ist RTE-04: das Regelwerk gegen einen ganzen Testkorpus
|
|
// simulieren (nicht nur ein Objekt wie handleTest), mit Abgleich erwartete
|
|
// gegen tatsaechliche Zuordnung. Reiner Lesevorgang ueber routingsim.Simulate
|
|
// (kein routing_decisions-/Klaerungskorb-Eintrag). Bereitet zugleich die
|
|
// Schnittstelle fuer QA-04 (Pruefgate Routing) vor, ohne dessen Umfang
|
|
// vorwegzunehmen.
|
|
func (s *Server) handleSimulate(w http.ResponseWriter, r *http.Request, _ string) {
|
|
var in simulateInput
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltige anfrage")
|
|
return
|
|
}
|
|
corpus := make([]routingsim.TestCase, 0, len(in.Corpus))
|
|
for _, c := range in.Corpus {
|
|
corpus = append(corpus, routingsim.TestCase{Label: c.Label, Object: c.Object, ExpectedRuleID: c.ExpectedRuleID})
|
|
}
|
|
results, err := routingsim.Simulate(r.Context(), s.store, corpus)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "simulation fehlgeschlagen")
|
|
return
|
|
}
|
|
out := make([]simulateResult, 0, len(results))
|
|
for _, res := range results {
|
|
out = append(out, simulateResult{
|
|
Label: res.Label,
|
|
ExpectedRuleID: res.ExpectedRuleID,
|
|
ActualRuleID: res.ActualRuleID,
|
|
ActualRuleName: res.ActualRuleName,
|
|
Actions: res.Actions,
|
|
Matched: res.Matched,
|
|
AsExpected: res.AsExpected,
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, msg string) {
|
|
writeJSON(w, status, map[string]string{"error": msg})
|
|
}
|