Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: codegen for doc #291

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
17 changes: 11 additions & 6 deletions atlasaction/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -1251,23 +1251,28 @@ func RenderTemplate(name string, data any) (string, error) {
return buf.String(), nil
}

// toEnvName converts the given string to an environment variable name.
func toEnvName(s string) string {
// ToEnvName converts the given string to an environment variable name.
func ToEnvName(s string) string {
return strings.ToUpper(strings.NewReplacer(
" ", "_", "-", "_", "/", "_",
).Replace(s))
}

// toInputVarName converts the given string to an input variable name.
func toInputVarName(input string) string {
return fmt.Sprintf("ATLAS_INPUT_%s", toEnvName(input))
// ToInputVarName converts the given string to an input variable name.
func ToInputVarName(input string) string {
return "ATLAS_INPUT_" + ToEnvName(input)
}

// ToInputVarName converts the given string to an input variable name.
func ToOutputVarName(action, output string) string {
return "ATLAS_OUTPUT_" + ToEnvName(action+"_"+output)
}

// toOutputVar converts the given values to an output variable.
// The action and output are used to create the output variable name with the format:
// ATLAS_OUTPUT_<ACTION>_<OUTPUT>="<value>"
func toOutputVar(action, output, value string) string {
return fmt.Sprintf("ATLAS_OUTPUT_%s=%q", toEnvName(action+"_"+output), value)
return fmt.Sprintf("%s=%q", ToOutputVarName(action, output), value)
}

// fprintln writes the given values to the file using fmt.Fprintln.
Expand Down
2 changes: 1 addition & 1 deletion atlasaction/bitbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (a *bbPipe) GetTriggerContext(context.Context) (*TriggerContext, error) {

// GetInput implements the Action interface.
func (a *bbPipe) GetInput(name string) string {
return strings.TrimSpace(a.getenv(toInputVarName(name)))
return strings.TrimSpace(a.getenv(ToInputVarName(name)))
}

// SetOutput implements Action.
Expand Down
4 changes: 2 additions & 2 deletions atlasaction/circleci_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ func (a *circleCIOrb) Getenv(key string) string {

// GetInput implements the Action interface.
func (a *circleCIOrb) GetInput(name string) string {
v := a.getenv(toInputVarName(name))
v := a.getenv(ToInputVarName(name))
if v == "" {
// TODO: Remove this fallback once all the actions are updated.
v = a.getenv(toEnvName("INPUT_" + name))
v = a.getenv(ToEnvName("INPUT_" + name))
}
return strings.TrimSpace(v)
}
Expand Down
112 changes: 112 additions & 0 deletions atlasaction/gen/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package gen

import (
"cmp"
"errors"
"io/fs"
"iter"
"maps"
"slices"
"strings"

"gopkg.in/yaml.v3"
)

type (
// Actions represents a list of actions.
Actions struct {
Actions []ActionSpec
}
ActionSpec struct {
ID string `yaml:"id"`
Description string `yaml:"description"`
Name string `yaml:"name"`
Inputs map[string]ActionInput `yaml:"inputs,omitempty"`
Outputs map[string]ActionOutput `yaml:"outputs,omitempty"`
}
ActionInput struct {
Description string `yaml:"description,omitempty"`
Default string `yaml:"default,omitempty"`
Required bool `yaml:"required"`
}
ActionOutput struct {
Description string `yaml:"description"`
}
)

// ParseFS parses the actions from the given file system.
func ParseFS(fsys fs.FS) (*Actions, error) {
level1, err := fs.Glob(fsys, "**/*/action.yml")
if err != nil {
return nil, err
}
level2, err := fs.Glob(fsys, "**/*/*/action.yml")
if err != nil {
return nil, err
}
files := append(level1, level2...)
actions := make([]ActionSpec, len(files))
for i, f := range files {
if err := actions[i].fromPath(fsys, f); err != nil {
return nil, err
}
}
slices.SortFunc(actions, func(i, j ActionSpec) int {
return cmp.Compare(i.ID, j.ID)
})
return &Actions{Actions: actions}, nil
}

func (a *ActionSpec) fromPath(fsys fs.FS, path string) error {
f, err := fsys.Open(path)
if err != nil {
return err
}
defer f.Close()
if err = yaml.NewDecoder(f).Decode(a); err != nil {
return err
}
if id, ok := strings.CutSuffix(path, "/action.yml"); ok {
a.ID = strings.Trim(id, "/")
return nil
}
return errors.New("gen: invalid action.yml path")
}

func (a *ActionSpec) SortedInputs() iter.Seq2[string, ActionInput] {
orders := []string{"working-directory", "config", "env", "vars", "dev-url"}
return func(yield func(string, ActionInput) bool) {
keys := slices.SortedFunc(maps.Keys(a.Inputs), ComparePriority(orders))
for _, k := range keys {
if !yield(k, a.Inputs[k]) {
return
}
}
}
}

func (a *ActionSpec) SortedOutputs() iter.Seq2[string, ActionOutput] {
return func(yield func(string, ActionOutput) bool) {
keys := slices.Sorted(maps.Keys(a.Outputs))
for _, k := range keys {
if !yield(k, a.Outputs[k]) {
return
}
}
}
}

func ComparePriority[T cmp.Ordered](ordered []T) func(T, T) int {
return func(x, y T) int {
switch xi, yi := slices.Index(ordered, x), slices.Index(ordered, y); {
case xi != -1 && yi != -1:
return cmp.Compare(xi, yi)
case yi != -1:
return -1
case xi != -1:
return +1
}
// fallback to default comparison
return cmp.Compare(x, y)
}
}
132 changes: 132 additions & 0 deletions atlasaction/gen/templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package gen

import (
"bytes"
"io/fs"
"strings"
"text/template"

"ariga.io/atlas-action/atlasaction"
)

var (
// templates holds the Go templates for the code generation.
templates *Template
// Funcs are the predefined template
// functions used by the codegen.
Funcs = template.FuncMap{
"xtemplate": xtemplate,
"hasTemplate": hasTemplate,
"trimnl": func(s string) string {
return strings.Trim(s, "\n")
},
"nl2sp": func(s string) string {
return strings.ReplaceAll(s, "\n", " ")
},
"env": atlasaction.ToEnvName,
"inputvar": atlasaction.ToInputVarName,
"outputvar": atlasaction.ToOutputVarName,
"dockers": func() []DockerURL {
return []DockerURL{
{Label: "MySQL", Driver: "mysql"},
{Label: "Postgres", Driver: "postgres"},
{Label: "MariaDB", Driver: "mariadb"},
{Label: "SQL Server", Driver: "sqlserver"},
{Label: "ClickHouse", Driver: "clickhouse"},
{Label: "SQLite", Driver: "sqlite"},
}
},
}
)

// LoadTemplates loads the templates from the given file system.
func LoadTemplates(fsys fs.FS, patterns ...string) *Template {
templates = MustParse(NewTemplate("templates").
ParseFS(fsys, patterns...))
return templates
}

type DockerURL struct {
Label string
Driver string
}

func (d DockerURL) DevURL() string {
switch d.Driver {
case "mysql":
return "docker://mysql/8/dev"
case "postgres":
return "docker://postgres/15/dev?search_path=public"
case "mariadb":
return "docker://maria/latest/schema"
case "sqlserver":
return "docker://sqlserver/2022-latest?mode=schema"
case "clickhouse":
return "docker://clickhouse/23.11/dev"
case "sqlite":
return "sqlite://db?mode=memory"
}
return ""
}

type Template struct {
*template.Template
FuncMap template.FuncMap
}

// MustParse is a helper that wraps a call to a function returning (*Template, error)
// and panics if the error is non-nil.
func MustParse(t *Template, err error) *Template {
if err != nil {
panic(err)
}
return t
}

// NewTemplate creates an empty template with the standard codegen functions.
func NewTemplate(name string) *Template {
t := &Template{Template: template.New(name)}
return t.Funcs(Funcs)
}

// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
// instead of the host operating system's file system.
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
if _, err := t.Template.ParseFS(fsys, patterns...); err != nil {
return nil, err
}
return t, nil
}

// Funcs merges the given funcMap with the template functions.
func (t *Template) Funcs(funcMap template.FuncMap) *Template {
t.Template.Funcs(funcMap)
if t.FuncMap == nil {
t.FuncMap = template.FuncMap{}
}
for name, f := range funcMap {
if _, ok := t.FuncMap[name]; !ok {
t.FuncMap[name] = f
}
}
return t
}

// xtemplate dynamically executes templates by their names.
func xtemplate(name string, v any) (string, error) {
buf := bytes.NewBuffer(nil)
if err := templates.ExecuteTemplate(buf, name, v); err != nil {
return "", err
}
return buf.String(), nil
}

// hasTemplate checks whether a template exists in the loaded templates.
func hasTemplate(name string) bool {
for _, t := range templates.Templates() {
if t.Name() == name {
return true
}
}
return false
}
2 changes: 1 addition & 1 deletion atlasaction/gitlab_ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (a *gitlabCI) Getenv(key string) string {

// GetInput implements the Action interface.
func (a *gitlabCI) GetInput(name string) string {
return strings.TrimSpace(a.getenv(toInputVarName(name)))
return strings.TrimSpace(a.getenv(ToInputVarName(name)))
}

// SetOutput implements the Action interface.
Expand Down
33 changes: 33 additions & 0 deletions tools/gen/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module atlas-action-gen

go 1.24rc1

replace ariga.io/atlas-action => ../../

require (
ariga.io/atlas-action v0.0.0-00010101000000-000000000000
gopkg.in/yaml.v3 v3.0.1
)

require (
ariga.io/atlas v0.21.2-0.20240418081819-02b3f6239b04 // indirect
ariga.io/atlas-go-sdk v0.6.5 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/sethvargo/go-githubactions v1.3.0 // indirect
github.com/vektah/gqlparser v1.3.1 // indirect
github.com/zclconf/go-cty v1.14.1 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
)
Loading
Loading