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
128 changes: 128 additions & 0 deletions pkg/definition/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package definition

import (
"fmt"
"os"
"path/filepath"

"gopkg.in/yaml.v3"
)

// MassdriverYAML represents the structure of a massdriver.yaml artifact definition file.
// This is an experimental format that provides a more ergonomic authoring experience.
type MassdriverYAML struct {
Name string `yaml:"name"`
Label string `yaml:"label"`
Icon string `yaml:"icon"`
UI *UIConfig `yaml:"ui"`
Exports []ExportConfig `yaml:"exports"`
Schema map[string]any `yaml:"schema"`
}

// UIConfig represents the UI configuration section
type UIConfig struct {
ConnectionOrientation string `yaml:"connectionOrientation"`
EnvironmentDefaultGroup string `yaml:"environmentDefaultGroup"`
Instructions []InstructionConfig `yaml:"instructions"`
}

// InstructionConfig represents an instruction file reference
type InstructionConfig struct {
Label string `yaml:"label"`
Path string `yaml:"path"`
}

// ExportConfig represents an export template configuration
type ExportConfig struct {
DownloadButtonText string `yaml:"downloadButtonText"`
FileFormat string `yaml:"fileFormat"`
TemplatePath string `yaml:"templatePath"`
TemplateLang string `yaml:"templateLang"`
}

// Build reads a massdriver.yaml file and builds it into the artifact definition
// format expected by the Massdriver API.
func Build(path string) (map[string]any, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read massdriver.yaml: %w", err)
}

var config MassdriverYAML
if err := yaml.Unmarshal(content, &config); err != nil {
return nil, fmt.Errorf("failed to parse massdriver.yaml: %w", err)
}

baseDir := filepath.Dir(path)

// Build the $md block
mdBlock := map[string]any{
"name": config.Name,
"label": config.Label,
"icon": config.Icon,
}

// Process UI configuration
if config.UI != nil {
uiBlock := map[string]any{}

if config.UI.ConnectionOrientation != "" {
uiBlock["connectionOrientation"] = config.UI.ConnectionOrientation
}
if config.UI.EnvironmentDefaultGroup != "" {
uiBlock["environmentDefaultGroup"] = config.UI.EnvironmentDefaultGroup
}

// Process instructions
instructions := []map[string]any{}
for _, instruction := range config.UI.Instructions {
instructionPath := filepath.Join(baseDir, instruction.Path)
instructionContent, err := os.ReadFile(instructionPath)
if err != nil {
return nil, fmt.Errorf("failed to read instruction file %s: %w", instruction.Path, err)
}
instructions = append(instructions, map[string]any{
"label": instruction.Label,
"content": string(instructionContent),
})
}
uiBlock["instructions"] = instructions

mdBlock["ui"] = uiBlock
}

// Process exports
exports := []map[string]any{}
for _, export := range config.Exports {
templatePath := filepath.Join(baseDir, export.TemplatePath)
templateContent, err := os.ReadFile(templatePath)
if err != nil {
return nil, fmt.Errorf("failed to read export template %s: %w", export.TemplatePath, err)
}
exports = append(exports, map[string]any{
"downloadButtonText": export.DownloadButtonText,
"fileFormat": export.FileFormat,
"template": string(templateContent),
"templateLang": export.TemplateLang,
})
}
mdBlock["export"] = exports

// Build the final structure: merge $md with schema
result := map[string]any{
"$md": mdBlock,
}

// Merge schema into result
for key, value := range config.Schema {
result[key] = value
}

return result, nil
}

// IsMassdriverYAMLArtifactDefinition checks if the given path is a massdriver.yaml
// file that should be treated as an artifact definition in the experimental format.
func IsMassdriverYAMLArtifactDefinition(path string) bool {
return filepath.Base(path) == "massdriver.yaml"
}
158 changes: 158 additions & 0 deletions pkg/definition/build_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package definition_test

import (
"path/filepath"
"testing"

"github.com/massdriver-cloud/mass/pkg/definition"
)

func TestIsMassdriverYAMLArtifactDefinition(t *testing.T) {
tests := []struct {
name string
path string
want bool
}{
{
name: "massdriver.yaml file",
path: "some/path/massdriver.yaml",
want: true,
},
{
name: "massdriver.yaml in root",
path: "massdriver.yaml",
want: true,
},
{
name: "json artifact definition",
path: "some/path/artifact.json",
want: false,
},
{
name: "other yaml file",
path: "some/path/artifact.yaml",
want: false,
},
{
name: "yml file",
path: "some/path/artifact.yml",
want: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := definition.IsMassdriverYAMLArtifactDefinition(tc.path)
if got != tc.want {
t.Errorf("IsMassdriverYAMLArtifactDefinition(%q) = %v, want %v", tc.path, got, tc.want)
}
})
}
}

func TestBuild(t *testing.T) {
tests := []struct {
name string
path string
wantErr bool
check func(t *testing.T, result map[string]any)
}{
{
name: "builds massdriver.yaml with instructions and exports",
path: filepath.Join("testdata", "massdriver-yaml-artifact", "massdriver.yaml"),
wantErr: false,
check: func(t *testing.T, result map[string]any) {
// Check $md block
md, ok := result["$md"].(map[string]any)
if !ok {
t.Fatal("expected $md to be a map")
}

if md["name"] != "test-artifact" {
t.Errorf("expected name to be test-artifact, got %v", md["name"])
}
if md["label"] != "Test Artifact Definition" {
t.Errorf("expected label to be Test Artifact Definition, got %v", md["label"])
}
if md["icon"] != "https://example.com/icon.png" {
t.Errorf("expected icon to be https://example.com/icon.png, got %v", md["icon"])
}

// Check UI block
ui, ok := md["ui"].(map[string]any)
if !ok {
t.Fatal("expected ui to be a map")
}
if ui["connectionOrientation"] != "environmentDefault" {
t.Errorf("expected connectionOrientation to be environmentDefault, got %v", ui["connectionOrientation"])
}
if ui["environmentDefaultGroup"] != "credentials" {
t.Errorf("expected environmentDefaultGroup to be credentials, got %v", ui["environmentDefaultGroup"])
}

// Check instructions
instructions, ok := ui["instructions"].([]map[string]any)
if !ok {
t.Fatal("expected instructions to be a slice of maps")
}
if len(instructions) != 2 {
t.Errorf("expected 2 instructions, got %d", len(instructions))
}
if instructions[0]["label"] != "CLI Setup" {
t.Errorf("expected first instruction label to be CLI Setup, got %v", instructions[0]["label"])
}
// Check that content was read
content, ok := instructions[0]["content"].(string)
if !ok || content == "" {
t.Error("expected instruction content to be a non-empty string")
}

// Check exports
exports, ok := md["export"].([]map[string]any)
if !ok {
t.Fatal("expected export to be a slice of maps")
}
if len(exports) != 1 {
t.Errorf("expected 1 export, got %d", len(exports))
}
if exports[0]["downloadButtonText"] != "Download Config" {
t.Errorf("expected downloadButtonText to be Download Config, got %v", exports[0]["downloadButtonText"])
}
if exports[0]["fileFormat"] != "yaml" {
t.Errorf("expected fileFormat to be yaml, got %v", exports[0]["fileFormat"])
}
if exports[0]["templateLang"] != "liquid" {
t.Errorf("expected templateLang to be liquid, got %v", exports[0]["templateLang"])
}
// Check that template was read
template, ok := exports[0]["template"].(string)
if !ok || template == "" {
t.Error("expected export template to be a non-empty string")
}

// Check schema fields are merged at top level
if result["$schema"] != "http://json-schema.org/draft-07/schema" {
t.Errorf("expected $schema at top level, got %v", result["$schema"])
}
if result["title"] != "Test Artifact" {
t.Errorf("expected title to be Test Artifact, got %v", result["title"])
}
if result["type"] != "object" {
t.Errorf("expected type to be object, got %v", result["type"])
}
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := definition.Build(tc.path)
if (err != nil) != tc.wantErr {
t.Fatalf("Build() error = %v, wantErr %v", err, tc.wantErr)
}
if tc.check != nil {
tc.check(t, result)
}
})
}
}
12 changes: 11 additions & 1 deletion pkg/definition/publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,20 @@ func TestPublish(t *testing.T) {
}
tests := []test{
{
name: "simple",
name: "simple json",
path: "testdata/simple-artifact.json",
wantBody: `{"$schema":"http://json-schema.org/draft-07/schema","type":"object","title":"Test Artifact","properties":{"data":{"type":"object"}},"specs":{"type":"object"}}}`,
},
{
name: "massdriver.yaml format",
path: "testdata/massdriver-yaml-simple/massdriver.yaml",
wantBody: `{"$schema":"http://json-schema.org/draft-07/schema","type":"object","title":"Test Artifact"}`,
},
{
name: "massdriver.yaml with instructions and exports",
path: "testdata/massdriver-yaml-artifact/massdriver.yaml",
wantBody: `{"$schema":"http://json-schema.org/draft-07/schema","type":"object","title":"Test Artifact"}`,
},
}

for _, tc := range tests {
Expand Down
23 changes: 23 additions & 0 deletions pkg/definition/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ import (
)

func Read(ctx context.Context, mdClient *client.Client, path string) (map[string]any, error) {
// Check if this is a massdriver.yaml file (experimental artifact definition format)
if IsMassdriverYAMLArtifactDefinition(path) {
built, buildErr := Build(path)
if buildErr != nil {
return nil, fmt.Errorf("failed to build massdriver.yaml artifact definition: %w", buildErr)
}

// Dereference the built schema
opts := DereferenceOptions{
Client: mdClient,
Cwd: filepath.Dir(path),
}
dereferencedAny, derefErr := DereferenceSchema(built, opts)
if derefErr != nil {
return nil, fmt.Errorf("failed to dereference artifact definition: %w", derefErr)
}
dereferenced, ok := dereferencedAny.(map[string]any)
if !ok {
return nil, fmt.Errorf("dereferenced artifact definition is not a map")
}
return dereferenced, nil
}

artdefBytes, readErr := os.ReadFile(path)
if readErr != nil {
return nil, fmt.Errorf("failed to read artifact definition: %w", readErr)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Config
metadata:
name: {{ artifact.id }}
credentials:
token: {{ artifact.token }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# CLI Setup Guide

Follow these steps to configure the CLI:

1. Install the CLI tool
2. Run `tool configure`
3. Enter your credentials
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Console Guide

Access the console at https://console.example.com

1. Navigate to Settings
2. Click on Credentials
3. Copy your token
Loading
Loading