Initial commit with basic HTTP endpoint working
This commit is contained in:
57
internal/cache.go
Normal file
57
internal/cache.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package internal
|
||||
|
||||
// DiagramCache provides the ability to cache diagram results.
|
||||
type DiagramCache interface {
|
||||
// Store stores a diagram in the cache.
|
||||
Store(diagram *Diagram) error
|
||||
// Has returns true if we have a cache stored for the given diagram description.
|
||||
Has(diagram *Diagram) (bool, error)
|
||||
// Get returns a cached version of the given diagram description.
|
||||
Get(diagram *Diagram) (*Diagram, error)
|
||||
}
|
||||
|
||||
// NewDiagramCache returns an implementation of DiagramCache.
|
||||
func NewDiagramCache() DiagramCache {
|
||||
return &inMemoryDiagramCache{
|
||||
idToDiagram: map[string]*Diagram{},
|
||||
}
|
||||
}
|
||||
|
||||
// inMemoryDiagramCache is an in-memory implementation of DiagramCache.
|
||||
type inMemoryDiagramCache struct {
|
||||
idToDiagram map[string]*Diagram
|
||||
}
|
||||
|
||||
// Store stores a diagram in the cache.
|
||||
func (c *inMemoryDiagramCache) Store(diagram *Diagram) error {
|
||||
id, err := diagram.ID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.idToDiagram[id] = diagram
|
||||
return nil
|
||||
}
|
||||
|
||||
// Has returns true if we have a cache stored for the given diagram description.
|
||||
func (c *inMemoryDiagramCache) Has(diagram *Diagram) (bool, error) {
|
||||
id, err := diagram.ID()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if d, ok := c.idToDiagram[id]; ok && d != nil {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get returns a cached version of the given diagram description.
|
||||
func (c *inMemoryDiagramCache) Get(diagram *Diagram) (*Diagram, error) {
|
||||
id, err := diagram.ID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d, ok := c.idToDiagram[id]; ok && d != nil {
|
||||
return d, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
50
internal/diagram.go
Normal file
50
internal/diagram.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NewDiagram returns a new diagram.
|
||||
func NewDiagram(description []byte) *Diagram {
|
||||
return &Diagram{
|
||||
description: []byte(strings.TrimSpace(string(description))),
|
||||
}
|
||||
}
|
||||
|
||||
type Diagram struct {
|
||||
// iD is the ID of the Diagram
|
||||
id string
|
||||
// description is the description of the diagram.
|
||||
description []byte
|
||||
// Output is the filepath to the output file.
|
||||
Output string
|
||||
}
|
||||
|
||||
// ID returns an ID for the diagram.
|
||||
// The ID is set from the diagram description.
|
||||
func (d *Diagram) ID() (string, error) {
|
||||
if d.id != "" {
|
||||
return d.id, nil
|
||||
}
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString(d.description)
|
||||
hash := md5.Sum([]byte(encoded))
|
||||
d.id = hex.EncodeToString(hash[:])
|
||||
|
||||
return d.id, nil
|
||||
}
|
||||
|
||||
// Description returns the diagram description.
|
||||
func (d *Diagram) Description() []byte {
|
||||
return d.description
|
||||
}
|
||||
|
||||
// Description returns the diagram description.
|
||||
func (d *Diagram) WithDescription(description []byte) *Diagram {
|
||||
d.description = description
|
||||
d.id = ""
|
||||
return d
|
||||
}
|
||||
94
internal/generator.go
Normal file
94
internal/generator.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Generator provides the ability to generate a diagram.
|
||||
type Generator interface {
|
||||
// Generate generates the given diagram.
|
||||
Generate(diagram *Diagram) error
|
||||
}
|
||||
|
||||
func NewGenerator(cache DiagramCache, mermaidCLIPath string, inPath string, outPath string) Generator {
|
||||
return &cachingGenerator{
|
||||
cache: cache,
|
||||
mermaidCLIPath: mermaidCLIPath,
|
||||
inPath: inPath,
|
||||
outPath: outPath,
|
||||
}
|
||||
}
|
||||
|
||||
// cachingGenerator is an implementation of Generator.
|
||||
type cachingGenerator struct {
|
||||
cache DiagramCache
|
||||
mermaidCLIPath string
|
||||
inPath string
|
||||
outPath string
|
||||
}
|
||||
|
||||
// Generate generates the given diagram.
|
||||
func (c cachingGenerator) Generate(diagram *Diagram) error {
|
||||
has, err := c.cache.Has(diagram)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if has {
|
||||
cached, err := c.cache.Get(diagram)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*diagram = *cached
|
||||
return nil
|
||||
}
|
||||
if err := c.generate(diagram); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.cache.Store(diagram); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generate does the actual file generation.
|
||||
func (c cachingGenerator) generate(diagram *Diagram) error {
|
||||
id, err := diagram.ID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
has, err := c.cache.Has(diagram)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if has {
|
||||
cached, err := c.cache.Get(diagram)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*diagram = *cached
|
||||
return nil
|
||||
}
|
||||
|
||||
inPath := fmt.Sprintf("%s/%s.mmd", c.inPath, id)
|
||||
outPath := fmt.Sprintf("%s/%s.svg", c.outPath, id)
|
||||
|
||||
if err := ioutil.WriteFile(inPath, diagram.description, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command(c.mermaidCLIPath, "-i", inPath, "-o", outPath)
|
||||
var stdOut bytes.Buffer
|
||||
cmd.Stdout = bufio.NewWriter(&stdOut)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("%w: %s", err, string(stdOut.Bytes()))
|
||||
}
|
||||
|
||||
diagram.Output = outPath
|
||||
|
||||
return nil
|
||||
}
|
||||
67
internal/http.go
Normal file
67
internal/http.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func writeJSON(rw http.ResponseWriter, value interface{}, status int) {
|
||||
bytes, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
panic("could not marshal value: " + err.Error())
|
||||
}
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(status)
|
||||
if _, err := rw.Write(bytes); err != nil {
|
||||
panic("could not write bytes to response: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func writeSVG(rw http.ResponseWriter, data []byte, status int) {
|
||||
rw.Header().Set("Content-Type", "image/svg+xml")
|
||||
rw.WriteHeader(status)
|
||||
if _, err := rw.Write(data); err != nil {
|
||||
panic("could not write bytes to response: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func writeErr(rw http.ResponseWriter, err error, status int) {
|
||||
writeJSON(rw, map[string]interface{}{
|
||||
"error": err,
|
||||
}, status)
|
||||
}
|
||||
|
||||
func GenerateHTTPHandler(generator Generator) func(rw http.ResponseWriter, r *http.Request) {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeErr(rw, fmt.Errorf("expected HTTP method POST"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Get description from request body
|
||||
bytes, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeErr(rw, fmt.Errorf("could not read body: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a diagram from the description
|
||||
d := NewDiagram(bytes)
|
||||
|
||||
// Generate the diagram
|
||||
if err := generator.Generate(d); err != nil {
|
||||
writeErr(rw, fmt.Errorf("could not generate diagram: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Output the diagram as an SVG.
|
||||
// We assume generate always generates an SVG at this point in time.
|
||||
diagramBytes, err := ioutil.ReadFile(d.Output)
|
||||
if err != nil {
|
||||
writeErr(rw, fmt.Errorf("could not read diagram bytes: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeSVG(rw, diagramBytes, http.StatusOK)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user