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
2 changes: 2 additions & 0 deletions cmd/daco/internal/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/dacolabs/cli/internal/translate/databrickssql"
"github.com/dacolabs/cli/internal/translate/dqxyaml"
"github.com/dacolabs/cli/internal/translate/gotypes"
"github.com/dacolabs/cli/internal/translate/markdown"
"github.com/dacolabs/cli/internal/translate/protobuf"
"github.com/dacolabs/cli/internal/translate/pydantic"
"github.com/dacolabs/cli/internal/translate/pyspark"
Expand All @@ -39,6 +40,7 @@ func registerTranslators() translate.Register {
translators["protobuf"] = &protobuf.Translator{}
translators["spark-sql"] = &sparksql.Translator{}
translators["dqx-yaml"] = &dqxyaml.Translator{}
translators["markdown"] = &markdown.Translator{}
return translators
}

Expand Down
122 changes: 122 additions & 0 deletions internal/translate/markdown/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Markdown

Translates JSON Schema to human-readable Markdown documentation (.md).

## Example

**Input** (JSON Schema):

```json
{
"type": "object",
"description": "User information",
"properties": {
"name": { "type": "string", "description": "Full name" },
"age": { "type": "integer" }
},
"required": ["name"]
}
```

**Output** (Markdown):

```markdown
# Users

User information

## Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Full name |
| `age` | integer | No | |
```

## Supported JSON Schema Features

### Type Keywords
- [x] type
- [x] enum
- [x] const

### Type Values
- [x] string
- [x] integer
- [x] number
- [x] boolean
- [x] array
- [x] object
- [ ] null

### Schema Composition
- [ ] allOf
- [ ] anyOf
- [ ] oneOf
- [ ] not

### Object Keywords
- [x] properties
- [x] required
- [ ] additionalProperties
- [ ] patternProperties
- [ ] propertyNames
- [ ] minProperties / maxProperties
- [ ] unevaluatedProperties
- [ ] dependentRequired

### Array Keywords
- [x] items
- [ ] prefixItems
- [ ] contains
- [x] minItems / maxItems
- [ ] uniqueItems
- [ ] unevaluatedItems
- [ ] maxContains / minContains

### Numeric Validation
- [x] minimum / maximum
- [x] exclusiveMinimum / exclusiveMaximum
- [x] multipleOf

### String Validation
- [x] minLength / maxLength
- [x] pattern

### References & Definitions
- [x] $ref
- [x] $defs
- [ ] $id
- [ ] $anchor
- [ ] $dynamicRef / $dynamicAnchor

### String Formats
- [x] date
- [x] date-time
- [x] time
- [x] duration
- [x] uuid
- [x] uri / uri-reference / uri-template
- [x] iri / iri-reference
- [x] email / idn-email
- [x] hostname / idn-hostname
- [x] ipv4 / ipv6
- [x] json-pointer / relative-json-pointer
- [x] regex

### Annotations
- [x] description
- [ ] title
- [ ] default
- [ ] deprecated
- [ ] readOnly / writeOnly
- [ ] examples

### Conditional
- [ ] if / then / else
- [ ] dependentSchemas

### Content
- [ ] contentEncoding
- [ ] contentMediaType
- [ ] contentSchema
23 changes: 23 additions & 0 deletions internal/translate/markdown/markdown.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# {{ .Root.Name }}
{{ if .Description }}
{{ .Description }}
{{ end }}
## Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
{{- range .Root.Fields }}
| `{{ .Name }}` | {{ .Type }} | {{ if .Nullable }}No{{ else }}Yes{{ end }} | {{ .Description }}{{- if formatConstraints .Constraints }} ({{ formatConstraints .Constraints }}){{ end }} |
{{- end }}
{{ range .Defs }}

---

## {{ .Name }}

| Field | Type | Required | Description |
|-------|------|----------|-------------|
{{- range .Fields }}
| `{{ .Name }}` | {{ .Type }} | {{ if .Nullable }}No{{ else }}Yes{{ end }} | {{ .Description }}{{- if formatConstraints .Constraints }} ({{ formatConstraints .Constraints }}){{ end }} |
{{- end }}
{{ end }}
33 changes: 33 additions & 0 deletions internal/translate/markdown/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Daco Labs

// Package markdown provides markdown schema documentation utilities.
package markdown

import (
"github.com/dacolabs/cli/internal/translate"
)

type resolver struct{}

func (r *resolver) PrimitiveType(schemaType, format string) string {
return schemaType
}

func (r *resolver) ArrayType(elemType string) string {
return "array(" + elemType + ")"
}

func (r *resolver) RefType(defName string) string {
return "[" + translate.ToPascalCase(defName) + "](#" + translate.ToPascalCase(defName) + ")"
}

func (r *resolver) FormatDefName(defName string) string {
return translate.ToPascalCase(defName)
}

func (r *resolver) FormatRootName(portName string) string {
return translate.ToPascalCase(portName)
}

func (r *resolver) EnrichField(f *translate.Field) {}
111 changes: 111 additions & 0 deletions internal/translate/markdown/translator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Daco Labs

package markdown

import (
"bytes"
"embed"
"fmt"
"strings"
"text/template"

"github.com/dacolabs/cli/internal/translate"
"github.com/dacolabs/jsonschema-go/jsonschema"
)

//go:embed markdown.go.tmpl
var tmplFS embed.FS

var funcMap = template.FuncMap{
"formatConstraints": formatConstraints,
}

var tmpl = template.Must(template.New("markdown.go.tmpl").Funcs(funcMap).ParseFS(tmplFS, "markdown.go.tmpl"))

// Translator translates JSON schemas to markdown documentation.
type Translator struct{}

// FileExtension returns the file extension for markdown files.
func (t *Translator) FileExtension() string {
return ".md"
}

// Translate converts a JSON schema to markdown documentation.
func (t *Translator) Translate(portName string, schema *jsonschema.Schema, _ string) ([]byte, error) {
data, err := translate.Prepare(portName, schema, &resolver{})
if err != nil {
return nil, fmt.Errorf("failed to prepare schema data: %w", err)
}

var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, "markdown.go.tmpl", data); err != nil {
return nil, fmt.Errorf("failed to execute template: %w", err)
}

return buf.Bytes(), nil
}

// formatConstraints formats the constraints for a field as a human-readable string.
// Template functions receive values by value, so we cannot use a pointer here.
func formatConstraints(c translate.Constraints) string { //nolint:gocritic // hugeParam: template functions receive values by value
var parts []string

if len(c.Enum) > 0 {
enumVals := make([]string, len(c.Enum))
for i, v := range c.Enum {
enumVals[i] = fmt.Sprintf("`%v`", v)
}
parts = append(parts, "enum: "+strings.Join(enumVals, ", "))
}

if c.Const != nil {
parts = append(parts, fmt.Sprintf("const: `%v`", *c.Const))
}

if c.Pattern != "" {
parts = append(parts, fmt.Sprintf("pattern: `%s`", c.Pattern))
}

if c.Format != "" {
parts = append(parts, fmt.Sprintf("format: %s", c.Format))
}

if c.MinLength != nil {
parts = append(parts, fmt.Sprintf("minLength: %d", *c.MinLength))
}

if c.MaxLength != nil {
parts = append(parts, fmt.Sprintf("maxLength: %d", *c.MaxLength))
}

if c.Minimum != nil {
parts = append(parts, fmt.Sprintf("minimum: %v", *c.Minimum))
}

if c.Maximum != nil {
parts = append(parts, fmt.Sprintf("maximum: %v", *c.Maximum))
}

if c.ExclusiveMinimum != nil {
parts = append(parts, fmt.Sprintf("exclusiveMinimum: %v", *c.ExclusiveMinimum))
}

if c.ExclusiveMaximum != nil {
parts = append(parts, fmt.Sprintf("exclusiveMaximum: %v", *c.ExclusiveMaximum))
}

if c.MultipleOf != nil {
parts = append(parts, fmt.Sprintf("multipleOf: %v", *c.MultipleOf))
}

if c.MinItems != nil {
parts = append(parts, fmt.Sprintf("minItems: %d", *c.MinItems))
}

if c.MaxItems != nil {
parts = append(parts, fmt.Sprintf("maxItems: %d", *c.MaxItems))
}

return strings.Join(parts, ", ")
}
Loading