Resolve corresponding artifact family dependencies
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
const (
|
||||
partyCharactersFamilySource = "party.characters"
|
||||
characterIDToken = "{character_id}"
|
||||
memberArtifactSourcePrefix = "narratio.member_artifact."
|
||||
)
|
||||
|
||||
var memberVariableSelectors = map[string]func(PartyCharacter) string{
|
||||
@@ -44,6 +45,9 @@ func expandPipelineArtifactFamilies(cfg *PipelineConfig, party ResolvedParty) er
|
||||
}
|
||||
|
||||
expanded := cloneArtifactDefinitions(cfg.Scriptorium.Artifacts)
|
||||
if err := rejectConcreteMemberArtifactSources(expanded); err != nil {
|
||||
return err
|
||||
}
|
||||
familyKeys := sortedFamilyKeys(families)
|
||||
for _, familyKey := range familyKeys {
|
||||
if !artifactpolicy.IsConfiguredKey(familyKey) {
|
||||
@@ -53,6 +57,9 @@ func expandPipelineArtifactFamilies(cfg *PipelineConfig, party ResolvedParty) er
|
||||
return fmt.Errorf("pipeline.scriptorium.artifact_families.%s collides with configured artifact key %q", familyKey, familyKey)
|
||||
}
|
||||
}
|
||||
if err := validateFamilyMemberDependencies(families, familyKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
characters := append([]PartyCharacter(nil), party.Canonical.Characters...)
|
||||
sort.Slice(characters, func(left, right int) bool { return characters[left].ID < characters[right].ID })
|
||||
@@ -94,14 +101,22 @@ func expandPipelineArtifactFamilies(cfg *PipelineConfig, party ResolvedParty) er
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dependencies, err := expandedFamilyDependencies(prefix, family, character.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inputs, err := expandFamilyInputs(prefix, family, character.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expanded[key] = ScriptoriumArtifactConfig{
|
||||
Enabled: family.Enabled, DependsOn: append([]string(nil), family.DependsOn...), RenderDebug: family.RenderDebug,
|
||||
Enabled: family.Enabled, DependsOn: dependencies, RenderDebug: family.RenderDebug,
|
||||
PromptID: family.PromptID, ProfileID: family.ProfileID, OutputPath: outputPath, Timeout: family.Timeout,
|
||||
Inputs: cloneArtifactInputs(family.Inputs), Vars: vars,
|
||||
Inputs: inputs, Vars: vars,
|
||||
}
|
||||
outputOwners[outputPath] = key
|
||||
familyOrigin.Members = append(familyOrigin.Members, key)
|
||||
catalog.Members[key] = ArtifactFamilyMemberOrigin{Family: familyKey, CharacterID: character.ID, Source: familyOrigin.Source}
|
||||
catalog.Members[key] = ArtifactFamilyMemberOrigin{Family: familyKey, CharacterID: character.ID, Source: familyOrigin.Source, Dependencies: append([]string(nil), dependencies...), Inputs: inputSourceMap(inputs)}
|
||||
}
|
||||
catalog.Families[familyKey] = familyOrigin
|
||||
}
|
||||
@@ -109,6 +124,9 @@ func expandPipelineArtifactFamilies(cfg *PipelineConfig, party ResolvedParty) er
|
||||
cfg.Scriptorium.ArtifactFamilies = nil
|
||||
cfg.familyCatalog = catalog
|
||||
cfg.resolution.artifactFamiliesExpanded = true
|
||||
if err := rejectConcreteMemberArtifactSources(expanded); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateScriptorium(cfg.Scriptorium, cfg.Notarius); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -118,6 +136,127 @@ func expandPipelineArtifactFamilies(cfg *PipelineConfig, party ResolvedParty) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateFamilyMemberDependencies(families map[string]ScriptoriumArtifactFamilyConfig, familyKeys []string) error {
|
||||
for _, familyKey := range familyKeys {
|
||||
family := families[familyKey]
|
||||
seen := make(map[string]struct{}, len(family.MemberDependencies))
|
||||
for index, raw := range family.MemberDependencies {
|
||||
dependency := strings.TrimSpace(raw)
|
||||
prefix := fmt.Sprintf("pipeline.scriptorium.artifact_families.%s.member_dependencies[%d]", familyKey, index)
|
||||
if !artifactpolicy.IsConfiguredKey(dependency) {
|
||||
return fmt.Errorf("%s must be a valid family key", prefix)
|
||||
}
|
||||
if dependency == familyKey {
|
||||
return fmt.Errorf("%s must not reference its own family", prefix)
|
||||
}
|
||||
if _, duplicate := seen[dependency]; duplicate {
|
||||
return fmt.Errorf("%s duplicates family %q", prefix, dependency)
|
||||
}
|
||||
seen[dependency] = struct{}{}
|
||||
dependencyFamily, exists := families[dependency]
|
||||
if !exists {
|
||||
return fmt.Errorf("%s references unknown family %q", prefix, dependency)
|
||||
}
|
||||
if strings.TrimSpace(dependencyFamily.ForEach) != partyCharactersFamilySource {
|
||||
return fmt.Errorf("%s family %q must use %q", prefix, dependency, partyCharactersFamilySource)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func expandedFamilyDependencies(prefix string, family ScriptoriumArtifactFamilyConfig, characterID string) ([]string, error) {
|
||||
dependencies := append([]string(nil), family.DependsOn...)
|
||||
memberDependencies := append([]string(nil), family.MemberDependencies...)
|
||||
sort.Strings(memberDependencies)
|
||||
for _, familyKey := range memberDependencies {
|
||||
dependencies = append(dependencies, familyKey+"_"+characterID)
|
||||
}
|
||||
return normalizedFamilyDependencies(prefix, dependencies), nil
|
||||
}
|
||||
|
||||
func normalizedFamilyDependencies(_ string, dependencies []string) []string {
|
||||
seen := make(map[string]struct{}, len(dependencies))
|
||||
normalized := make([]string, 0, len(dependencies))
|
||||
for _, dependency := range dependencies {
|
||||
dependency = strings.TrimSpace(dependency)
|
||||
if _, exists := seen[dependency]; exists {
|
||||
continue
|
||||
}
|
||||
seen[dependency] = struct{}{}
|
||||
normalized = append(normalized, dependency)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func expandFamilyInputs(prefix string, family ScriptoriumArtifactFamilyConfig, characterID string) (map[string]ScriptoriumInputConfig, error) {
|
||||
inputs := cloneArtifactInputs(family.Inputs)
|
||||
for name, input := range inputs {
|
||||
familyKey, matched, err := parseMemberArtifactSource(input.Source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s.inputs.%s.source: %w", prefix, name, err)
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
if !familyDependencyContains(family.MemberDependencies, familyKey) {
|
||||
return nil, fmt.Errorf("%s.inputs.%s.source %q requires member_dependencies entry %q", prefix, name, input.Source, familyKey)
|
||||
}
|
||||
input.Source = artifactpolicy.ConfiguredSourceID(familyKey + "_" + characterID)
|
||||
inputs[name] = input
|
||||
}
|
||||
return inputs, nil
|
||||
}
|
||||
|
||||
func parseMemberArtifactSource(source string) (string, bool, error) {
|
||||
trimmed := strings.TrimSpace(source)
|
||||
if !strings.HasPrefix(trimmed, "narratio.member_artifact") {
|
||||
return "", false, nil
|
||||
}
|
||||
if !strings.HasPrefix(trimmed, memberArtifactSourcePrefix) {
|
||||
return "", true, fmt.Errorf("malformed member artifact source %q", source)
|
||||
}
|
||||
family := strings.TrimPrefix(trimmed, memberArtifactSourcePrefix)
|
||||
if !artifactpolicy.IsConfiguredKey(family) {
|
||||
return "", true, fmt.Errorf("malformed member artifact source %q", source)
|
||||
}
|
||||
return family, true, nil
|
||||
}
|
||||
|
||||
func rejectConcreteMemberArtifactSources(artifacts map[string]ScriptoriumArtifactConfig) error {
|
||||
for artifactKey, artifact := range artifacts {
|
||||
for inputName, input := range artifact.Inputs {
|
||||
if _, matched, err := parseMemberArtifactSource(input.Source); matched {
|
||||
if err != nil {
|
||||
return fmt.Errorf("pipeline.scriptorium.artifacts.%s.inputs.%s.source: %w", artifactKey, inputName, err)
|
||||
}
|
||||
return fmt.Errorf("pipeline.scriptorium.artifacts.%s.inputs.%s.source uses member artifact syntax outside an artifact family", artifactKey, inputName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func familyDependencyContains(values []string, want string) bool {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func inputSourceMap(inputs map[string]ScriptoriumInputConfig) map[string]string {
|
||||
if len(inputs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(inputs))
|
||||
for name, input := range inputs {
|
||||
out[name] = input.Source
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func validateFamilyOutputPattern(field, pattern string) error {
|
||||
if strings.Count(pattern, characterIDToken) != 1 {
|
||||
return fmt.Errorf("%s must contain exactly one %s token", field, characterIDToken)
|
||||
@@ -248,6 +387,14 @@ func cloneArtifactFamilyCatalog(in ArtifactFamilyCatalog) ArtifactFamilyCatalog
|
||||
out.Families[key] = family
|
||||
}
|
||||
for key, member := range in.Members {
|
||||
member.Dependencies = append([]string(nil), member.Dependencies...)
|
||||
if len(member.Inputs) > 0 {
|
||||
inputs := make(map[string]string, len(member.Inputs))
|
||||
for name, source := range member.Inputs {
|
||||
inputs[name] = source
|
||||
}
|
||||
member.Inputs = inputs
|
||||
}
|
||||
out.Members[key] = member
|
||||
}
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user