Skip to content

Commit

Permalink
Merge pull request #2 from cneill/http
Browse files Browse the repository at this point in the history
HTTP
  • Loading branch information
cneill authored Sep 18, 2023
2 parents fcc1cbc + e679fb2 commit 4aeb81b
Show file tree
Hide file tree
Showing 13 changed files with 572 additions and 47 deletions.
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,34 @@ NAME:
jsonstruct - generate Go structs for JSON values
USAGE:
jsonstruct [global options] command [command options] [file]...
jsonstruct [global options] command [command options] [FILE]...
DESCRIPTION:
You can either pass in files as args or JSON in STDIN. Results are printed to STDOUT.
COMMANDS:
http run a web app to generate structs in the browser
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--name value, -n value override the default name derived from filename
--value-comments, -c add a comment to struct fields with the example value(s) (default: false)
--sort-fields, -s sort the fields in alphabetical order; default behavior is to mirror input (default: false)
--inline-structs, -i use inline structs instead of creating different types for each object (default: false)
--print-filenames, -f print the filename above the structs defined within (default: false)
--debug enable debug logs (default: false)
--help, -h show help
--name value, -n value override the default name derived from filename
--value-comments, -c add a comment to struct fields with the example value(s) (default: false)
--sort-fields, -s sort the fields in alphabetical order; default behavior is to mirror input (default: false)
--inline-structs, -i use inline structs instead of creating different types for each object (default: false)
--print-filenames, -f print the filename above the structs defined within (default: false)
--out-file FILE, -o FILE write the results to FILE
--debug, -d enable debug logs (default: false)
--help, -h show help
```

## Examples

### Webapp

![http-screenshot](./img/http-screenshot.png)

The `http` command allows you to run a webapp to generate these structs in the browser.

### JSON object

**Input:**
Expand Down
24 changes: 24 additions & 0 deletions cmd/jsonstruct/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import "github.com/urfave/cli/v2"

func httpCommand() *cli.Command {
return &cli.Command{
Name: "http",
Action: httpListener,
Usage: "run a web app to generate structs in the browser",
Description: "This will run a web app to let you generate new structs in your browser, listening on localhost:8080 by default.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "host",
Usage: "the `HOST` to listen on",
Value: "127.0.0.1",
},
&cli.IntFlag{
Name: "port",
Usage: "the `PORT` to listen on",
Value: 8080,
},
},
}
}
129 changes: 129 additions & 0 deletions cmd/jsonstruct/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package main

import (
"embed"
"fmt"
"html/template"
"io/fs"
"net/http"
"strings"

"github.com/cneill/jsonstruct"
"github.com/urfave/cli/v2"
)

//go:embed http/static/*
var staticContent embed.FS

//go:embed http/templates
var templatesContent embed.FS

func httpListener(ctx *cli.Context) error {
staticFS, err := fs.Sub(staticContent, "http/static")
if err != nil {
return fmt.Errorf("failed to set up static files: %w", err)
}

mux := http.NewServeMux()

mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
mux.HandleFunc("/generate", GenerateHandler)
mux.HandleFunc("/", IndexHandler)

listen := fmt.Sprintf("%s:%d", ctx.String("host"), ctx.Int("port"))

fmt.Printf("Listening on %s...\n", listen)

//nolint:gosec // I don't think timeouts matter that much for this use case
if err := http.ListenAndServe(listen, mux); err != nil {
return fmt.Errorf("listening error: %w", err)
}

return nil
}

// GenerateHandler serves the generated content.
func GenerateHandler(writer http.ResponseWriter, req *http.Request) {
generateTemplate, err := template.New("generate").ParseFS(templatesContent, "http/templates/*.gohtml")
if err != nil {
doErr(writer, fmt.Errorf("failed to load generate template: %w", err))
return
}

if err := req.ParseForm(); err != nil {
doErr(writer, fmt.Errorf("failed to parse form: %w", err))
return
}

input := req.PostForm.Get("input")
if input == "" {
return
}

r := strings.NewReader(input)

parser := jsonstruct.NewParser(r, log)

jStructs, err := parser.Start()
if err != nil {
doErr(writer, fmt.Errorf("failed to parse input: %w", err))
return
}

// set the names of the top-level structs from our example file based on the file's name
name := "WebGenerated"

for i := 0; i < len(jStructs); i++ {
jStructs[i].SetName(fmt.Sprintf("%s%d", name, i+1))
}

formatter, err := jsonstruct.NewFormatter(&jsonstruct.FormatterOptions{
SortFields: req.PostForm.Get("sort_fields") == "on",
ValueComments: req.PostForm.Get("value_comments") == "on",
InlineStructs: req.PostForm.Get("inline_structs") == "on",
})
if err != nil {
doErr(writer, fmt.Errorf("failed to set up formatter: %w", err))
return
}

result, err := formatter.FormatStructs(jStructs...)
if err != nil {
doErr(writer, fmt.Errorf("failed to format structs: %w", err))
return
}

// fmt.Fprintf(outFile, "%s\n", result)

data := struct {
Generated string
}{result}

if err := generateTemplate.Execute(writer, data); err != nil {
doErr(writer, fmt.Errorf("failed to execute generate template: %w", err))
return
}
}

// IndexHandler serves the main page.
func IndexHandler(writer http.ResponseWriter, _ *http.Request) {
indexTemplate, err := template.New("index").ParseFS(templatesContent, "http/templates/*.gohtml")
if err != nil {
doErr(writer, fmt.Errorf("failed to load index template: %w", err))
return
}

if err := indexTemplate.Execute(writer, nil); err != nil {
doErr(writer, fmt.Errorf("failed to execute index template: %w", err))
return
}
}

func doErr(writer http.ResponseWriter, err error) {
fmt.Printf("ERROR WITH REQUEST: %v\n", err)
writer.WriteHeader(http.StatusBadRequest)

if _, err := writer.Write([]byte(fmt.Sprintf("%v", err))); err != nil {
fmt.Printf("error writing error: %v\n", err)
}
}
1 change: 1 addition & 0 deletions cmd/jsonstruct/http/static/htmx.min.js

Large diffs are not rendered by default.

88 changes: 88 additions & 0 deletions cmd/jsonstruct/http/static/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
.body {
background-color: #000000;
color: #ffffff;
padding: 0;
margin: 0;
}

.form, .output-pre {
margin: 0 !important;
padding: 0 !important;
width: 100%;
height: 100%;
}

.container {
display: grid;
grid-template-rows: 80% 20%;
grid-template-columns: 50% 50%;
height: 100vh;
width: 100vw;
padding: 0;
margin: 0;
}

.input-container {
grid-row: 1/2;
grid-column: 1/2;
margin: 0;
padding: 0;
}

.output-container {
grid-row: 1/2;
grid-column: 2/3;
margin: 0;
padding: 0;
}

.options-container {
padding-top: 10px;
grid-row: 2/3;
grid-column: 1/3;
}

.input, .output {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
background-color: #000000;
color: #ffffff;
font-family: Lucida Console, monospace;
border-color: #333333;
}

.fieldset {
border-radius: 5px;
}

.fieldset-legend {
color: #000000;
background-color: #ffffff;
border-radius: 5px;
}

.button {
border-width: 3px;
border-style: solid;
border-radius: 3px;
font-weight: bold;
cursor: pointer;
}

.button--green {
background-color: #33aa44;
border-color: #1e5926;
color: #ffffff;
}

.button--blue {
background-color: #1122ff;
border-color: #0d157e;
color: #ffffff;
}

.htmx-indicator {
display: none;
}
46 changes: 46 additions & 0 deletions cmd/jsonstruct/http/static/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
function clipboardCopy() {
let outputElem = document.querySelector("#output");

if (outputElem) {
navigator.clipboard.writeText(outputElem.innerText);
}
}


window.onload = function() {
var copy = document.querySelector("#copy");

copy.addEventListener("click", e => {
clipboardCopy();
});
}

document.body.addEventListener("htmx:beforeRequest", e => {
let inputElem = e.detail.elt.querySelector(".input");
if (!inputElem) {
return;
}

try {
JSON.parse(inputElem.value);
} catch (err) {
if (inputElem.value != "") {
e.preventDefault();
}
}
});

document.body.addEventListener("htmx:beforeSwap", e => {
if (e.detail.xhr.status >= 400) {
e.detail.shouldSwap = false;
e.detail.isError = true;
}
});

document.body.addEventListener("htmx:afterSwap", e => {
let codeElem = e.detail.elt.querySelector(".output");

if (codeElem) {
Prism.highlightElement(codeElem);
}
});
Binary file added cmd/jsonstruct/http/static/loading.webp
Binary file not shown.
Loading

0 comments on commit 4aeb81b

Please sign in to comment.