Files
fn_registry/registry/parser.go
T
egutierrez 94a1a81297 feat: parser de frontmatter YAML para .md de funciones y tipos
Implementa ParseFunctionMD y ParseTypeMD que extraen frontmatter YAML
de archivos .md y construyen los structs del registry. Soporta los tres
kinds (function, pipeline, component) con sus campos especificos.
Extrae automaticamente el ejemplo del body si existe seccion ## Ejemplo.

Dependencia: gopkg.in/yaml.v3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 02:07:06 +01:00

209 lines
5.6 KiB
Go

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 ""
}