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()
 | 
						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"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// 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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ 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,9 +221,11 @@ func (p *parser) directive() error {
 | 
				
			|||||||
	dir := p.Val()
 | 
						dir := p.Val()
 | 
				
			||||||
	nesting := 0
 | 
						nesting := 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.checkDirectives {
 | 
				
			||||||
		if _, ok := ValidDirectives[dir]; !ok {
 | 
							if _, ok := ValidDirectives[dir]; !ok {
 | 
				
			||||||
			return p.Errf("Unknown directive '%s'", dir)
 | 
								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
 | 
				
			||||||
	p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
 | 
						p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user