refactor: some architecture rework, added a single processing context that will be used to track dependencies

dev
Antonio De Lucreziis 2 years ago
parent 3f491e3e8f
commit f5af79c7d9

@ -40,12 +40,16 @@ type Operation interface {
Configure(options map[string]any) error Configure(options map[string]any) error
} }
type Context struct {
Files []string
}
type ListOperation interface { type ListOperation interface {
Operation Operation
ProcessList(items []Content) ([]Content, error) ProcessList(ctx *Context, items []Content) ([]Content, error)
} }
type ItemOperation interface { type ItemOperation interface {
Operation Operation
ProcessItem(item Content) (*Content, error) ProcessItem(ctx *Context, item Content) (*Content, error)
} }

@ -6,11 +6,9 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// Operation should have at least one key in "source", "use", "target". The remaining keys are options for that operation
type Operation map[string]any
type Pipeline struct { type Pipeline struct {
Pipeline []Operation `yaml:"pipeline"` // Pipeline is a list of operations, each one should have at least one key in "source", "use", "target". The remaining keys are options for that operation
Pipeline []map[string]any `yaml:"pipeline"`
} }
type BuildOptions struct { type BuildOptions struct {

@ -53,7 +53,9 @@ func (op *Categorize) Configure(config map[string]any) error {
return nil return nil
} }
func (op *Categorize) ProcessList(contents []cabret.Content) ([]cabret.Content, error) { var _ cabret.ListOperation = &Categorize{}
func (op *Categorize) ProcessList(ctx *cabret.Context, contents []cabret.Content) ([]cabret.Content, error) {
key := strcase.ToCamel(op.Key) key := strcase.ToCamel(op.Key)
categories := map[string][]cabret.Content{} categories := map[string][]cabret.Content{}

@ -30,7 +30,9 @@ func (op *Chunk) Configure(options map[string]any) error {
return nil return nil
} }
func (op *Chunk) ProcessList(items []cabret.Content) ([]cabret.Content, error) { var _ cabret.ListOperation = &Chunk{}
func (op *Chunk) ProcessList(ctx *cabret.Context, items []cabret.Content) ([]cabret.Content, error) {
totalChunks := len(items) / op.Size totalChunks := len(items) / op.Size
chunks := make([][]cabret.Content, totalChunks, totalChunks+1) chunks := make([][]cabret.Content, totalChunks, totalChunks+1)

@ -23,7 +23,9 @@ func (op *Frontmatter) Configure(config map[string]any) error {
return nil return nil
} }
func (op *Frontmatter) ProcessItem(content cabret.Content) (*cabret.Content, error) { var _ cabret.ItemOperation = &Frontmatter{}
func (op *Frontmatter) ProcessItem(ctx *cabret.Context, content cabret.Content) (*cabret.Content, error) {
md := goldmark.New( md := goldmark.New(
goldmark.WithExtensions( goldmark.WithExtensions(
meta.Meta, meta.Meta,

@ -64,7 +64,9 @@ func (op *Layout) Configure(config map[string]any) error {
return fmt.Errorf(`invalid config`) return fmt.Errorf(`invalid config`)
} }
func (op Layout) ProcessItem(content cabret.Content) (*cabret.Content, error) { var _ cabret.ItemOperation = &Layout{}
func (op Layout) ProcessItem(ctx *cabret.Context, content cabret.Content) (*cabret.Content, error) {
// expand glob patterns // expand glob patterns
tmplFiles := []string{} tmplFiles := []string{}
for _, pat := range op.TemplatePatterns { for _, pat := range op.TemplatePatterns {
@ -82,17 +84,17 @@ func (op Layout) ProcessItem(content cabret.Content) (*cabret.Content, error) {
return nil, err return nil, err
} }
ctx := util.CloneMap(content.Metadata) metadata := util.CloneMap(content.Metadata)
if content.Type == HtmlMimeType { if content.Type == HtmlMimeType {
ctx["Content"] = goHtmlTemplate.HTML(content.Data) metadata["Content"] = goHtmlTemplate.HTML(content.Data)
} else { } else {
ctx["Content"] = content.Data metadata["Content"] = content.Data
} }
log.Printf(`[operation.Layout] rendering into layout "%s"`, strings.Join(op.TemplatePatterns, ", ")) log.Printf(`[operation.Layout] rendering into layout "%s"`, strings.Join(op.TemplatePatterns, ", "))
data, err := tmpl.Render(ctx) data, err := tmpl.Render(metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -24,7 +24,9 @@ func (op *Markdown) Configure(config map[string]any) error {
return nil return nil
} }
func (op Markdown) ProcessItem(content cabret.Content) (*cabret.Content, error) { var _ cabret.ItemOperation = &Markdown{}
func (op Markdown) ProcessItem(ctx *cabret.Context, content cabret.Content) (*cabret.Content, error) {
md := goldmark.New( md := goldmark.New(
goldmark.WithExtensions( goldmark.WithExtensions(
extension.GFM, extension.GFM,

@ -107,7 +107,9 @@ func (op *Program) Configure(options map[string]any) error {
return nil return nil
} }
func (op *Program) ProcessItem(item cabret.Content) (*cabret.Content, error) { var _ cabret.ItemOperation = &Program{}
func (op *Program) ProcessItem(ctx *cabret.Context, item cabret.Content) (*cabret.Content, error) {
ioFmt, ok := ioProgramFormats[op.IOFormat] ioFmt, ok := ioProgramFormats[op.IOFormat]
if !ok { if !ok {
return nil, fmt.Errorf(`unknown io format "%s"`, op.IOFormat) return nil, fmt.Errorf(`unknown io format "%s"`, op.IOFormat)

@ -2,6 +2,7 @@ package operation
import ( import (
"fmt" "fmt"
"log"
"reflect" "reflect"
"github.com/aziis98/cabret" "github.com/aziis98/cabret"
@ -13,6 +14,13 @@ const ShortFormValueKey = "value"
var registry = map[string]reflect.Type{} var registry = map[string]reflect.Type{}
func registerType(name string, op cabret.Operation) { func registerType(name string, op cabret.Operation) {
switch op.(type) {
case cabret.ItemOperation:
case cabret.ListOperation:
default:
log.Fatal(`operation must also satisfy one of cabret.ListOperation or cabret.ItemOperation`)
}
typ := reflect.TypeOf(op).Elem() typ := reflect.TypeOf(op).Elem()
registry[name] = typ registry[name] = typ
} }

@ -36,7 +36,9 @@ func (op *Slice) Configure(options map[string]any) error {
return nil return nil
} }
func (op *Slice) ProcessList(items []cabret.Content) ([]cabret.Content, error) { var _ cabret.ListOperation = &Slice{}
func (op *Slice) ProcessList(ctx *cabret.Context, items []cabret.Content) ([]cabret.Content, error) {
from := op.From from := op.From
to := op.To to := op.To

@ -53,22 +53,24 @@ func (op *Source) Configure(config map[string]any) error {
return fmt.Errorf(`invalid config for "source": %#v`, config) return fmt.Errorf(`invalid config for "source": %#v`, config)
} }
func (op Source) ProcessList(contents []cabret.Content) ([]cabret.Content, error) { var _ cabret.ListOperation = &Source{}
files, err := cabret.FindFiles([]string{})
func (op Source) ProcessList(ctx *cabret.Context, contents []cabret.Content) ([]cabret.Content, error) {
files, err := cabret.FindFiles()
if err != nil { if err != nil {
return nil, err return nil, err
} }
matches := []cabret.MatchResult{} matches := []cabret.MatchResult{}
for _, patternStr := range op.Patterns { for _, rawPattern := range op.Patterns {
pat, err := path.ParsePattern(patternStr) pattern, err := path.ParsePattern(rawPattern)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, f := range files { for _, f := range files {
if ok, captures, _ := pat.Match(f); ok { if ok, captures, _ := pattern.Match(f); ok {
matches = append(matches, cabret.MatchResult{ matches = append(matches, cabret.MatchResult{
File: f, File: f,
Captures: captures, Captures: captures,

@ -36,7 +36,9 @@ func (op *Target) Configure(config map[string]any) error {
return fmt.Errorf(`invalid config for "target": %#v`, config) return fmt.Errorf(`invalid config for "target": %#v`, config)
} }
func (op Target) ProcessItem(c cabret.Content) (*cabret.Content, error) { var _ cabret.ItemOperation = &Target{}
func (op Target) ProcessItem(ctx *cabret.Context, c cabret.Content) (*cabret.Content, error) {
log.Printf(`[operation.Target] expanding pattern "%s"`, op.PathTemplate) log.Printf(`[operation.Target] expanding pattern "%s"`, op.PathTemplate)
target := op.PathTemplate target := op.PathTemplate

@ -32,7 +32,9 @@ func (op *Template) Configure(options map[string]any) error {
return nil return nil
} }
func (op *Template) ProcessList(items []cabret.Content) ([]cabret.Content, error) { var _ cabret.ListOperation = &Template{}
func (op *Template) ProcessList(ctx *cabret.Context, items []cabret.Content) ([]cabret.Content, error) {
var t bytes.Buffer var t bytes.Buffer
// concatenate all templates // concatenate all templates

@ -8,11 +8,12 @@ import (
"github.com/aziis98/cabret/operation" "github.com/aziis98/cabret/operation"
) )
func switchMap(m map[string]any, v *any) func(k string) bool { // switchMapHasKey returns a function that returns true if the map "m" contains the given key and binds the corresponding value to the provided "target" pointer, useful for writing map key checks using a switch instead of an if chain
func switchMapHasKey(m map[string]any, target *any) func(k string) bool {
return func(k string) bool { return func(k string) bool {
val, ok := m[k] val, ok := m[k]
if ok { if ok {
*v = val *target = val
} }
return ok return ok
} }
@ -21,48 +22,48 @@ func switchMap(m map[string]any, v *any) func(k string) bool {
func ParsePipeline(p config.Pipeline) ([]cabret.Operation, error) { func ParsePipeline(p config.Pipeline) ([]cabret.Operation, error) {
ops := []cabret.Operation{} ops := []cabret.Operation{}
for _, opConfig := range p.Pipeline { for _, operationConfig := range p.Pipeline {
var v any var rawValue any
has := switchMap(opConfig, &v) hasKey := switchMapHasKey(operationConfig, &rawValue)
switch { switch {
case has("source"): case hasKey("source"):
value, ok := v.(string) value, ok := rawValue.(string)
if !ok { if !ok {
return nil, fmt.Errorf(`expected string but got "%v" of type %T`, v, v) return nil, fmt.Errorf(`expected string but got "%v" of type %T`, rawValue, rawValue)
} }
opConfig[operation.ShortFormValueKey] = value operationConfig[operation.ShortFormValueKey] = value
op, err := ParseOperation("source", opConfig) op, err := ParseOperation("source", operationConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ops = append(ops, op) ops = append(ops, op)
case has("target"): case hasKey("target"):
value, ok := v.(string) value, ok := rawValue.(string)
if !ok { if !ok {
return nil, fmt.Errorf(`expected string but got "%v" of type %T`, v, v) return nil, fmt.Errorf(`expected string but got "%v" of type %T`, rawValue, rawValue)
} }
opConfig[operation.ShortFormValueKey] = value operationConfig[operation.ShortFormValueKey] = value
op, err := ParseOperation("target", opConfig) op, err := ParseOperation("target", operationConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ops = append(ops, op) ops = append(ops, op)
case has("use"): case hasKey("use"):
name, ok := v.(string) name, ok := rawValue.(string)
if !ok { if !ok {
return nil, fmt.Errorf(`expected string but got "%v" of type %T`, v, v) return nil, fmt.Errorf(`expected string but got "%v" of type %T`, rawValue, rawValue)
} }
op, err := ParseOperation(name, opConfig) op, err := ParseOperation(name, operationConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -70,7 +71,7 @@ func ParsePipeline(p config.Pipeline) ([]cabret.Operation, error) {
ops = append(ops, op) ops = append(ops, op)
default: default:
return nil, fmt.Errorf(`pipeline entry is missing one of "use", "source" or "target", got %#v`, opConfig) return nil, fmt.Errorf(`pipeline entry is missing one of "use", "source" or "target", got %#v`, operationConfig)
} }
} }

@ -9,13 +9,22 @@ import (
) )
func RunConfig(cfg *config.Cabretfile) error { func RunConfig(cfg *config.Cabretfile) error {
files, err := cabret.FindFiles(cfg.Options.Excludes...)
if err != nil {
return err
}
ctx := &cabret.Context{
Files: files,
}
for _, p := range cfg.Build { for _, p := range cfg.Build {
ops, err := parse.ParsePipeline(p) ops, err := parse.ParsePipeline(p)
if err != nil { if err != nil {
return err return err
} }
if _, err := RunPipeline([]cabret.Content{}, ops); err != nil { if _, err := RunPipeline(ops, ctx, []cabret.Content{}); err != nil {
return err return err
} }
} }
@ -23,10 +32,11 @@ func RunConfig(cfg *config.Cabretfile) error {
return nil return nil
} }
func RunPipeline(contents []cabret.Content, ops []cabret.Operation) ([]cabret.Content, error) { // RunPipeline runs the given pipeline "ops" with the context ctx and initial contents "contents"
for _, op := range ops { func RunPipeline(operations []cabret.Operation, ctx *cabret.Context, contents []cabret.Content) ([]cabret.Content, error) {
for _, op := range operations {
var err error var err error
contents, err = RunOperation(op, contents) contents, err = RunOperation(op, ctx, contents)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -35,15 +45,15 @@ func RunPipeline(contents []cabret.Content, ops []cabret.Operation) ([]cabret.Co
return contents, nil return contents, nil
} }
func RunOperation(op cabret.Operation, inputs []cabret.Content) ([]cabret.Content, error) { func RunOperation(op cabret.Operation, ctx *cabret.Context, inputs []cabret.Content) ([]cabret.Content, error) {
switch op := op.(type) { switch op := op.(type) {
case cabret.ListOperation: case cabret.ListOperation:
return op.ProcessList(inputs) return op.ProcessList(ctx, inputs)
case cabret.ItemOperation: case cabret.ItemOperation:
outputs := []cabret.Content{} outputs := []cabret.Content{}
for _, item := range inputs { for _, item := range inputs {
result, err := op.ProcessItem(item) result, err := op.ProcessItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -8,30 +8,30 @@ import (
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
func FindFiles(excludes []string) ([]string, error) { func FindFiles(excludePatterns ...string) ([]string, error) {
paths := []string{} // TODO: Use [filepath.Glob] instead of [*path.Pattern]
excludeMatchers := []*path.Pattern{} concreteExcludePatterns := []*path.Pattern{}
for _, p := range excludes { for _, rawPattern := range excludePatterns {
m, err := path.ParsePattern(p) pattern, err := path.ParsePattern(rawPattern)
if err != nil { if err != nil {
return nil, err return nil, err
} }
excludeMatchers = append(excludeMatchers, m) concreteExcludePatterns = append(concreteExcludePatterns, pattern)
} }
paths := []string{}
if err := filepath.Walk(".", func(p string, info fs.FileInfo, err error) error { if err := filepath.Walk(".", func(p string, info fs.FileInfo, err error) error {
if info.IsDir() { if info.IsDir() {
return nil return nil
} }
excluded := slices.ContainsFunc(excludeMatchers, func(excludePattern *path.Pattern) bool { if slices.ContainsFunc(concreteExcludePatterns, func(pattern *path.Pattern) bool {
ok, _, _ := excludePattern.Match(p) ok, _, _ := pattern.Match(p)
return ok return ok
}) }) {
if excluded {
return nil return nil
} }

Loading…
Cancel
Save