update databasing system with minimum API, schema parsing and FreeCAD
integration
This commit is contained in:
235
internal/schema/schema.go
Normal file
235
internal/schema/schema.go
Normal file
@@ -0,0 +1,235 @@
|
||||
// Package schema handles YAML schema parsing and validation for part numbering.
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Schema represents a part numbering schema.
|
||||
type Schema struct {
|
||||
Name string `yaml:"name"`
|
||||
Version int `yaml:"version"`
|
||||
Description string `yaml:"description"`
|
||||
Separator string `yaml:"separator"`
|
||||
Uniqueness Uniqueness `yaml:"uniqueness"`
|
||||
Segments []Segment `yaml:"segments"`
|
||||
Format string `yaml:"format"`
|
||||
Examples []string `yaml:"examples"`
|
||||
PropertySchemas *PropertySchemas `yaml:"property_schemas,omitempty"`
|
||||
}
|
||||
|
||||
// PropertySchemas defines property schemas per category.
|
||||
type PropertySchemas struct {
|
||||
Version int `yaml:"version" json:"version"`
|
||||
Defaults map[string]PropertyDefinition `yaml:"defaults" json:"defaults"`
|
||||
Categories map[string]map[string]PropertyDefinition `yaml:"categories" json:"categories"`
|
||||
}
|
||||
|
||||
// PropertyDefinition defines a single property's schema.
|
||||
type PropertyDefinition struct {
|
||||
Type string `yaml:"type" json:"type"` // string, number, boolean, date
|
||||
Default any `yaml:"default" json:"default"`
|
||||
Required bool `yaml:"required,omitempty" json:"required,omitempty"`
|
||||
Unit string `yaml:"unit,omitempty" json:"unit,omitempty"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// GetPropertiesForCategory returns merged properties for a category.
|
||||
func (ps *PropertySchemas) GetPropertiesForCategory(category string) map[string]PropertyDefinition {
|
||||
result := make(map[string]PropertyDefinition)
|
||||
|
||||
// Start with defaults
|
||||
for k, v := range ps.Defaults {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
// Add category-specific (first character of category code)
|
||||
if len(category) > 0 {
|
||||
categoryPrefix := string(category[0])
|
||||
if catProps, ok := ps.Categories[categoryPrefix]; ok {
|
||||
for k, v := range catProps {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ApplyDefaults fills in missing properties with defaults.
|
||||
func (ps *PropertySchemas) ApplyDefaults(properties map[string]any, category string) map[string]any {
|
||||
if properties == nil {
|
||||
properties = make(map[string]any)
|
||||
}
|
||||
|
||||
defs := ps.GetPropertiesForCategory(category)
|
||||
|
||||
for key, def := range defs {
|
||||
if _, exists := properties[key]; !exists && def.Default != nil {
|
||||
properties[key] = def.Default
|
||||
}
|
||||
}
|
||||
|
||||
return properties
|
||||
}
|
||||
|
||||
// Uniqueness defines how part number uniqueness is enforced.
|
||||
type Uniqueness struct {
|
||||
Scope string `yaml:"scope"` // global, per-project, per-type, per-schema
|
||||
CaseSensitive bool `yaml:"case_sensitive"` // default false
|
||||
}
|
||||
|
||||
// Segment represents a part number segment.
|
||||
type Segment struct {
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"` // string, enum, serial, date, constant
|
||||
Length int `yaml:"length"`
|
||||
MinLength int `yaml:"min_length"`
|
||||
MaxLength int `yaml:"max_length"`
|
||||
Case string `yaml:"case"` // upper, lower, preserve
|
||||
Padding string `yaml:"padding"`
|
||||
Start int `yaml:"start"`
|
||||
Scope string `yaml:"scope"` // template for serial scope
|
||||
Value string `yaml:"value"` // for constant type
|
||||
Values map[string]string `yaml:"values"`
|
||||
Validation Validation `yaml:"validation"`
|
||||
Required bool `yaml:"required"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
// Validation defines validation rules for a segment.
|
||||
type Validation struct {
|
||||
Pattern string `yaml:"pattern"`
|
||||
Message string `yaml:"message"`
|
||||
}
|
||||
|
||||
// SchemaFile wraps the schema in a file structure.
|
||||
type SchemaFile struct {
|
||||
Schema Schema `yaml:"schema"`
|
||||
}
|
||||
|
||||
// Load reads a schema from a YAML file.
|
||||
func Load(path string) (*Schema, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading schema file: %w", err)
|
||||
}
|
||||
|
||||
var sf SchemaFile
|
||||
if err := yaml.Unmarshal(data, &sf); err != nil {
|
||||
return nil, fmt.Errorf("parsing schema YAML: %w", err)
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if sf.Schema.Separator == "" {
|
||||
sf.Schema.Separator = "-"
|
||||
}
|
||||
if sf.Schema.Uniqueness.Scope == "" {
|
||||
sf.Schema.Uniqueness.Scope = "global"
|
||||
}
|
||||
|
||||
return &sf.Schema, nil
|
||||
}
|
||||
|
||||
// LoadAll reads all schemas from a directory.
|
||||
func LoadAll(dir string) (map[string]*Schema, error) {
|
||||
schemas := make(map[string]*Schema)
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading schema directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(entry.Name(), ".yaml") && !strings.HasSuffix(entry.Name(), ".yml") {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, entry.Name())
|
||||
schema, err := Load(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading %s: %w", entry.Name(), err)
|
||||
}
|
||||
schemas[schema.Name] = schema
|
||||
}
|
||||
|
||||
return schemas, nil
|
||||
}
|
||||
|
||||
// Validate checks that the schema definition is valid.
|
||||
func (s *Schema) Validate() error {
|
||||
if s.Name == "" {
|
||||
return fmt.Errorf("schema name is required")
|
||||
}
|
||||
if len(s.Segments) == 0 {
|
||||
return fmt.Errorf("schema must have at least one segment")
|
||||
}
|
||||
|
||||
for i, seg := range s.Segments {
|
||||
if err := seg.Validate(); err != nil {
|
||||
return fmt.Errorf("segment %d (%s): %w", i, seg.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks that a segment definition is valid.
|
||||
func (seg *Segment) Validate() error {
|
||||
if seg.Name == "" {
|
||||
return fmt.Errorf("segment name is required")
|
||||
}
|
||||
|
||||
validTypes := map[string]bool{
|
||||
"string": true,
|
||||
"enum": true,
|
||||
"serial": true,
|
||||
"date": true,
|
||||
"constant": true,
|
||||
}
|
||||
if !validTypes[seg.Type] {
|
||||
return fmt.Errorf("invalid segment type: %s", seg.Type)
|
||||
}
|
||||
|
||||
switch seg.Type {
|
||||
case "enum":
|
||||
if len(seg.Values) == 0 {
|
||||
return fmt.Errorf("enum segment requires values")
|
||||
}
|
||||
case "constant":
|
||||
if seg.Value == "" {
|
||||
return fmt.Errorf("constant segment requires value")
|
||||
}
|
||||
case "serial":
|
||||
if seg.Length <= 0 {
|
||||
return fmt.Errorf("serial segment requires positive length")
|
||||
}
|
||||
}
|
||||
|
||||
if seg.Validation.Pattern != "" {
|
||||
if _, err := regexp.Compile(seg.Validation.Pattern); err != nil {
|
||||
return fmt.Errorf("invalid validation pattern: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSegment returns a segment by name.
|
||||
func (s *Schema) GetSegment(name string) *Segment {
|
||||
for i := range s.Segments {
|
||||
if s.Segments[i].Name == name {
|
||||
return &s.Segments[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user