clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

main
atlas / format / semconv / registry.go
registry.go 4.7K
1package semconv23import (4 "fmt"5 "slices"6 "sort"7 "strconv"8 "strings"9)1011// This file is the hand-written half of the registry: the value checkers, and12// the reading and validating surface over them. The vocabulary itself --13// namespace, version, entities, stability tiers, key constants and the14// registry map -- is generated into registry_gen.go from spec/registry.yaml.15//16// The split is deliberate. A checker is code and stays code; the spec17// references it by name, so adding a checker is a Go edit and using one is a18// spec edit, and a spec naming a checker nobody wrote fails codegen rather19// than admitting a key that checks nothing.2021// definition is one registered key: where it attaches, how settled it is, and22// what values it admits.23type definition struct {24 entity Entity25 stability Stability26 check func(value string) error27}2829// Keys lists every registered key in a stable order, for the tools that30// report on adoption and for the tests that hold the registry to its prose.31func Keys() []string {32 out := make([]string, 0, len(registry))33 for key := range registry {34 out = append(out, key)35 }36 sort.Strings(out)37 return out38}3940// EntityOf reports where a key attaches. The false result is a reader's cue41// to ignore the attribute: an unregistered key is data from a vocabulary this42// build does not speak, never a reason to refuse a bundle.43func EntityOf(key string) (Entity, bool) {44 definition, known := registry[key]45 return definition.entity, known46}4748// StabilityOf reports how settled a key is.49func StabilityOf(key string) (Stability, bool) {50 definition, known := registry[key]51 return definition.stability, known52}5354// Check holds one attribute to the registry: registered, attached to this55// entity, and carrying a value its vocabulary admits. It is the single-key56// face of [Validate], for a producer building an attribute set one key at a57// time.58func Check(entity Entity, key, value string) error {59 definition, known := registry[key]60 if !known {61 return fmt.Errorf("attribute %q is not registered", key)62 }63 if definition.entity != entity {64 return fmt.Errorf("attribute %q attaches to a %s, not a %s", key, definition.entity, entity)65 }66 if err := definition.check(value); err != nil {67 return fmt.Errorf("attribute %q: %w", key, err)68 }69 return nil70}7172// Validate is the producer-strict gate: every key of attrs in the atlas73// namespace must pass [Check] against entity. Keys outside the namespace are74// not this registry's business and pass through.75//76// Attributes are visited in sorted order, so a set with several faults always77// reports the same one.78func Validate(entity Entity, attrs map[string]string) error {79 for _, key := range sortedKeys(attrs) {80 if !strings.HasPrefix(key, Namespace) {81 continue82 }83 if err := Check(entity, key, attrs[key]); err != nil {84 return err85 }86 }87 return nil88}8990func enum(values ...string) func(string) error {91 return func(value string) error {92 if slices.Contains(values, value) {93 return nil94 }95 return fmt.Errorf("%q is not one of %s", value, strings.Join(values, "|"))96 }97}9899func slug(value string) error {100 if value == "" {101 return fmt.Errorf("empty")102 }103 for _, r := range value {104 if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-' || r == '_' {105 continue106 }107 return fmt.Errorf("%q is not a slug", value)108 }109 return nil110}111112func decimal(value string) error {113 if _, err := strconv.ParseFloat(value, 64); err != nil {114 return fmt.Errorf("%q is not a number", value)115 }116 return nil117}118119func positiveDecimal(value string) error {120 parsed, err := strconv.ParseFloat(value, 64)121 if err != nil || parsed <= 0 {122 return fmt.Errorf("%q is not a positive number", value)123 }124 return nil125}126127func huc12(value string) error {128 if len(value) != 12 {129 return fmt.Errorf("%q is not twelve digits", value)130 }131 for _, r := range value {132 if r < '0' || r > '9' {133 return fmt.Errorf("%q is not twelve digits", value)134 }135 }136 return nil137}138139func setName(value string) error {140 set, name, found := strings.Cut(value, "/")141 if !found || set == "" || name == "" {142 return fmt.Errorf("%q is not set/name", value)143 }144 if err := slug(set); err != nil {145 return err146 }147 return slug(name)148}149150func numbers(count int) func(string) error {151 return func(value string) error {152 parts := strings.Split(value, ",")153 if len(parts) != count {154 return fmt.Errorf("%q wants %d comma-separated numbers", value, count)155 }156 for _, part := range parts {157 if _, err := strconv.ParseFloat(part, 64); err != nil {158 return fmt.Errorf("%q is not a number", part)159 }160 }161 return nil162 }163}164165func sortedKeys(attrs map[string]string) []string {166 out := make([]string, 0, len(attrs))167 for key := range attrs {168 out = append(out, key)169 }170 sort.Strings(out)171 return out172}