diff --git a/go.mod b/go.mod index e2be7856..e15c4702 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module fn-registry go 1.22.2 -require github.com/mattn/go-sqlite3 v1.14.37 +require ( + github.com/mattn/go-sqlite3 v1.14.37 + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/go.sum b/go.sum index 9c79a75d..710b3523 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/registry/parser.go b/registry/parser.go new file mode 100644 index 00000000..e4212667 --- /dev/null +++ b/registry/parser.go @@ -0,0 +1,208 @@ +package registry + +import ( + "bytes" + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v3" +) + +// rawFunction mirrors the YAML frontmatter of a function .md file. +type rawFunction struct { + Name string `yaml:"name"` + Kind string `yaml:"kind"` + Lang string `yaml:"lang"` + Domain string `yaml:"domain"` + Version string `yaml:"version"` + Purity string `yaml:"purity"` + Signature string `yaml:"signature"` + Description string `yaml:"description"` + Tags []string `yaml:"tags"` + UsesFunctions []string `yaml:"uses_functions"` + UsesTypes []string `yaml:"uses_types"` + Returns []string `yaml:"returns"` + ReturnsOptional bool `yaml:"returns_optional"` + ErrorType string `yaml:"error_type"` + Imports []string `yaml:"imports"` + Tested bool `yaml:"tested"` + Tests []string `yaml:"tests"` + TestFilePath string `yaml:"test_file_path"` + FilePath string `yaml:"file_path"` + + // Component fields + Props []PropDef `yaml:"props"` + Emits []string `yaml:"emits"` + HasState *bool `yaml:"has_state"` + Framework string `yaml:"framework"` + Variant []string `yaml:"variant"` +} + +// rawType mirrors the YAML frontmatter of a type .md file. +type rawType struct { + Name string `yaml:"name"` + Lang string `yaml:"lang"` + Domain string `yaml:"domain"` + Version string `yaml:"version"` + Algebraic string `yaml:"algebraic"` + Definition string `yaml:"definition"` + Description string `yaml:"description"` + Tags []string `yaml:"tags"` + UsesTypes []string `yaml:"uses_types"` + FilePath string `yaml:"file_path"` +} + +// extractFrontmatter splits a .md file into YAML frontmatter and body. +func extractFrontmatter(data []byte) ([]byte, []byte, error) { + content := data + if !bytes.HasPrefix(content, []byte("---\n")) && !bytes.HasPrefix(content, []byte("---\r\n")) { + return nil, nil, fmt.Errorf("missing opening --- in frontmatter") + } + + // Skip opening --- + rest := content[4:] + idx := bytes.Index(rest, []byte("\n---")) + if idx < 0 { + return nil, nil, fmt.Errorf("missing closing --- in frontmatter") + } + + fm := rest[:idx] + body := rest[idx+4:] // skip \n--- + return fm, body, nil +} + +// ParseFunctionMD parses a function .md file into a Function. +func ParseFunctionMD(path string) (*Function, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", path, err) + } + + fm, body, err := extractFrontmatter(data) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", path, err) + } + + var raw rawFunction + if err := yaml.Unmarshal(fm, &raw); err != nil { + return nil, fmt.Errorf("parsing YAML in %s: %w", path, err) + } + + if raw.Name == "" { + return nil, fmt.Errorf("%s: name is required", path) + } + if raw.Kind == "" { + return nil, fmt.Errorf("%s: kind is required", path) + } + if raw.Description == "" { + return nil, fmt.Errorf("%s: description is required", path) + } + + example := extractExample(body) + + f := &Function{ + ID: GenerateID(raw.Name, raw.Lang, raw.Domain), + Name: raw.Name, + Kind: Kind(raw.Kind), + Lang: raw.Lang, + Domain: raw.Domain, + Version: raw.Version, + Purity: Purity(raw.Purity), + Signature: raw.Signature, + Description: raw.Description, + Tags: raw.Tags, + UsesFunctions: raw.UsesFunctions, + UsesTypes: raw.UsesTypes, + Returns: raw.Returns, + ReturnsOptional: raw.ReturnsOptional, + ErrorType: raw.ErrorType, + Imports: raw.Imports, + Tested: raw.Tested, + Tests: raw.Tests, + TestFilePath: raw.TestFilePath, + FilePath: raw.FilePath, + Props: raw.Props, + Emits: raw.Emits, + HasState: raw.HasState, + Framework: raw.Framework, + Variant: raw.Variant, + } + + if example != "" && f.Example == "" { + f.Example = example + } + + return f, nil +} + +// ParseTypeMD parses a type .md file into a Type. +func ParseTypeMD(path string) (*Type, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", path, err) + } + + fm, _, err := extractFrontmatter(data) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", path, err) + } + + var raw rawType + if err := yaml.Unmarshal(fm, &raw); err != nil { + return nil, fmt.Errorf("parsing YAML in %s: %w", path, err) + } + + if raw.Name == "" { + return nil, fmt.Errorf("%s: name is required", path) + } + if raw.Description == "" { + return nil, fmt.Errorf("%s: description is required", path) + } + + t := &Type{ + ID: GenerateID(raw.Name, raw.Lang, raw.Domain), + Name: raw.Name, + Lang: raw.Lang, + Domain: raw.Domain, + Version: raw.Version, + Algebraic: Algebraic(raw.Algebraic), + Definition: strings.TrimSpace(raw.Definition), + Description: raw.Description, + Tags: raw.Tags, + UsesTypes: raw.UsesTypes, + FilePath: raw.FilePath, + } + + return t, nil +} + +// extractExample pulls the first code block after an "## Ejemplo" heading. +func extractExample(body []byte) string { + lines := strings.Split(string(body), "\n") + inExample := false + inCode := false + var code []string + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "## Ejemplo") { + inExample = true + continue + } + if inExample && !inCode && strings.HasPrefix(trimmed, "```") { + inCode = true + continue + } + if inCode { + if strings.HasPrefix(trimmed, "```") { + return strings.Join(code, "\n") + } + code = append(code, line) + } + if inExample && !inCode && strings.HasPrefix(trimmed, "##") { + break + } + } + return "" +}