281 lines
9.5 KiB
Go
281 lines
9.5 KiB
Go
package engine
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"math/big"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
var (
|
|
errEmptySequenceIdentityFields = errors.New("sequence identity requires at least one field")
|
|
errInvalidSequenceIdentityNumber = errors.New("invalid sequence identity number")
|
|
errUnsupportedSequenceIdentityValue = errors.New("unsupported sequence identity value")
|
|
)
|
|
|
|
type sequenceIdentityDiagnosticCodes struct {
|
|
invalid Code
|
|
conflict Code
|
|
}
|
|
|
|
type sequenceIdentityRecord struct {
|
|
tuple string
|
|
index int
|
|
location document.Location
|
|
}
|
|
|
|
// normalizeSequenceIdentityFields treats identity field names as a set.
|
|
func normalizeSequenceIdentityFields(fields []string) ([]string, error) {
|
|
if len(fields) == 0 {
|
|
return nil, errEmptySequenceIdentityFields
|
|
}
|
|
normalized := append([]string(nil), fields...)
|
|
sort.Strings(normalized)
|
|
write := 0
|
|
for _, field := range normalized {
|
|
if write != 0 && normalized[write-1] == field {
|
|
continue
|
|
}
|
|
normalized[write] = field
|
|
write++
|
|
}
|
|
return normalized[:write], nil
|
|
}
|
|
|
|
// normalizeSequenceIdentityNumber returns coefficient and exponent without
|
|
// expanding powers of ten. The represented value is coefficient * 10^exponent.
|
|
func normalizeSequenceIdentityNumber(value string) (string, error) {
|
|
if value == "" {
|
|
return "", errInvalidSequenceIdentityNumber
|
|
}
|
|
negative := false
|
|
if value[0] == '-' {
|
|
negative = true
|
|
value = value[1:]
|
|
}
|
|
if value == "" {
|
|
return "", errInvalidSequenceIdentityNumber
|
|
}
|
|
|
|
exponent := new(big.Int)
|
|
if index := strings.IndexAny(value, "eE"); index >= 0 {
|
|
if strings.IndexAny(value[index+1:], "eE") >= 0 {
|
|
return "", errInvalidSequenceIdentityNumber
|
|
}
|
|
text := value[index+1:]
|
|
if text == "" {
|
|
return "", errInvalidSequenceIdentityNumber
|
|
}
|
|
if _, ok := exponent.SetString(text, 10); !ok {
|
|
return "", errInvalidSequenceIdentityNumber
|
|
}
|
|
value = value[:index]
|
|
}
|
|
|
|
point := strings.IndexByte(value, '.')
|
|
if point >= 0 && strings.IndexByte(value[point+1:], '.') >= 0 {
|
|
return "", errInvalidSequenceIdentityNumber
|
|
}
|
|
integerPart := value
|
|
fractionPart := ""
|
|
if point >= 0 {
|
|
integerPart = value[:point]
|
|
fractionPart = value[point+1:]
|
|
}
|
|
if integerPart == "" || (point >= 0 && fractionPart == "") {
|
|
return "", errInvalidSequenceIdentityNumber
|
|
}
|
|
if len(integerPart) > 1 && integerPart[0] == '0' {
|
|
return "", errInvalidSequenceIdentityNumber
|
|
}
|
|
digits := integerPart + fractionPart
|
|
for _, digit := range digits {
|
|
if digit < '0' || digit > '9' {
|
|
return "", errInvalidSequenceIdentityNumber
|
|
}
|
|
}
|
|
coefficient := strings.TrimLeft(digits, "0")
|
|
if coefficient == "" {
|
|
return "0e0", nil
|
|
}
|
|
exponent.Sub(exponent, big.NewInt(int64(len(fractionPart))))
|
|
trimmed := strings.TrimRight(coefficient, "0")
|
|
exponent.Add(exponent, big.NewInt(int64(len(coefficient)-len(trimmed))))
|
|
coefficient = trimmed
|
|
if negative {
|
|
coefficient = "-" + coefficient
|
|
}
|
|
return coefficient + "e" + exponent.String(), nil
|
|
}
|
|
|
|
func sequenceScalarIdentityComponent(node *document.Node) (byte, string, error) {
|
|
if node == nil {
|
|
return 0, "", errUnsupportedSequenceIdentityValue
|
|
}
|
|
switch node.Kind {
|
|
case document.String:
|
|
return 's', node.Text, nil
|
|
case document.Number:
|
|
normalized, err := normalizeSequenceIdentityNumber(node.Text)
|
|
return 'n', normalized, err
|
|
case document.Boolean:
|
|
if node.Text != "true" && node.Text != "false" {
|
|
return 0, "", errUnsupportedSequenceIdentityValue
|
|
}
|
|
return 'b', node.Text, nil
|
|
case document.Null:
|
|
return 'z', "", nil
|
|
default:
|
|
return 0, "", errUnsupportedSequenceIdentityValue
|
|
}
|
|
}
|
|
|
|
func sequenceIdentityTuple(values []*document.Node) (string, error) {
|
|
var encoded bytes.Buffer
|
|
for _, value := range values {
|
|
kind, component, err := sequenceScalarIdentityComponent(value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
encoded.WriteByte(kind)
|
|
var length [binary.MaxVarintLen64]byte
|
|
n := binary.PutUvarint(length[:], uint64(len(component)))
|
|
encoded.Write(length[:n])
|
|
encoded.WriteString(component)
|
|
}
|
|
return encoded.String(), nil
|
|
}
|
|
|
|
// sequenceMappingIndex is built once per record. Structural fields are omitted
|
|
// by entry index, so a directive is excluded only at its actual occurrence.
|
|
type sequenceMappingIndex struct {
|
|
entries []document.Entry
|
|
byKey map[string]int
|
|
}
|
|
|
|
func newSequenceMappingIndex(record *document.Node) sequenceMappingIndex {
|
|
index := sequenceMappingIndex{byKey: make(map[string]int)}
|
|
if record == nil || record.Kind != document.Mapping {
|
|
return index
|
|
}
|
|
index.entries = record.Mapping
|
|
directiveIndex := -1
|
|
if directive, exists := inheritanceDirective(record); exists {
|
|
for entryIndex := range record.Mapping {
|
|
if record.Mapping[entryIndex].Key == directive.Key && record.Mapping[entryIndex].KeyLocation == directive.KeyLocation {
|
|
directiveIndex = entryIndex
|
|
break
|
|
}
|
|
}
|
|
}
|
|
for entryIndex, entry := range record.Mapping {
|
|
if entryIndex == directiveIndex {
|
|
continue
|
|
}
|
|
if _, exists := index.byKey[entry.Key]; !exists {
|
|
index.byKey[entry.Key] = entryIndex
|
|
}
|
|
}
|
|
return index
|
|
}
|
|
|
|
func (index sequenceMappingIndex) identity(fields []string) (string, []document.Location, string, int) {
|
|
values := make([]*document.Node, len(fields))
|
|
locations := make([]document.Location, len(fields))
|
|
for fieldIndex, field := range fields {
|
|
entryIndex, exists := index.byKey[field]
|
|
if !exists {
|
|
return "", nil, field, -1
|
|
}
|
|
entry := index.entries[entryIndex]
|
|
if entry.Value == nil || (entry.Value.Kind != document.String && entry.Value.Kind != document.Number && entry.Value.Kind != document.Boolean && entry.Value.Kind != document.Null) {
|
|
return "", nil, field, entryIndex
|
|
}
|
|
if _, _, err := sequenceScalarIdentityComponent(entry.Value); err != nil {
|
|
return "", nil, field, entryIndex
|
|
}
|
|
values[fieldIndex] = entry.Value
|
|
locations[fieldIndex] = entry.ValueLocation
|
|
if entry.Value != nil && entry.Value.Location != (document.Location{}) {
|
|
locations[fieldIndex] = entry.Value.Location
|
|
}
|
|
}
|
|
tuple, err := sequenceIdentityTuple(values)
|
|
if err != nil {
|
|
return "", nil, fields[0], -2
|
|
}
|
|
return tuple, locations, "", -3
|
|
}
|
|
|
|
// validateSequenceIdentity checks mapping shape, direct identity fields, and
|
|
// uniqueness in one tuple-to-first-location index. It does not alter the nodes.
|
|
func validateSequenceIdentity(sequence *document.Node, path Path, fields []string, codes sequenceIdentityDiagnosticCodes) (map[string]sequenceIdentityRecord, []Diagnostic) {
|
|
return validateSequenceIdentityFrom(sequence, path, fields, codes, 0)
|
|
}
|
|
|
|
func validateSequenceIdentityFrom(sequence *document.Node, path Path, fields []string, codes sequenceIdentityDiagnosticCodes, startIndex int) (map[string]sequenceIdentityRecord, []Diagnostic) {
|
|
normalized, err := normalizeSequenceIdentityFields(fields)
|
|
if err != nil {
|
|
location := (*Location)(nil)
|
|
if sequence != nil {
|
|
converted := publicLocation(sequence.Location)
|
|
location = &converted
|
|
}
|
|
return nil, []Diagnostic{{Code: codes.invalid, Severity: SeverityError, Path: path, Message: "sequence identity requires at least one field", Primary: location}}
|
|
}
|
|
return validateNormalizedSequenceIdentityFrom(sequence, path, normalized, codes, startIndex)
|
|
}
|
|
|
|
func validateNormalizedSequenceIdentityFrom(sequence *document.Node, path Path, normalized []string, codes sequenceIdentityDiagnosticCodes, startIndex int) (map[string]sequenceIdentityRecord, []Diagnostic) {
|
|
if sequence == nil || sequence.Kind != document.Sequence {
|
|
location := (*Location)(nil)
|
|
if sequence != nil {
|
|
converted := publicLocation(sequence.Location)
|
|
location = &converted
|
|
}
|
|
return nil, []Diagnostic{{Code: codes.invalid, Severity: SeverityError, Path: path, Message: "identified collection must be a sequence", Primary: location}}
|
|
}
|
|
|
|
firstByTuple := make(map[string]sequenceIdentityRecord, len(sequence.Sequence))
|
|
for itemIndex, item := range sequence.Sequence {
|
|
itemPath := path.Index(startIndex + itemIndex)
|
|
if item == nil || item.Kind != document.Mapping {
|
|
var primary *Location
|
|
if item != nil {
|
|
converted := publicLocation(item.Location)
|
|
primary = &converted
|
|
}
|
|
return nil, []Diagnostic{{Code: codes.invalid, Severity: SeverityError, Path: itemPath, Message: "identified collection items must be mappings", Primary: primary}}
|
|
}
|
|
mapping := newSequenceMappingIndex(item)
|
|
tuple, locations, missing, entryIndex := mapping.identity(normalized)
|
|
if entryIndex == -1 {
|
|
location := publicLocation(item.Location)
|
|
return nil, []Diagnostic{{Code: codes.invalid, Severity: SeverityError, Path: itemPath.Key(missing), Message: "identified collection item is missing an identity field", Primary: &location}}
|
|
}
|
|
if entryIndex == -2 || entryIndex >= 0 {
|
|
location := publicLocation(item.Location)
|
|
fieldPath := itemPath.Key(missing)
|
|
if entryIndex >= 0 {
|
|
location = publicLocation(item.Mapping[entryIndex].ValueLocation)
|
|
if value := item.Mapping[entryIndex].Value; value != nil {
|
|
location = publicLocation(value.Location)
|
|
}
|
|
}
|
|
return nil, []Diagnostic{{Code: codes.invalid, Severity: SeverityError, Path: fieldPath, Message: "identity fields must be scalar values", Primary: &location}}
|
|
}
|
|
if first, duplicate := firstByTuple[tuple]; duplicate {
|
|
current := publicLocation(locations[0])
|
|
return nil, []Diagnostic{{Code: codes.conflict, Severity: SeverityError, Path: path, Message: "identified collection contains duplicate identities", Primary: ptrLocation(publicLocation(first.location)), Related: []Location{current}}}
|
|
}
|
|
firstByTuple[tuple] = sequenceIdentityRecord{tuple: tuple, index: itemIndex, location: locations[0]}
|
|
}
|
|
return firstByTuple, nil
|
|
}
|
|
|
|
func ptrLocation(location Location) *Location { return &location }
|