feat: subpackage rendering

This commit is contained in:
sh0rez 2020-04-30 00:47:43 +02:00
parent 09e8e3ddd9
commit 6e95ea768a
No known key found for this signature in database
GPG key ID: 87C71DF9F8181FF1
7 changed files with 306 additions and 203 deletions

76
pkg/docsonnet/field.go Normal file
View file

@ -0,0 +1,76 @@
package docsonnet
import (
"encoding/json"
"errors"
)
// Field represents any field of an object.
type Field struct {
// Function value
Function *Function `json:"function,omitempty"`
// Object value
Object *Object `json:"object,omitempty"`
// Any other value
Value *Value `json:"value,omitempty"`
}
func (o *Field) UnmarshalJSON(data []byte) error {
type fake Field
var f fake
if err := json.Unmarshal(data, &f); err != nil {
return err
}
switch {
case f.Function != nil:
o.Function = f.Function
case f.Object != nil:
o.Object = f.Object
case f.Value != nil:
o.Value = f.Value
default:
return errors.New("field has no value")
}
return nil
}
func (o Field) MarshalJSON() ([]byte, error) {
if o.Function == nil && o.Object == nil && o.Value == nil {
return nil, errors.New("field has no value")
}
type fake Field
return json.Marshal(fake(o))
}
// Fields is a list of fields
type Fields map[string]Field
func (fPtr *Fields) UnmarshalJSON(data []byte) error {
if *fPtr == nil {
*fPtr = make(Fields)
}
f := *fPtr
tmp := make(map[string]Field)
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
for k, v := range tmp {
switch {
case v.Function != nil:
v.Function.Name = k
case v.Object != nil:
v.Object.Name = k
case v.Value != nil:
v.Value.Name = k
}
f[k] = v
}
return nil
}

View file

@ -0,0 +1,48 @@
package docsonnet
import (
"encoding/json"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestRemarshal(t *testing.T) {
o := Object{
Help: "grafana.libsonnet is the offical Jsonnet library for Grafana",
Fields: map[string]Field{
"new": {Function: &Function{
Name: "new",
Help: "new returns Grafana resources with sane defaults",
}},
"addConfig": {Function: &Function{
Name: "addConfig",
Help: "addConfig adds config entries to grafana.ini",
}},
"datasource": {Object: &Object{
Name: "datasource",
Help: "ds-util makes creating datasources easy",
Fields: map[string]Field{
"new": {Function: &Function{
Name: "new",
Help: "new creates a new datasource",
}},
},
}},
},
}
data, err := json.Marshal(o)
if err != nil {
t.Fatal(err)
}
var got Object
if err := json.Unmarshal(data, &got); err != nil {
t.Fatal(err)
}
if str := cmp.Diff(o, got); str != "" {
t.Fatal(str)
}
}

78
pkg/docsonnet/load.go Normal file
View file

@ -0,0 +1,78 @@
package docsonnet
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"github.com/google/go-jsonnet"
"github.com/markbates/pkger"
)
// Load extracts docsonnet data from the given Jsonnet document
func Load(filename string) (*Package, error) {
// get load.libsonnet from embedded data
file, err := pkger.Open("/load.libsonnet")
if err != nil {
return nil, err
}
load, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
// setup Jsonnet vm
vm := jsonnet.MakeVM()
importer, err := newImporter()
if err != nil {
return nil, err
}
vm.Importer(importer)
// invoke load.libsonnet
vm.ExtCode("main", fmt.Sprintf(`(import "%s")`, filename))
data, err := vm.EvaluateSnippet("load.libsonnet", string(load))
if err != nil {
return nil, err
}
// parse the result
var d Package
if err := json.Unmarshal([]byte(data), &d); err != nil {
log.Fatalln(err)
}
return &d, nil
}
// importer wraps jsonnet.FileImporter, to statically provide load.libsonnet,
// bundled with the binary
type importer struct {
fi jsonnet.FileImporter
util jsonnet.Contents
}
func newImporter() (*importer, error) {
file, err := pkger.Open("/doc-util/main.libsonnet")
if err != nil {
return nil, err
}
load, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
return &importer{
fi: jsonnet.FileImporter{},
util: jsonnet.MakeContents(string(load)),
}, nil
}
func (i *importer) Import(importedFrom, importedPath string) (contents jsonnet.Contents, foundAt string, err error) {
if importedPath == "doc-util/main.libsonnet" {
return i.util, "<internal>", nil
}
return i.fi.Import(importedFrom, importedPath)
}

58
pkg/docsonnet/model.go Normal file
View file

@ -0,0 +1,58 @@
package docsonnet
// Package represents a Jsonnet package, having an API (list of Fields) and
// perhaps subpackages
type Package struct {
Name string `json:"name"`
Import string `json:"import"`
Help string `json:"help"`
API Fields `json:"api,omitempty"`
Sub map[string]Package `json:"sub,omitempty"`
}
// Object represents a Jsonnet object, a list of key-value fields
type Object struct {
Name string `json:"-"`
Help string `json:"help"`
// children
Fields Fields `json:"fields"`
}
// Function represents a Jsonnet function, a named construct that takes
// arguments
type Function struct {
Name string `json:"-"`
Help string `json:"help"`
Args []Argument `json:"args,omitempty"`
}
// Argument is a function argument, optionally also having a default value
type Argument struct {
Type Type `json:"type"`
Name string `json:"name"`
Default interface{} `json:"default"`
}
// Value is a value of any other type than the special Object and Function types
type Value struct {
Name string `json:"-"`
Help string `json:"help"`
Type Type `json:"type"`
}
// Type is a Jsonnet type
type Type string
const (
TypeString = "string"
TypeNumber = "number"
TypeBool = "boolean"
TypeObject = "object"
TypeArray = "array"
TypeAny = "any"
TypeFunc = "function"
)

34
pkg/render/paths.go Normal file
View file

@ -0,0 +1,34 @@
package render
import (
"strings"
"github.com/sh0rez/docsonnet/pkg/docsonnet"
)
func Paths(pkg docsonnet.Package) map[string]docsonnet.Package {
p := paths(pkg)
for k, v := range p {
delete(p, k)
k = strings.TrimPrefix(k, pkg.Name+"/")
p[k] = v
}
return p
}
func paths(pkg docsonnet.Package) map[string]docsonnet.Package {
pkgs := make(map[string]docsonnet.Package)
pkgs[pkg.Name+".md"] = pkg
if len(pkg.Sub) == 0 {
return pkgs
}
for _, sub := range pkg.Sub {
for k, v := range paths(sub) {
pkgs[pkg.Name+"/"+k] = v
}
}
return pkgs
}

129
pkg/render/render.go Normal file
View file

@ -0,0 +1,129 @@
package render
import (
"fmt"
"sort"
"strings"
"github.com/sh0rez/docsonnet/pkg/docsonnet"
"github.com/sh0rez/docsonnet/pkg/md"
"github.com/sh0rez/docsonnet/pkg/slug"
)
func Render(pkg docsonnet.Package) string {
// head
elems := []md.Elem{
md.Headline(1, "package "+pkg.Name),
md.CodeBlock("jsonnet", fmt.Sprintf(`local %s = import "%s"`, pkg.Name, pkg.Import)),
md.Text(pkg.Help),
}
// subpackages
if len(pkg.Sub) > 0 {
elems = append(elems, md.Headline(2, "Subpackages"))
var items []md.Elem
for k := range Paths(pkg) {
name := strings.TrimSuffix(k, ".md")
if name == pkg.Name {
continue
}
items = append(items, md.Link(md.Text(name), k))
}
elems = append(elems, md.List(items...))
}
// fields of this package
if len(pkg.API) > 0 {
// index
elems = append(elems,
md.Headline(2, "Index"),
md.List(renderIndex(pkg.API, "", slug.New())...),
)
// api
elems = append(elems, md.Headline(2, "Fields"))
elems = append(elems, renderApi(pkg.API, "")...)
}
return md.Doc(elems...).String()
}
func renderIndex(api docsonnet.Fields, path string, s *slug.Slugger) []md.Elem {
var elems []md.Elem
for _, k := range sortFields(api) {
v := api[k]
switch {
case v.Function != nil:
fn := v.Function
name := md.Text(fmt.Sprintf("fn %s(%s)", fn.Name, renderParams(fn.Args)))
link := "#" + s.Slug("fn "+path+fn.Name)
elems = append(elems, md.Link(md.Code(name), link))
case v.Object != nil:
obj := v.Object
name := md.Text("obj " + path + obj.Name)
link := "#" + s.Slug("obj "+path+obj.Name)
elems = append(elems, md.Link(md.Code(name), link))
elems = append(elems, md.List(renderIndex(obj.Fields, path+obj.Name+".", s)...))
}
}
return elems
}
func renderApi(api docsonnet.Fields, path string) []md.Elem {
var elems []md.Elem
for _, k := range sortFields(api) {
v := api[k]
switch {
case v.Function != nil:
fn := v.Function
elems = append(elems,
md.Headline(3, fmt.Sprintf("fn %s%s", path, fn.Name)),
md.CodeBlock("ts", fmt.Sprintf("%s(%s)", fn.Name, renderParams(fn.Args))),
md.Text(fn.Help),
)
case v.Object != nil:
obj := v.Object
elems = append(elems,
md.Headline(2, fmt.Sprintf("obj %s%s", path, obj.Name)),
md.Text(obj.Help),
)
elems = append(elems, renderApi(obj.Fields, path+obj.Name+".")...)
}
}
return elems
}
func sortFields(api docsonnet.Fields) []string {
keys := make([]string, len(api))
for k := range api {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
iK, jK := keys[i], keys[j]
if api[iK].Function != nil && api[jK].Function == nil {
return true
}
if api[iK].Function == nil && api[jK].Function != nil {
return false
}
return iK < jK
})
return keys
}
func renderParams(a []docsonnet.Argument) string {
args := make([]string, 0, len(a))
for _, a := range a {
arg := a.Name
if a.Default != nil {
arg = fmt.Sprintf("%s=%v", arg, a.Default)
}
args = append(args, arg)
}
return strings.Join(args, ", ")
}