Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Binary output
coding-context
coding-context-cli
*.test
89 changes: 89 additions & 0 deletions pkg/codingcontext/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package codingcontext_test

import (
"context"
"fmt"
"log"

"github.com/kitproj/coding-context-cli/pkg/codingcontext"
)

// TaskMetadata represents the custom frontmatter structure for a task
type TaskMetadata struct {
TaskName string `yaml:"task_name"`
Resume bool `yaml:"resume"`
Priority string `yaml:"priority"`
Environment string `yaml:"environment"`
Selectors map[string]any `yaml:"selectors"`
}

// ExampleMarkdown_ParseFrontmatter demonstrates how to parse task frontmatter
// into a custom struct when using the coding-context library.
func ExampleMarkdown_ParseFrontmatter() {

Check failure on line 22 in pkg/codingcontext/example_test.go

View workflow job for this annotation

GitHub Actions / build

unreachable func: ExampleMarkdown_ParseFrontmatter

Check failure on line 22 in pkg/codingcontext/example_test.go

View workflow job for this annotation

GitHub Actions / build

unreachable func: ExampleMarkdown_ParseFrontmatter
// Create a context and run it to get a result
// In a real application, you would configure this properly
cc := codingcontext.New(
codingcontext.WithWorkDir("."),
)

// Assuming there's a task file with frontmatter like:
// ---
// task_name: deploy
// priority: high
// environment: production
// ---
result, err := cc.Run(context.Background(), "deploy")
if err != nil {
log.Fatal(err)
}

// Parse the task frontmatter into your custom struct
var taskMeta TaskMetadata
if err := result.Task.ParseFrontmatter(&taskMeta); err != nil {
log.Fatal(err)
}

// Now you can use the strongly-typed task metadata
fmt.Printf("Task: %s\n", taskMeta.TaskName)
fmt.Printf("Priority: %s\n", taskMeta.Priority)
fmt.Printf("Environment: %s\n", taskMeta.Environment)

// You can also parse rule frontmatter the same way
for _, rule := range result.Rules {
var ruleMeta struct {
Language string `yaml:"language"`
Stage string `yaml:"stage"`
}
if err := rule.ParseFrontmatter(&ruleMeta); err == nil {
fmt.Printf("Rule: language=%s, stage=%s\n", ruleMeta.Language, ruleMeta.Stage)
}
}
}

// ExampleParseMarkdownFile demonstrates how to parse a markdown file
// with frontmatter into a custom struct.
func ExampleParseMarkdownFile() {

Check failure on line 65 in pkg/codingcontext/example_test.go

View workflow job for this annotation

GitHub Actions / build

unreachable func: ExampleParseMarkdownFile

Check failure on line 65 in pkg/codingcontext/example_test.go

View workflow job for this annotation

GitHub Actions / build

unreachable func: ExampleParseMarkdownFile
// Define your custom struct with yaml tags
type TaskFrontmatter struct {
TaskName string `yaml:"task_name"`
Resume bool `yaml:"resume"`
Priority string `yaml:"priority"`
Tags []string `yaml:"tags"`
}

// Parse the markdown file
var frontmatter TaskFrontmatter
content, err := codingcontext.ParseMarkdownFile("path/to/task.md", &frontmatter)
if err != nil {
log.Fatal(err)
}

// Access the parsed frontmatter
fmt.Printf("Task: %s\n", frontmatter.TaskName)
fmt.Printf("Resume: %v\n", frontmatter.Resume)
fmt.Printf("Priority: %s\n", frontmatter.Priority)
fmt.Printf("Tags: %v\n", frontmatter.Tags)

// Access the content
fmt.Printf("Content length: %d\n", len(content))
}
118 changes: 118 additions & 0 deletions pkg/codingcontext/markdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,121 @@ func TestParseMarkdownFile_FileNotFound(t *testing.T) {
t.Error("ParseMarkdownFile() expected error for non-existent file, got nil")
}
}

func TestParseMarkdownFile_CustomStruct(t *testing.T) {
// Define a custom struct for task frontmatter
type TaskFrontmatter struct {
TaskName string `yaml:"task_name"`
Resume bool `yaml:"resume"`
Priority string `yaml:"priority"`
Tags []string `yaml:"tags"`
}

tests := []struct {
name string
content string
wantContent string
wantTaskName string
wantResume bool
wantPriority string
wantTags []string
wantErr bool
}{
{
name: "parse task with all fields",
content: `---
task_name: fix-bug
resume: false
priority: high
tags:
- backend
- urgent
---
# Fix Bug

Please fix the bug in the backend service.
`,
wantContent: "# Fix Bug\n\nPlease fix the bug in the backend service.\n",
wantTaskName: "fix-bug",
wantResume: false,
wantPriority: "high",
wantTags: []string{"backend", "urgent"},
wantErr: false,
},
{
name: "parse task with partial fields",
content: `---
task_name: deploy
resume: true
---
# Deploy Application

Deploy the application to staging.
`,
wantContent: "# Deploy Application\n\nDeploy the application to staging.\n",
wantTaskName: "deploy",
wantResume: true,
wantPriority: "", // zero value for missing field
wantTags: nil,
wantErr: false,
},
{
name: "parse without frontmatter",
content: `# Simple Task

This task has no frontmatter.
`,
wantContent: "# Simple Task\n\nThis task has no frontmatter.\n",
wantTaskName: "", // zero value
wantResume: false,
wantPriority: "",
wantTags: nil,
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a temporary file
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "test.md")
if err := os.WriteFile(tmpFile, []byte(tt.content), 0644); err != nil {
t.Fatalf("failed to create temp file: %v", err)
}

// Parse the file into custom struct
var frontmatter TaskFrontmatter
content, err := ParseMarkdownFile(tmpFile, &frontmatter)

// Check error
if (err != nil) != tt.wantErr {
t.Errorf("ParseMarkdownFile() error = %v, wantErr %v", err, tt.wantErr)
return
}

// Check content
if content != tt.wantContent {
t.Errorf("ParseMarkdownFile() content = %q, want %q", content, tt.wantContent)
}

// Check frontmatter fields
if frontmatter.TaskName != tt.wantTaskName {
t.Errorf("frontmatter.TaskName = %q, want %q", frontmatter.TaskName, tt.wantTaskName)
}
if frontmatter.Resume != tt.wantResume {
t.Errorf("frontmatter.Resume = %v, want %v", frontmatter.Resume, tt.wantResume)
}
if frontmatter.Priority != tt.wantPriority {
t.Errorf("frontmatter.Priority = %q, want %q", frontmatter.Priority, tt.wantPriority)
}
if len(frontmatter.Tags) != len(tt.wantTags) {
t.Errorf("frontmatter.Tags length = %d, want %d", len(frontmatter.Tags), len(tt.wantTags))
}
for i, tag := range tt.wantTags {
if i < len(frontmatter.Tags) && frontmatter.Tags[i] != tag {
t.Errorf("frontmatter.Tags[%d] = %q, want %q", i, frontmatter.Tags[i], tag)
}
}
})
}
}
43 changes: 43 additions & 0 deletions pkg/codingcontext/result.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package codingcontext

import (
"fmt"
"path/filepath"
"strings"

yaml "github.com/goccy/go-yaml"
)

// Markdown represents a markdown file with frontmatter and content
Expand All @@ -24,6 +27,46 @@ func (m *Markdown) BootstrapPath() string {
return baseNameWithoutExt + "-bootstrap"
}

// ParseFrontmatter unmarshals the frontmatter into the provided struct.
// The target parameter should be a pointer to a struct with yaml tags.
// Returns an error if the frontmatter cannot be unmarshaled into the target.
//
// Example:
//
// type TaskMeta struct {
// TaskName string `yaml:"task_name"`
// Resume bool `yaml:"resume"`
// Priority string `yaml:"priority"`
// }
//
// var meta TaskMeta
// if err := markdown.ParseFrontmatter(&meta); err != nil {
// // handle error
// }
func (m *Markdown) ParseFrontmatter(target any) error {
if target == nil {
return fmt.Errorf("target cannot be nil")
}

if m.FrontMatter == nil {
return fmt.Errorf("frontmatter is nil")
}

// Marshal the frontmatter map to YAML bytes, then unmarshal into target
// This approach leverages the existing YAML library without adding new dependencies
yamlBytes, err := yaml.Marshal(m.FrontMatter)
if err != nil {
return fmt.Errorf("failed to marshal frontmatter: %w", err)
}

// Unmarshal the YAML bytes into the target struct
if err := yaml.Unmarshal(yamlBytes, target); err != nil {
return fmt.Errorf("failed to unmarshal frontmatter into target: %w", err)
}

return nil
}

// Result holds the assembled context from running a task
type Result struct {
Rules []Markdown // List of included rule files
Expand Down
Loading