2026-09-12 15:58:06 +00:00
package engine
import (
"encoding"
"math"
"reflect"
"regexp"
"strconv"
"time"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
var integerPattern = regexp . MustCompile ( ` ^-?(0|[1-9][0-9]*)$ ` )
var unsignedIntegerPattern = regexp . MustCompile ( ` ^(0|[1-9][0-9]*)$ ` )
type decodeIssue struct {
code Code
path Path
message string
location document . Location
}
type inputPresence struct {
location document . Location
}
type decodedConfiguration struct {
value reflect . Value
presence map [ string ] inputPresence
origins map [ string ] Origin
baseline * document . Node
final * document . Node
}
type decodeContext struct {
versionPath * Path
}
func decodeConfiguration ( schema * schemaDescriptor , defaults reflect . Value , input * document . Node ) ( decodedConfiguration , [ ] decodeIssue ) {
return decodeConfigurationWithContext ( schema , defaults , input , decodeContext { } )
}
func decodeConfigurationWithContext ( schema * schemaDescriptor , defaults reflect . Value , input * document . Node , context decodeContext ) ( decodedConfiguration , [ ] decodeIssue ) {
if schema == nil || schema . root == nil || ! defaults . IsValid ( ) || defaults . Type ( ) != schema . root . typeOf {
return decodedConfiguration { } , [ ] decodeIssue { { code : CodeSchemaUnsupportedType , message : "configuration defaults do not match the schema" } }
}
baseline , projectionIssue := projectConfiguration ( schema , defaults , defaultProjectionLimits ( ) )
if projectionIssue != nil {
return decodedConfiguration { } , [ ] decodeIssue { decodeIssueFromProjection ( projectionIssue ) }
}
working , cloneIssue := cloneConfiguration ( schema , defaults , defaultProjectionLimits ( ) )
if cloneIssue != nil {
return decodedConfiguration { } , [ ] decodeIssue { decodeIssueFromProjection ( cloneIssue ) }
}
presence := make ( map [ string ] inputPresence )
if input != nil {
collectPresence ( input , Path { } , presence )
decoded , issues := decodeValue ( input , working , schema . root , Path { } , schema , context )
if len ( issues ) != 0 {
return decodedConfiguration { } , issues
}
working = decoded
}
final , projectionIssue := projectConfiguration ( schema , working , defaultProjectionLimits ( ) )
if projectionIssue != nil {
return decodedConfiguration { } , [ ] decodeIssue { decodeIssueFromProjection ( projectionIssue ) }
}
origins := make ( map [ string ] Origin )
collectOrigins ( final , Path { } , presence , origins )
return decodedConfiguration {
value : working ,
presence : presence ,
origins : origins ,
baseline : baseline ,
final : final ,
} , nil
}
func decodeValue ( node * document . Node , base reflect . Value , descriptor * typeDescriptor , path Path , schema * schemaDescriptor , context decodeContext ) ( reflect . Value , [ ] decodeIssue ) {
if node . Kind == document . Null {
switch descriptor . kind {
case descriptorPointer , descriptorMap , descriptorSlice , descriptorDynamic :
return reflect . Zero ( descriptor . typeOf ) , nil
case descriptorSecret :
return reflect . Value { } , [ ] decodeIssue { newDecodeIssue ( CodeSecretRequired , path , node , "required secret reference cannot be null" ) }
default :
return reflect . Value { } , [ ] decodeIssue { newDecodeIssue ( CodeDecodeNull , path , node , "null is not allowed for this configuration type" ) }
}
}
if descriptor . kind == descriptorPointer {
2026-10-25 21:18:35 +00:00
terminal , ok := terminalDescriptor ( descriptor )
if ! ok {
return reflect . Value { } , [ ] decodeIssue { newDecodeIssue ( CodeDecodeType , path , node , "configuration type cannot represent a non-null value" ) }
}
var pointers [ ] * typeDescriptor
currentBase := base
for current := descriptor ; current != terminal ; current = current . element {
pointers = append ( pointers , current )
if currentBase . IsNil ( ) {
currentBase = reflect . Zero ( current . typeOf . Elem ( ) )
} else {
currentBase = currentBase . Elem ( )
}
2026-09-12 15:58:06 +00:00
}
2026-10-25 21:18:35 +00:00
decoded , issues := decodeValue ( node , currentBase , terminal , path , schema , context )
2026-09-12 15:58:06 +00:00
if len ( issues ) != 0 {
return reflect . Value { } , issues
}
2026-10-25 21:18:35 +00:00
for i := len ( pointers ) - 1 ; i >= 0 ; i -- {
result := reflect . New ( pointers [ i ] . typeOf . Elem ( ) )
result . Elem ( ) . Set ( decoded )
decoded = result
}
return decoded , nil
2026-09-12 15:58:06 +00:00
}
switch descriptor . kind {
case descriptorStruct :
return decodeStruct ( node , base , descriptor , path , schema , context )
case descriptorMap :
return decodeMap ( node , base , descriptor , path , schema , context )
case descriptorSlice :
return decodeSlice ( node , descriptor , path , schema , context )
case descriptorArray :
return decodeArray ( node , descriptor , path , schema , context )
case descriptorDynamic :
return decodeDynamic ( node , base , path , schema , context )
case descriptorString :
if node . Kind != document . String && ! ( pathMatches ( context . versionPath , path ) && isIntegerVersion ( node ) ) {
return typeMismatch ( path , node , "string" )
}
result := reflect . New ( descriptor . typeOf ) . Elem ( )
result . SetString ( node . Text )
return result , nil
case descriptorBoolean :
if node . Kind != document . Boolean {
return typeMismatch ( path , node , "boolean" )
}
result := reflect . New ( descriptor . typeOf ) . Elem ( )
result . SetBool ( node . Text == "true" )
return result , nil
case descriptorSigned :
if node . Kind != document . Number || ! integerPattern . MatchString ( node . Text ) {
return typeMismatch ( path , node , "integer" )
}
value , err := strconv . ParseInt ( node . Text , 10 , descriptor . typeOf . Bits ( ) )
if err != nil {
return rangeFailure ( path , node , "integer" )
}
result := reflect . New ( descriptor . typeOf ) . Elem ( )
result . SetInt ( value )
return result , nil
case descriptorUnsigned :
if node . Kind != document . Number || ! unsignedIntegerPattern . MatchString ( node . Text ) {
return typeMismatch ( path , node , "unsigned integer" )
}
value , err := strconv . ParseUint ( node . Text , 10 , descriptor . typeOf . Bits ( ) )
if err != nil {
return rangeFailure ( path , node , "unsigned integer" )
}
result := reflect . New ( descriptor . typeOf ) . Elem ( )
result . SetUint ( value )
return result , nil
case descriptorFloat :
if node . Kind != document . Number {
return typeMismatch ( path , node , "number" )
}
value , err := strconv . ParseFloat ( node . Text , descriptor . typeOf . Bits ( ) )
if err != nil || math . IsNaN ( value ) || math . IsInf ( value , 0 ) {
return rangeFailure ( path , node , "number" )
}
result := reflect . New ( descriptor . typeOf ) . Elem ( )
result . SetFloat ( value )
return result , nil
case descriptorDuration :
if node . Kind != document . String {
return typeMismatch ( path , node , "duration string" )
}
value , err := time . ParseDuration ( node . Text )
if err != nil {
return reflect . Value { } , [ ] decodeIssue { newDecodeIssue ( CodeDecodeDuration , path , node , "duration string is invalid" ) }
}
result := reflect . New ( descriptor . typeOf ) . Elem ( )
result . SetInt ( int64 ( value ) )
return result , nil
case descriptorSecret :
if node . Kind != document . String {
return typeMismatch ( path , node , "secret environment name" )
}
2026-09-12 16:04:31 +00:00
return schema . secrets . Make ( SecretState { Name : node . Text , Specified : true } ) , nil
2026-09-12 15:58:06 +00:00
case descriptorText :
if node . Kind != document . String {
return typeMismatch ( path , node , "text string" )
}
result := reflect . New ( descriptor . typeOf ) . Elem ( )
unmarshaler := result . Addr ( ) . Interface ( ) . ( encoding . TextUnmarshaler )
if err := unmarshaler . UnmarshalText ( [ ] byte ( node . Text ) ) ; err != nil {
return reflect . Value { } , [ ] decodeIssue { newDecodeIssue ( CodeDecodeText , path , node , "text value is invalid for its configuration type" ) }
}
return result , nil
default :
return reflect . Value { } , [ ] decodeIssue { newDecodeIssue ( CodeSchemaUnsupportedType , path , node , "configuration type is not supported" ) }
}
}
func decodeStruct ( node * document . Node , base reflect . Value , descriptor * typeDescriptor , path Path , schema * schemaDescriptor , context decodeContext ) ( reflect . Value , [ ] decodeIssue ) {
if node . Kind != document . Mapping {
return typeMismatch ( path , node , "mapping" )
}
result := reflect . New ( descriptor . typeOf ) . Elem ( )
result . Set ( base )
var issues [ ] decodeIssue
for _ , entry := range node . Mapping {
childPath := path . Key ( entry . Key )
if position , ok := descriptor . fieldByName [ entry . Key ] ; ok {
field := descriptor . fields [ position ]
target , ok := fieldForDecode ( result , field . index , true )
if ! ok {
issues = append ( issues , decodeIssue { code : CodeSchemaUnsupportedType , path : childPath , message : "configuration field could not be addressed" , location : entry . ValueLocation } )
continue
}
decoded , fieldIssues := decodeValue ( entry . Value , target , field . descriptor , childPath , schema , context )
if len ( fieldIssues ) != 0 {
issues = append ( issues , fieldIssues ... )
continue
}
target . Set ( decoded )
continue
}
if descriptor . inlineMap != nil {
if issue := decodeInlineMapEntry ( result , * descriptor . inlineMap , entry , childPath , schema , context ) ; issue != nil {
issues = append ( issues , issue ... )
}
continue
}
issues = append ( issues , decodeIssue { code : CodeDecodeUnknownField , path : childPath , message : "configuration field is not declared" , location : entry . KeyLocation } )
}
return result , issues
}
func decodeInlineMapEntry ( target reflect . Value , field fieldDescriptor , entry document . Entry , path Path , schema * schemaDescriptor , context decodeContext ) [ ] decodeIssue {
mapValue , ok := fieldForDecode ( target , field . index , true )
if ! ok || mapValue . Kind ( ) != reflect . Map {
return [ ] decodeIssue { { code : CodeSchemaInline , path : path , message : "inline map could not be addressed" , location : entry . KeyLocation } }
}
if mapValue . IsNil ( ) {
mapValue . Set ( reflect . MakeMap ( mapValue . Type ( ) ) )
}
key := reflect . New ( mapValue . Type ( ) . Key ( ) ) . Elem ( )
key . SetString ( entry . Key )
base := mapValue . MapIndex ( key )
if ! base . IsValid ( ) {
base = reflect . Zero ( mapValue . Type ( ) . Elem ( ) )
}
decoded , issues := decodeValue ( entry . Value , base , field . descriptor . element , path , schema , context )
if len ( issues ) == 0 {
mapValue . SetMapIndex ( key , decoded )
}
return issues
}
func decodeMap ( node * document . Node , base reflect . Value , descriptor * typeDescriptor , path Path , schema * schemaDescriptor , context decodeContext ) ( reflect . Value , [ ] decodeIssue ) {
if node . Kind != document . Mapping {
return typeMismatch ( path , node , "mapping" )
}
result := base
if result . IsNil ( ) {
result = reflect . MakeMap ( descriptor . typeOf )
}
var issues [ ] decodeIssue
for _ , entry := range node . Mapping {
key := reflect . New ( descriptor . typeOf . Key ( ) ) . Elem ( )
key . SetString ( entry . Key )
entryBase := result . MapIndex ( key )
if ! entryBase . IsValid ( ) {
entryBase = reflect . Zero ( descriptor . typeOf . Elem ( ) )
}
decoded , entryIssues := decodeValue ( entry . Value , entryBase , descriptor . element , path . Key ( entry . Key ) , schema , context )
if len ( entryIssues ) != 0 {
issues = append ( issues , entryIssues ... )
continue
}
result . SetMapIndex ( key , decoded )
}
return result , issues
}
func decodeSlice ( node * document . Node , descriptor * typeDescriptor , path Path , schema * schemaDescriptor , context decodeContext ) ( reflect . Value , [ ] decodeIssue ) {
if node . Kind != document . Sequence {
return typeMismatch ( path , node , "sequence" )
}
result := reflect . MakeSlice ( descriptor . typeOf , len ( node . Sequence ) , len ( node . Sequence ) )
var issues [ ] decodeIssue
for i , child := range node . Sequence {
decoded , childIssues := decodeValue ( child , reflect . Zero ( descriptor . typeOf . Elem ( ) ) , descriptor . element , path . Index ( i ) , schema , context )
if len ( childIssues ) != 0 {
issues = append ( issues , childIssues ... )
continue
}
result . Index ( i ) . Set ( decoded )
}
return result , issues
}
func decodeArray ( node * document . Node , descriptor * typeDescriptor , path Path , schema * schemaDescriptor , context decodeContext ) ( reflect . Value , [ ] decodeIssue ) {
if node . Kind != document . Sequence {
return typeMismatch ( path , node , "sequence" )
}
if len ( node . Sequence ) != descriptor . length {
return reflect . Value { } , [ ] decodeIssue { newDecodeIssue ( CodeDecodeArrayLength , path , node , "configuration array has the wrong length" ) }
}
result := reflect . New ( descriptor . typeOf ) . Elem ( )
var issues [ ] decodeIssue
for i , child := range node . Sequence {
decoded , childIssues := decodeValue ( child , reflect . Zero ( descriptor . typeOf . Elem ( ) ) , descriptor . element , path . Index ( i ) , schema , context )
if len ( childIssues ) != 0 {
issues = append ( issues , childIssues ... )
continue
}
result . Index ( i ) . Set ( decoded )
}
return result , issues
}
func decodeDynamic ( node * document . Node , base reflect . Value , path Path , schema * schemaDescriptor , context decodeContext ) ( reflect . Value , [ ] decodeIssue ) {
if node . Kind == document . Mapping && base . IsValid ( ) && ! base . IsNil ( ) {
concrete := base . Elem ( )
operation := valueOperation { schema : schema }
descriptor , issue := operation . dynamicDescriptor ( concrete . Type ( ) , path )
if issue == nil && acceptsMapping ( descriptor ) {
decoded , issues := decodeValue ( node , concrete , descriptor , path , schema , context )
if len ( issues ) != 0 {
return reflect . Value { } , issues
}
result := reflect . New ( base . Type ( ) ) . Elem ( )
result . Set ( decoded )
return result , nil
}
}
var value any
switch node . Kind {
case document . Null :
return reflect . Zero ( base . Type ( ) ) , nil
case document . String :
value = node . Text
case document . Boolean :
value = node . Text == "true"
case document . Number :
if integerPattern . MatchString ( node . Text ) {
parsed , err := strconv . ParseInt ( node . Text , 10 , 64 )
if err != nil {
return rangeFailure ( path , node , "dynamic integer" )
}
value = parsed
} else {
parsed , err := strconv . ParseFloat ( node . Text , 64 )
if err != nil || math . IsNaN ( parsed ) || math . IsInf ( parsed , 0 ) {
return rangeFailure ( path , node , "dynamic number" )
}
value = parsed
}
case document . Sequence :
sequence := make ( [ ] any , len ( node . Sequence ) )
var issues [ ] decodeIssue
for i , child := range node . Sequence {
decoded , childIssues := decodeDynamic ( child , reflect . Zero ( reflect . TypeFor [ any ] ( ) ) , path . Index ( i ) , schema , context )
if len ( childIssues ) != 0 {
issues = append ( issues , childIssues ... )
continue
}
sequence [ i ] = decoded . Interface ( )
}
if len ( issues ) != 0 {
return reflect . Value { } , issues
}
value = sequence
case document . Mapping :
mapping := make ( map [ string ] any , len ( node . Mapping ) )
var issues [ ] decodeIssue
for _ , entry := range node . Mapping {
decoded , childIssues := decodeDynamic ( entry . Value , reflect . Zero ( reflect . TypeFor [ any ] ( ) ) , path . Key ( entry . Key ) , schema , context )
if len ( childIssues ) != 0 {
issues = append ( issues , childIssues ... )
continue
}
mapping [ entry . Key ] = decoded . Interface ( )
}
if len ( issues ) != 0 {
return reflect . Value { } , issues
}
value = mapping
default :
return reflect . Value { } , [ ] decodeIssue { newDecodeIssue ( CodeDecodeType , path , node , "dynamic configuration value has an unsupported kind" ) }
}
result := reflect . New ( base . Type ( ) ) . Elem ( )
if value != nil {
result . Set ( reflect . ValueOf ( value ) )
}
return result , nil
}
func acceptsMapping ( descriptor * typeDescriptor ) bool {
2026-10-25 21:18:35 +00:00
terminal , ok := terminalDescriptor ( descriptor )
if ! ok {
return false
2026-09-12 15:58:06 +00:00
}
2026-10-25 21:18:35 +00:00
return terminal . kind == descriptorStruct || terminal . kind == descriptorMap || terminal . kind == descriptorDynamic
2026-09-12 15:58:06 +00:00
}
func fieldForDecode ( root reflect . Value , index [ ] int , allocate bool ) ( reflect . Value , bool ) {
current := root
for position , fieldIndex := range index {
if current . Kind ( ) != reflect . Struct {
return reflect . Value { } , false
}
current = current . Field ( fieldIndex )
if position == len ( index ) - 1 {
return current , current . CanSet ( )
}
for current . Kind ( ) == reflect . Pointer {
if current . IsNil ( ) {
if ! allocate || ! current . CanSet ( ) {
return reflect . Value { } , false
}
current . Set ( reflect . New ( current . Type ( ) . Elem ( ) ) )
}
current = current . Elem ( )
}
}
return reflect . Value { } , false
}
func collectPresence ( node * document . Node , path Path , presence map [ string ] inputPresence ) {
2026-10-25 22:18:12 +00:00
presence [ path . String ( ) ] = inputPresence { location : node . Location }
2026-09-12 15:58:06 +00:00
switch node . Kind {
case document . Mapping :
for _ , entry := range node . Mapping {
collectPresence ( entry . Value , path . Key ( entry . Key ) , presence )
}
case document . Sequence :
for i , child := range node . Sequence {
collectPresence ( child , path . Index ( i ) , presence )
}
}
}
func collectOrigins ( node * document . Node , path Path , presence map [ string ] inputPresence , origins map [ string ] Origin ) {
if input , ok := presence [ path . String ( ) ] ; ok {
origins [ path . String ( ) ] = Origin { Layer : LayerBase , Locations : [ ] Location { { SourceID : input . location . SourceID , Line : input . location . Line , Column : input . location . Column } } }
} else {
origins [ path . String ( ) ] = Origin { Layer : LayerDefault }
}
switch node . Kind {
case document . Mapping :
for _ , entry := range node . Mapping {
collectOrigins ( entry . Value , path . Key ( entry . Key ) , presence , origins )
}
case document . Sequence :
for i , child := range node . Sequence {
collectOrigins ( child , path . Index ( i ) , presence , origins )
}
}
}
func decodeIssueFromProjection ( issue * projectionIssue ) decodeIssue {
return decodeIssue { code : issue . code , path : issue . path . clone ( ) , message : issue . message }
}
func newDecodeIssue ( code Code , path Path , node * document . Node , message string ) decodeIssue {
return decodeIssue { code : code , path : path . clone ( ) , message : message , location : node . Location }
}
func typeMismatch ( path Path , node * document . Node , expected string ) ( reflect . Value , [ ] decodeIssue ) {
return reflect . Value { } , [ ] decodeIssue { newDecodeIssue ( CodeDecodeType , path , node , "configuration value must be a " + expected ) }
}
func rangeFailure ( path Path , node * document . Node , expected string ) ( reflect . Value , [ ] decodeIssue ) {
return reflect . Value { } , [ ] decodeIssue { newDecodeIssue ( CodeDecodeRange , path , node , expected + " is outside the supported range" ) }
}