mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-04 03:27:23 -05:00 
			
		
		
		
	Code to convert between JSON and Caddyfile
This will be used by the API so clients have an easier time manipulating the configuration
This commit is contained in:
		
							parent
							
								
									c487b702a2
								
							
						
					
					
						commit
						ee5c842c7d
					
				
							
								
								
									
										153
									
								
								caddy/caddyfile/json.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								caddy/caddyfile/json.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,153 @@
 | 
			
		||||
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"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										62
									
								
								caddy/caddyfile/json_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								caddy/caddyfile/json_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
			
		||||
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) {
 | 
			
		||||
	flags := log.Flags()
 | 
			
		||||
	log.SetFlags(0)
 | 
			
		||||
 | 
			
		||||
	serverBlocks, err := parse.ServerBlocks(filename, input)
 | 
			
		||||
	serverBlocks, err := parse.ServerBlocks(filename, input, true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -5,9 +5,11 @@ import "io"
 | 
			
		||||
 | 
			
		||||
// ServerBlocks parses the input just enough to organize tokens,
 | 
			
		||||
// in order, by server block. No further parsing is performed.
 | 
			
		||||
// Server blocks are returned in the order in which they appear.
 | 
			
		||||
func ServerBlocks(filename string, input io.Reader) ([]serverBlock, error) {
 | 
			
		||||
	p := parser{Dispenser: NewDispenser(filename, input)}
 | 
			
		||||
// If checkDirectives is true, only valid directives will be allowed
 | 
			
		||||
// otherwise we consider it a parse error. Server blocks are returned
 | 
			
		||||
// 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()
 | 
			
		||||
	return blocks, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ type parser struct {
 | 
			
		||||
	Dispenser
 | 
			
		||||
	block           serverBlock // current server block being parsed
 | 
			
		||||
	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) {
 | 
			
		||||
@ -220,9 +221,11 @@ func (p *parser) directive() error {
 | 
			
		||||
	dir := p.Val()
 | 
			
		||||
	nesting := 0
 | 
			
		||||
 | 
			
		||||
	if p.checkDirectives {
 | 
			
		||||
		if _, ok := ValidDirectives[dir]; !ok {
 | 
			
		||||
			return p.Errf("Unknown directive '%s'", dir)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// The directive itself is appended as a relevant token
 | 
			
		||||
	p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
 | 
			
		||||
 | 
			
		||||
@ -375,6 +375,6 @@ func setupParseTests() {
 | 
			
		||||
 | 
			
		||||
func testParser(input string) parser {
 | 
			
		||||
	buf := strings.NewReader(input)
 | 
			
		||||
	p := parser{Dispenser: NewDispenser("Test", buf)}
 | 
			
		||||
	p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true}
 | 
			
		||||
	return p
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user