Initial commit with basic HTTP endpoint working

This commit is contained in:
Tom Wright
2020-04-06 23:25:47 +01:00
commit c77040304c
18 changed files with 1012 additions and 0 deletions

57
internal/cache.go Normal file
View 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
View 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
View 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
View 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)
}
}