Commit ee5c842c authored by Matthew Holt's avatar Matthew Holt

Code to convert between JSON and Caddyfile

This will be used by the API so clients have an easier time manipulating the configuration
parent c487b702
package caddyfile
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/mholt/caddy/caddy/parse"
)
const filename = "Caddyfile"
// ToJSON converts caddyfile to its JSON representation.
func ToJSON(caddyfile []byte) ([]byte, error) {
var j Caddyfile
serverBlocks, err := parse.ServerBlocks(filename, bytes.NewReader(caddyfile), false)
if err != nil {
return nil, err
}
for _, sb := range serverBlocks {
block := ServerBlock{Body: make(map[string]interface{})}
for _, host := range sb.HostList() {
block.Hosts = append(block.Hosts, host)
}
for dir, tokens := range sb.Tokens {
disp := parse.NewDispenserTokens(filename, tokens)
disp.Next() // the first token is the directive; skip it
block.Body[dir] = constructLine(disp)
}
j = append(j, block)
}
result, err := json.Marshal(j)
if err != nil {
return nil, err
}
return result, nil
}
// constructLine transforms tokens into a JSON-encodable structure;
// 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
// directive) - not to be used at any other nesting level.
func constructLine(d parse.Dispenser) interface{} {
var args []interface{}
all := d.RemainingArgs()
for _, arg := range all {
args = append(args, arg)
}
d.Next()
if d.Val() == "{" {
args = append(args, constructBlock(d))
}
return args
}
// constructBlock recursively processes tokens into a
// JSON-encodable structure.
func constructBlock(d parse.Dispenser) interface{} {
block := make(map[string]interface{})
for d.Next() {
if d.Val() == "}" {
break
}
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
}
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
func FromJSON(jsonBytes []byte) ([]byte, error) {
var j Caddyfile
var result string
err := json.Unmarshal(jsonBytes, &j)
if err != nil {
return nil, err
}
for _, sb := range j {
for i, host := range sb.Hosts {
if i > 0 {
result += ", "
}
result += host
}
result += jsonToText(sb.Body, 1)
}
return []byte(result), nil
}
// jsonToText recursively transforms a scope of JSON into plain
// Caddyfile text.
func jsonToText(scope interface{}, depth int) string {
var result string
switch val := scope.(type) {
case string:
result += " " + val
case int:
result += " " + strconv.Itoa(val)
case float64:
result += " " + fmt.Sprintf("%f", val)
case bool:
result += " " + fmt.Sprintf("%t", val)
case map[string]interface{}:
result += " {\n"
for param, args := range val {
result += strings.Repeat("\t", depth) + param
result += jsonToText(args, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
case []interface{}:
for _, v := range val {
result += jsonToText(v, depth)
}
}
return result
}
type Caddyfile []ServerBlock
type ServerBlock struct {
Hosts []string `json:"hosts"`
Body map[string]interface{} `json:"body"`
}
package caddyfile
import "testing"
var tests = []struct {
caddyfile, json string
}{
{ // 0
caddyfile: `foo: {
root /bar
}`,
json: `[{"hosts":["foo:"],"body":{"root":["/bar"]}}]`,
},
{ // 1
caddyfile: `host1:, host2: {
dir {
def
}
}`,
json: `[{"hosts":["host1:","host2:"],"body":{"dir":[{"def":null}]}}]`,
},
{ // 2
caddyfile: `host1:, host2: {
dir abc {
def ghi
jklmnop
}
}`,
json: `[{"hosts":["host1:","host2:"],"body":{"dir":["abc",{"def":["ghi"],"jklmnop":null}]}}]`,
},
{ // 3
caddyfile: `host1:1234, host2:5678 {
dir abc {
}
}`,
json: `[{"hosts":["host1:1234","host2:5678"],"body":{"dir":["abc",{}]}}]`,
},
}
func TestToJSON(t *testing.T) {
for i, test := range tests {
output, err := ToJSON([]byte(test.caddyfile))
if err != nil {
t.Errorf("Test %d: %v", i, err)
}
if string(output) != test.json {
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.json, string(output))
}
}
}
func TestFromJSON(t *testing.T) {
for i, test := range tests {
output, err := FromJSON([]byte(test.json))
if err != nil {
t.Errorf("Test %d: %v", i, err)
}
if string(output) != test.caddyfile {
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.caddyfile, string(output))
}
}
}
...@@ -29,7 +29,7 @@ func Load(filename string, input io.Reader) (Group, error) { ...@@ -29,7 +29,7 @@ func Load(filename string, input io.Reader) (Group, error) {
flags := log.Flags() flags := log.Flags()
log.SetFlags(0) log.SetFlags(0)
serverBlocks, err := parse.ServerBlocks(filename, input) serverBlocks, err := parse.ServerBlocks(filename, input, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -5,9 +5,11 @@ import "io" ...@@ -5,9 +5,11 @@ import "io"
// ServerBlocks parses the input just enough to organize tokens, // ServerBlocks parses the input just enough to organize tokens,
// in order, by server block. No further parsing is performed. // in order, by server block. No further parsing is performed.
// Server blocks are returned in the order in which they appear. // If checkDirectives is true, only valid directives will be allowed
func ServerBlocks(filename string, input io.Reader) ([]serverBlock, error) { // otherwise we consider it a parse error. Server blocks are returned
p := parser{Dispenser: NewDispenser(filename, input)} // in the order in which they appear.
func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]serverBlock, error) {
p := parser{Dispenser: NewDispenser(filename, input), checkDirectives: checkDirectives}
blocks, err := p.parseAll() blocks, err := p.parseAll()
return blocks, err return blocks, err
} }
......
...@@ -9,8 +9,9 @@ import ( ...@@ -9,8 +9,9 @@ import (
type parser struct { type parser struct {
Dispenser Dispenser
block serverBlock // current server block being parsed block serverBlock // current server block being parsed
eof bool // if we encounter a valid EOF in a hard place eof bool // if we encounter a valid EOF in a hard place
checkDirectives bool // if true, directives must be known
} }
func (p *parser) parseAll() ([]serverBlock, error) { func (p *parser) parseAll() ([]serverBlock, error) {
...@@ -220,8 +221,10 @@ func (p *parser) directive() error { ...@@ -220,8 +221,10 @@ func (p *parser) directive() error {
dir := p.Val() dir := p.Val()
nesting := 0 nesting := 0
if _, ok := ValidDirectives[dir]; !ok { if p.checkDirectives {
return p.Errf("Unknown directive '%s'", dir) if _, ok := ValidDirectives[dir]; !ok {
return p.Errf("Unknown directive '%s'", dir)
}
} }
// The directive itself is appended as a relevant token // The directive itself is appended as a relevant token
......
...@@ -375,6 +375,6 @@ func setupParseTests() { ...@@ -375,6 +375,6 @@ func setupParseTests() {
func testParser(input string) parser { func testParser(input string) parser {
buf := strings.NewReader(input) buf := strings.NewReader(input)
p := parser{Dispenser: NewDispenser("Test", buf)} p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true}
return p return p
} }
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