Commit c31e86db authored by Matthew Holt's avatar Matthew Holt

caddyfile: Change JSON format to use arrays, not objects

Since a directive can appear on multiple lines, the object syntax wasn't working well. This also fixes several other serialization bugs.
parent 13557eb5
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
"sort"
"strconv" "strconv"
"strings" "strings"
...@@ -23,18 +24,29 @@ func ToJSON(caddyfile []byte) ([]byte, error) { ...@@ -23,18 +24,29 @@ func ToJSON(caddyfile []byte) ([]byte, error) {
} }
for _, sb := range serverBlocks { for _, sb := range serverBlocks {
block := ServerBlock{Body: make(map[string]interface{})} block := ServerBlock{Body: [][]interface{}{}}
// Fill up host list
for _, host := range sb.HostList() { for _, host := range sb.HostList() {
block.Hosts = append(block.Hosts, strings.TrimSuffix(host, ":")) block.Hosts = append(block.Hosts, strings.TrimSuffix(host, ":"))
} }
for dir, tokens := range sb.Tokens { // Extract directives deterministically by sorting them
disp := parse.NewDispenserTokens(filename, tokens) var directives = make([]string, len(sb.Tokens))
disp.Next() // the first token is the directive; skip it for dir := range sb.Tokens {
block.Body[dir] = constructLine(disp) directives = append(directives, dir)
} }
sort.Strings(directives)
// Convert each directive's tokens into our JSON structure
for _, dir := range directives {
disp := parse.NewDispenserTokens(filename, sb.Tokens[dir])
for disp.Next() {
block.Body = append(block.Body, constructLine(&disp))
}
}
// tack this block onto the end of the list
j = append(j, block) j = append(j, block)
} }
...@@ -50,17 +62,18 @@ func ToJSON(caddyfile []byte) ([]byte, error) { ...@@ -50,17 +62,18 @@ func ToJSON(caddyfile []byte) ([]byte, error) {
// but only one line at a time, to be used at the top-level of // but only one line at a time, to be used at the top-level of
// a server block only (where the first token on each line is a // a server block only (where the first token on each line is a
// directive) - not to be used at any other nesting level. // directive) - not to be used at any other nesting level.
func constructLine(d parse.Dispenser) interface{} { // goes to end of line
func constructLine(d *parse.Dispenser) []interface{} {
var args []interface{} var args []interface{}
all := d.RemainingArgs() args = append(args, d.Val())
for _, arg := range all {
args = append(args, arg)
}
d.Next() for d.NextArg() {
if d.Val() == "{" { if d.Val() == "{" {
args = append(args, constructBlock(d)) args = append(args, constructBlock(d))
continue
}
args = append(args, d.Val())
} }
return args return args
...@@ -68,26 +81,15 @@ func constructLine(d parse.Dispenser) interface{} { ...@@ -68,26 +81,15 @@ func constructLine(d parse.Dispenser) interface{} {
// constructBlock recursively processes tokens into a // constructBlock recursively processes tokens into a
// JSON-encodable structure. // JSON-encodable structure.
func constructBlock(d parse.Dispenser) interface{} { // goes to end of block
block := make(map[string]interface{}) func constructBlock(d *parse.Dispenser) [][]interface{} {
block := [][]interface{}{}
for d.Next() { for d.Next() {
if d.Val() == "}" { if d.Val() == "}" {
break break
} }
block = append(block, constructLine(d))
dir := d.Val()
all := d.RemainingArgs()
var args []interface{}
for _, arg := range all {
args = append(args, arg)
}
if d.Val() == "{" {
args = append(args, constructBlock(d))
}
block[dir] = args
} }
return block return block
...@@ -103,7 +105,10 @@ func FromJSON(jsonBytes []byte) ([]byte, error) { ...@@ -103,7 +105,10 @@ func FromJSON(jsonBytes []byte) ([]byte, error) {
return nil, err return nil, err
} }
for _, sb := range j { for sbPos, sb := range j {
if sbPos > 0 {
result += "\n\n"
}
for i, host := range sb.Hosts { for i, host := range sb.Hosts {
if hostname, port, err := net.SplitHostPort(host); err == nil { if hostname, port, err := net.SplitHostPort(host); err == nil {
if port == "http" || port == "https" { if port == "http" || port == "https" {
...@@ -129,26 +134,36 @@ func jsonToText(scope interface{}, depth int) string { ...@@ -129,26 +134,36 @@ func jsonToText(scope interface{}, depth int) string {
switch val := scope.(type) { switch val := scope.(type) {
case string: case string:
if strings.ContainsAny(val, "\" \n\t\r") { if strings.ContainsAny(val, "\" \n\t\r") {
result += ` "` + strings.Replace(val, "\"", "\\\"", -1) + `"` result += `"` + strings.Replace(val, "\"", "\\\"", -1) + `"`
} else { } else {
result += " " + val result += val
} }
case int: case int:
result += " " + strconv.Itoa(val) result += strconv.Itoa(val)
case float64: case float64:
result += " " + fmt.Sprintf("%v", val) result += fmt.Sprintf("%v", val)
case bool: case bool:
result += " " + fmt.Sprintf("%t", val) result += fmt.Sprintf("%t", val)
case map[string]interface{}: case [][]interface{}:
result += " {\n" result += " {\n"
for param, args := range val { for _, arg := range val {
result += strings.Repeat("\t", depth) + param result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
result += jsonToText(args, depth+1) + "\n"
} }
result += strings.Repeat("\t", depth-1) + "}" result += strings.Repeat("\t", depth-1) + "}"
case []interface{}: case []interface{}:
for _, v := range val { for i, v := range val {
if block, ok := v.([]interface{}); ok {
result += "{\n"
for _, arg := range block {
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
continue
}
result += jsonToText(v, depth) result += jsonToText(v, depth)
if i < len(val)-1 {
result += " "
}
} }
} }
...@@ -161,5 +176,5 @@ type Caddyfile []ServerBlock ...@@ -161,5 +176,5 @@ type Caddyfile []ServerBlock
// ServerBlock represents a server block. // ServerBlock represents a server block.
type ServerBlock struct { type ServerBlock struct {
Hosts []string `json:"hosts"` Hosts []string `json:"hosts"`
Body map[string]interface{} `json:"body"` Body [][]interface{} `json:"body"`
} }
...@@ -9,7 +9,7 @@ var tests = []struct { ...@@ -9,7 +9,7 @@ var tests = []struct {
caddyfile: `foo { caddyfile: `foo {
root /bar root /bar
}`, }`,
json: `[{"hosts":["foo"],"body":{"root":["/bar"]}}]`, json: `[{"hosts":["foo"],"body":[["root","/bar"]]}]`,
}, },
{ // 1 { // 1
caddyfile: `host1, host2 { caddyfile: `host1, host2 {
...@@ -17,52 +17,87 @@ var tests = []struct { ...@@ -17,52 +17,87 @@ var tests = []struct {
def def
} }
}`, }`,
json: `[{"hosts":["host1","host2"],"body":{"dir":[{"def":null}]}}]`, json: `[{"hosts":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
}, },
{ // 2 { // 2
caddyfile: `host1, host2 { caddyfile: `host1, host2 {
dir abc { dir abc {
def ghi def ghi
jkl
} }
}`, }`,
json: `[{"hosts":["host1","host2"],"body":{"dir":["abc",{"def":["ghi"]}]}}]`, json: `[{"hosts":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
}, },
{ // 3 { // 3
caddyfile: `host1:1234, host2:5678 { caddyfile: `host1:1234, host2:5678 {
dir abc { dir abc {
} }
}`, }`,
json: `[{"hosts":["host1:1234","host2:5678"],"body":{"dir":["abc",{}]}}]`, json: `[{"hosts":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
}, },
{ // 4 { // 4
caddyfile: `host { caddyfile: `host {
foo "bar baz" foo "bar baz"
}`, }`,
json: `[{"hosts":["host"],"body":{"foo":["bar baz"]}}]`, json: `[{"hosts":["host"],"body":[["foo","bar baz"]]}]`,
}, },
{ // 5 { // 5
caddyfile: `host, host:80 { caddyfile: `host, host:80 {
foo "bar \"baz\"" foo "bar \"baz\""
}`, }`,
json: `[{"hosts":["host","host:80"],"body":{"foo":["bar \"baz\""]}}]`, json: `[{"hosts":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
}, },
{ // 6 { // 6
caddyfile: `host { caddyfile: `host {
foo "bar foo "bar
baz" baz"
}`, }`,
json: `[{"hosts":["host"],"body":{"foo":["bar\nbaz"]}}]`, json: `[{"hosts":["host"],"body":[["foo","bar\nbaz"]]}]`,
}, },
{ // 7 { // 7
caddyfile: `host { caddyfile: `host {
dir 123 4.56 true dir 123 4.56 true
}`, }`,
json: `[{"hosts":["host"],"body":{"dir":["123","4.56","true"]}}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...? json: `[{"hosts":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...?
}, },
{ // 8 { // 8
caddyfile: `http://host, https://host { caddyfile: `http://host, https://host {
}`, }`,
json: `[{"hosts":["host:http","host:https"],"body":{}}]`, // hosts in JSON are always host:port format (if port is specified), for consistency json: `[{"hosts":["host:http","host:https"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
},
{ // 9
caddyfile: `host {
dir1 a b
dir2 c d
}`,
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
},
{ // 10
caddyfile: `host {
dir a b
dir c d
}`,
json: `[{"hosts":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
},
{ // 11
caddyfile: `host {
dir1 a b
dir2 {
c
d
}
}`,
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
},
{ // 12
caddyfile: `host1 {
dir1
}
host2 {
dir2
}`,
json: `[{"hosts":["host1"],"body":[["dir1"]]},{"hosts":["host2"],"body":[["dir2"]]}]`,
}, },
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment