~evanj/cms

111c8a2fc2ab09cbe2fb53bf73a65fea37b95a4f — Evan M Jones 8 months ago 7fc5d72
WIP(*): Project init.
M cms.go => cms.go +13 -5
@@ 7,6 7,7 @@ import (
	"os"
	"strings"

	"git.sr.ht/~evanj/cms/internal/c/contenttype"
	"git.sr.ht/~evanj/cms/internal/c/ping"
	"git.sr.ht/~evanj/cms/internal/c/space"
	"git.sr.ht/~evanj/cms/internal/c/user"


@@ 25,10 26,11 @@ var (
)

type App struct {
	log   *log.Logger
	space http.Handler
	user  http.Handler
	ping  http.Handler
	log         *log.Logger
	contenttype http.Handler
	space       http.Handler
	user        http.Handler
	ping        http.Handler
}

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {


@@ 50,6 52,9 @@ func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	case "space":
		a.space.ServeHTTP(w, r)
		return
	case "contenttype":
		a.contenttype.ServeHTTP(w, r)
		return
	}

	http.NotFound(w, r)


@@ 68,13 73,16 @@ func init() {
	if err != nil {
		applogger.Fatal(err)
	}

	if err := db.EnsureSetup(); err != nil {
		applogger.Fatal(err)
	}

	app = &App{
		log: applogger,
		contenttype: contenttype.New(
			log.New(w, "[cms:contenttype] ", 0),
			db,
		),
		space: space.New(
			log.New(w, "[cms:space] ", 0),
			db,

M go.mod => go.mod +2 -0
@@ 4,5 4,7 @@ go 1.12

require (
	git.sr.ht/~evanj/security v0.0.0-20200228044358-9b9bc6682997
	github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
	github.com/kr/pretty v0.2.0 // indirect
	github.com/mattn/go-sqlite3 v2.0.3+incompatible
)

M go.sum => go.sum +7 -0
@@ 2,8 2,15 @@ git.sr.ht/~evanj/security v0.0.0-20200228044358-9b9bc6682997 h1:fdAj8fR4mpS/OAve
git.sr.ht/~evanj/security v0.0.0-20200228044358-9b9bc6682997/go.mod h1:40791KVgThT97CT6mTsF4NUNPeX2BAVlsuH1RiiSrAs=
github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg=
github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

A internal/c/contenttype/contenttype.go => internal/c/contenttype/contenttype.go +138 -0
@@ 0,0 1,138 @@
package contenttype

import (
	"fmt"
	"log"
	"net/http"
	"strings"

	"git.sr.ht/~evanj/cms/internal/c"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/s/db"
	"git.sr.ht/~evanj/cms/internal/s/tmpl"
)

var (
	contenttypeHTML = tmpl.MustParse("html/contenttype.html")
)

type ContentType struct {
	*c.Controller
	log *log.Logger
	db  dber
}

type dber interface {
	UserGetFromToken(token string) (user.User, error)
	SpaceGet(user user.User, spaceID string) (space.Space, error)
	ContentTypeNew(space space.Space, name string, params []db.ContentTypeNewParam) (contenttype.ContentType, error)
	ContentTypeGet(space space.Space, contenttypeID string) (contenttype.ContentType, error)
}

func New(log *log.Logger, db dber) *ContentType {
	return &ContentType{
		c.New(log, db),
		log,
		db,
	}
}

func (c *ContentType) create(w http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")
	spaceID := r.FormValue("space")

	i := 1
	for key := range r.Form {
		if strings.Contains(key, "field_type_") || strings.Contains(key, "field_name_") {
			i++
		}
	}

	var params []db.ContentTypeNewParam
	for e := 0; e < (i / 2); e++ {
		keyName := fmt.Sprintf("field_name_%d", e+1)
		keyType := fmt.Sprintf("field_type_%d", e+1)
		valName := r.FormValue(keyName)
		valType := r.FormValue(keyType)
		if valName == "" || valType == "" {
			c.Error(w, r, http.StatusBadRequest, "form has malformed data")
			return
		}
		params = append(params, db.ContentTypeNewParam{
			Name: valName,
			Type: valType,
		})
	}

	if len(params) < 1 {
		c.Error(w, r, http.StatusBadRequest, "contenttype must have at least one field")
		return
	}

	user, err := c.GetCookieUser(w, r)
	if err != nil {
		c.Error(w, r, http.StatusBadRequest, "must be logged in to create contenttype")
		return
	}

	space, err := c.db.SpaceGet(user, spaceID)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find desired space")
		return
	}

	ct, err := c.db.ContentTypeNew(space, name, params)
	if err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to create contenttype")
		return
	}

	url := fmt.Sprintf("/contenttype/%s/%s", space.ID(), ct.ID())
	c.log.Println("successfully created new contenttype for user", user.Name(), "in space", space.Name(), "redirecting to", url)
	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

func (c *ContentType) serve(w http.ResponseWriter, r *http.Request, spaceID, contenttypeID string) {
	user, err := c.GetCookieUser(w, r)
	if err != nil {
		c.Error(w, r, http.StatusBadRequest, "must be logged in to create contenttype")
		return
	}

	space, err := c.db.SpaceGet(user, spaceID)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find desired space")
		return
	}

	ct, err := c.db.ContentTypeGet(space, contenttypeID)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find desired contenttype")
		return
	}

	c.HTML(w, r, contenttypeHTML, map[string]interface{}{
		"User":        user,
		"Space":       space,
		"ContentType": ct,
	})
}

func (c *ContentType) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	switch r.URL.Path {
	case "/contenttype/new":
		c.create(w, r)
		return
	}

	parts := strings.Split(r.URL.Path, "/")
	if len(parts) > 3 {
		c.serve(w, r, parts[2], parts[3])
		return
	}

	http.NotFound(w, r)
}

A internal/c/contenttype/contenttype_test.go => internal/c/contenttype/contenttype_test.go +1 -0
@@ 0,0 1,1 @@
package contenttype_test

M internal/c/space/space.go => internal/c/space/space.go +19 -2
@@ 4,9 4,11 @@ import (
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"

	"git.sr.ht/~evanj/cms/internal/c"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/s/tmpl"


@@ 26,6 28,7 @@ type dber interface {
	UserGetFromToken(token string) (user.User, error)
	SpaceNew(user user.User, name, desc string) (space.Space, error)
	SpaceGet(user user.User, spaceID string) (space.Space, error)
	ContentTypesPerSpace(space space.Space, page int) ([]contenttype.ContentType, error)
}

func New(log *log.Logger, db dber) *Space {


@@ 49,9 52,23 @@ func (s *Space) serve(w http.ResponseWriter, r *http.Request, spaceID string) {
		return
	}

	page, err := strconv.Atoi(r.URL.Query().Get("page"))
	if err != nil || page < 1 {
		page = 1
	}

	page-- // Show one to user but start counting at zero for us.

	cts, err := s.db.ContentTypesPerSpace(space, page)
	if err != nil {
		s.Error(w, r, http.StatusInternalServerError, "failed to find contenttypes for space")
		return
	}

	s.HTML(w, r, spaceHTML, map[string]interface{}{
		"User":  user,
		"Space": space,
		"User":         user,
		"Space":        space,
		"ContentTypes": cts,
	})
}


M internal/c/user/user.go => internal/c/user/user.go +1 -1
@@ 90,7 90,7 @@ func (l *User) home(w http.ResponseWriter, r *http.Request) {

	spaces, err := l.db.SpacesPerUser(user, page)
	if err != nil {
		l.Error(w, r, http.StatusInternalServerError, "filed to find spaces for user")
		l.Error(w, r, http.StatusInternalServerError, "failed to find spaces for user")
		return
	}


M internal/m/space/space.go => internal/m/space/space.go +1 -0
@@ 3,4 3,5 @@ package space
type Space interface {
	ID() string
	Name() string
	Desc() string
}

M internal/m/valuetype/valuetype.go => internal/m/valuetype/valuetype.go +10 -6
@@ 1,15 1,19 @@
package valuetype

type ValueTypeEnum = int
type ValueTypeEnum = string

const (
	StringSmall ValueTypeEnum = iota
	StringBig
	File
	Reference
	StringSmall   ValueTypeEnum = "StringSmall"
	StringBig     ValueTypeEnum = "StringBig"
	InputHTML     ValueTypeEnum = "InputHTML"
	InputMarkdown ValueTypeEnum = "InputMarkdown"
	File          ValueTypeEnum = "File"
	Date          ValueTypeEnum = "Date"
	Reference     ValueTypeEnum = "Reference"
)

type ValueType interface {
	ID() string
	Name() ValueTypeEnum
	Type() ValueTypeEnum
	Name() string
}

A internal/s/db/contenttype.go => internal/s/db/contenttype.go +172 -0
@@ 0,0 1,172 @@
package db

import (
	"fmt"

	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
)

type ContentType struct {
	id     string
	name   string
	fields []ContentTypeField
}

type ContentTypeField struct {
	id, name, typ string
}

type ContentTypeNewParam struct {
	Name, Type string
}

var (
	queryCreateContentType           = `INSERT INTO cms_contenttype (NAME, SPACE_ID) VALUES (?, ?);`
	queryFindContentTypeByID         = `SELECT ID, NAME FROM cms_contenttype WHERE ID = ?`
	queryFindContentTypeByIDAndSpace = `SELECT ID, NAME FROM cms_contenttype WHERE ID = ? AND SPACE_ID = ?`
	queryDeleteContentTypeByID       = `DELETE FROM cms_space WHERE ID = ?;`
	queryFindContentTypesBySpace     = `SELECT ID, NAME FROM cms_contenttype WHERE SPACE_ID = ? LIMIT ? OFFSET ?`
	queryCreateContentTypeConnection = `INSERT INTO cms_contenttype_to_valuetype (NAME, CONTENTTYPE_ID, VALUETYPE_ID) VALUES (?, ?, ( SELECT ID FROM cms_valuetype WHERE VALUE = ? ));`
	queryFindValueTypes              = `SELECT cms_contenttype_to_valuetype.ID, NAME, VALUE FROM cms_contenttype_to_valuetype JOIN cms_valuetype ON VALUETYPE_ID = cms_valuetype.ID WHERE CONTENTTYPE_ID = ? ORDER BY cms_contenttype_to_valuetype.ID ASC;`
)

func (db *DB) ContentTypeNew(space space.Space, name string, params []ContentTypeNewParam) (contenttype.ContentType, error) {
	// var eg errgroup.Group

	res, err := db.Exec(queryCreateContentType, name, space.ID())
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to create contenttype")
	}

	id, err := res.LastInsertId()
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find identifier of contenttype")
	}

	for _, item := range params {
		// func(eg *errgroup.Group, contenttypeID int64, item ContentTypeNewParam) {
		// eg.Go(func() error {
		if _, err := db.Exec(queryCreateContentTypeConnection, item.Name, id, item.Type); err != nil {
			if _, err := db.Exec(queryDeleteContentTypeByID, id); err != nil {
				db.log.Println("huge problem, failed to delete orphan contenttype", err)
				return nil, fmt.Errorf("failed to create field(s) and created orphan contenttype")
			}
			return nil, fmt.Errorf("failed to create field(s)")
		}
		// })
		// }(&eg, id, item)
	}

	// if err := eg.Wait(); err != nil {
	// 	// We've partially created contenttype but have ultimately failed, attempt
	// 	// to cleanup old data.
	// 	if _, err := db.Exec(queryDeleteContentTypeByID, id); err != nil {
	// 		db.log.Println("huge problem, failed to delete orphan contenttype", err)
	// 		return nil, fmt.Errorf("failed to create field(s) and created orphan contenttype")
	// 	}
	// 	return nil, fmt.Errorf("failed to create field(s)")
	// }

	var ct ContentType
	if err := db.QueryRow(queryFindContentTypeByID, id).Scan(&ct.id, &ct.name); err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find user created")
	}

	rows, err := db.Query(queryFindValueTypes, ct.id)
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find field(s)")
	}
	for rows.Next() {
		var field ContentTypeField
		if err := rows.Scan(&field.id, &field.name, &field.typ); err != nil {
			return nil, fmt.Errorf("failed to scan field(s)")
		}
		ct.fields = append(ct.fields, field)
	}

	if len(ct.fields) != len(params) {
		return nil, fmt.Errorf("failed to create all fields")
	}

	return &ct, nil
}

func (db *DB) ContentTypesPerSpace(space space.Space, page int) ([]contenttype.ContentType, error) {
	var ret []contenttype.ContentType
	rows, err := db.Query(queryFindContentTypesBySpace, space.ID(), perPage, perPage*page)
	if err != nil {
		db.log.Println(err)
		return ret, err
	}

	for rows.Next() {
		var ct ContentType
		if err := rows.Scan(&ct.id, &ct.name); err != nil {
			return nil, err
		}
		ret = append(ret, &ct)
	}

	return ret, nil
}

func (db *DB) ContentTypeGet(space space.Space, contenttypeID string) (contenttype.ContentType, error) {
	var ct ContentType
	if err := db.QueryRow(queryFindContentTypeByIDAndSpace, contenttypeID, space.ID()).Scan(&ct.id, &ct.name); err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find contenttype for space")
	}

	rows, err := db.Query(queryFindValueTypes, ct.id)
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find field(s)")
	}
	for rows.Next() {
		var field ContentTypeField
		if err := rows.Scan(&field.id, &field.name, &field.typ); err != nil {
			return nil, fmt.Errorf("failed to scan field(s)")
		}
		ct.fields = append(ct.fields, field)
	}

	return &ct, nil
}

func (ct *ContentType) ID() string {
	return ct.id
}

func (ct *ContentType) Name() string {
	return ct.name
}

func (ct *ContentType) Fields() []valuetype.ValueType {
	var ret []valuetype.ValueType
	for _, item := range ct.fields {
		ret = append(ret, &ContentTypeField{
			item.id,
			item.name,
			item.typ,
		})
	}
	return ret
}

func (f *ContentTypeField) ID() string {
	return f.id
}

func (f *ContentTypeField) Name() string {
	return f.name
}

func (f *ContentTypeField) Type() string {
	return f.typ
}

M internal/s/db/db.go => internal/s/db/db.go +44 -2
@@ 4,6 4,7 @@ import (
	"database/sql"
	"log"

	"git.sr.ht/~evanj/cms/internal/m/valuetype"
	"git.sr.ht/~evanj/security"
	_ "github.com/mattn/go-sqlite3"
)


@@ 65,12 66,53 @@ func (db *DB) EnsureSetup() error {
	_, _ = db.Exec(`
		CREATE TABLE cms_user_to_space (
			ID INTEGER PRIMARY KEY AUTOINCREMENT,
			USER_ID INTEGER,
			SPACE_ID INTEGER,
			USER_ID INTEGER NOT NULL,
			SPACE_ID INTEGER NOT NULL,
			FOREIGN KEY(USER_ID) REFERENCES cms_user(ID),
			FOREIGN KEY(SPACE_ID) REFERENCES cms_space(ID)
		);
	`)

	// contenttype
	_, _ = db.Exec(`
		CREATE TABLE cms_contenttype (
			ID INTEGER PRIMARY KEY AUTOINCREMENT,
			NAME TEXT NOT NULL,
			SPACE_ID INTEGER NOT NULL,
			CONSTRAINT FG FOREIGN KEY(SPACE_ID) REFERENCES cms_space(ID),
			CONSTRAINT UNIQUEPERCONN UNIQUE(SPACE_ID, NAME)
		);
	`)

	// contenttype to valuetype
	_, _ = db.Exec(`
		CREATE TABLE cms_contenttype_to_valuetype (
			ID INTEGER PRIMARY KEY AUTOINCREMENT,
			NAME TEXT NOT NULL,
			CONTENTTYPE_ID INTEGER NOT NULL,
			VALUETYPE_ID INTEGER NOT NULL,
			FOREIGN KEY(CONTENTTYPE_ID) REFERENCES cms_contenttype(ID)
			FOREIGN KEY(VALUETYPE_ID) REFERENCES cms_valuetype(ID)
		);
	`)

	// valuetype
	// This will never be created by users.
	_, _ = db.Exec(`
		CREATE TABLE cms_valuetype (
			ID INTEGER PRIMARY KEY AUTOINCREMENT,
			VALUE TEXT UNIQUE NOT NULL
		);
	`)

	// Only valuetypes cms supports.
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.StringSmall)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.StringBig)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.InputHTML)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.InputMarkdown)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.File)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.Date)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.Reference)

	return nil
}

M internal/s/db/db_test.go => internal/s/db/db_test.go +46 -0
@@ 1,1 1,47 @@
package db_test

import (
	"log"
	"os"
	"testing"

	"git.sr.ht/~evanj/cms/internal/m/valuetype"
	"git.sr.ht/~evanj/cms/internal/s/db"
	"git.sr.ht/~evanj/security"
	"github.com/bmizerany/assert"
)

var conn, dberr = db.New(
	log.New(os.Stdout, "", 0),
	os.Getenv("TEST_DBTYPE"),
	os.Getenv("TEST_DB"),
	security.Default(os.Getenv("TEST_SECRET")),
)

func TestBasic(t *testing.T) {
	t.Parallel()

	// Create tables
	assert.Equal(t, nil, dberr)
	assert.Equal(t, nil, conn.EnsureSetup())

	// Create user
	user, err := conn.UserNew("tester", "passer", "passer")
	assert.Equal(t, nil, err)
	assert.Equal(t, "tester", user.Name())

	// Create space
	space, err := conn.SpaceNew(user, "spacer", "descer")
	assert.Equal(t, nil, err)
	assert.Equal(t, "spacer", space.Name())
	assert.Equal(t, "descer", space.Desc())

	// Create contenttype
	ct, err := conn.ContentTypeNew(space, "bloger", []db.ContentTypeNewParam{
		db.ContentTypeNewParam{"titler", valuetype.StringSmall},
		db.ContentTypeNewParam{"slugger", valuetype.StringSmall},
		db.ContentTypeNewParam{"descer", valuetype.StringBig},
	})
	assert.Equal(t, nil, err)
	assert.Equal(t, "blogger", ct.Name())
}

M internal/s/db/space.go => internal/s/db/space.go +4 -1
@@ 72,7 72,6 @@ func (db *DB) SpaceGet(user user.User, spaceID string) (space.Space, error) {

func (db *DB) SpacesPerUser(user user.User, page int) ([]space.Space, error) {
	var ret []space.Space

	rows, err := db.Query(queryFindSpacesByUser, user.ID(), perPage, perPage*page)
	if err != nil {
		db.log.Println(err)


@@ 97,3 96,7 @@ func (s *Space) ID() string {
func (s *Space) Name() string {
	return s.name
}

func (s *Space) Desc() string {
	return s.desc
}

A internal/s/tmpl/html/contenttype.html => internal/s/tmpl/html/contenttype.html +58 -0
@@ 0,0 1,58 @@
<!DOCTYPE html>
<html lang=en>

<head>
  <meta charset="utf-8">
  <title>CMS | {{ .Space.Name }}</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  <style>
    main { max-width: 600px; margin: 0 auto; }
    form input, form button { display: block; margin: 5px 0; }
    form div input, form div button { display: inline-block; }
    form > input { margin-top: 10px; }
  </style>

  <main>
    <header>
      <h1>CMS</h1>
      <p>A flexible CMS for everyone.</p>
    </header>
    <hr/>
    <article>

      <nav>
        <a href='/space/{{ .Space.ID }}'>Back</a>
      </nav>

      <h2>{{ .Space.Name }}: {{ .ContentType.Name }}</h2>

      <form method=POST action='/content/new' enctype='multipart/form-data'>
        <legend>Create Content:</legend>
        <input required type=hidden name=space value="{{ .Space.ID }}" />
        <input required type=hidden name=contenttype value="{{ .ContentType.ID }}" />
        {{ range .ContentType.Fields }}
          <input required type=text name=name placeholder="{{ .Name }}" />
        {{ end }}
        <input type=submit value=Go />
      </form>

      <p>Browse Content:</p>
      {{ if .Contents }}
        TODO
      {{ else }}
        <p>No content has been created with a content type of 
        {{ .ContentType.Name }}</p>
      {{ end }}

    </article>
    <hr/>
    <footer>
      <center>© 2015-2020 Evan Jones</center>
    </footer>
  </main>
</body>

</html>

M internal/s/tmpl/html/index.html => internal/s/tmpl/html/index.html +1 -0
@@ 9,6 9,7 @@

<body>
  <style>
    main { max-width: 600px; margin: 0 auto; }
    form input { display: block; margin: 10px 0; }
  </style>


M internal/s/tmpl/html/space.html => internal/s/tmpl/html/space.html +26 -26
@@ 9,6 9,7 @@

<body>
  <style>
    main { max-width: 600px; margin: 0 auto; }
    form input, form button { display: block; margin: 5px 0; }
    form div input, form div button { display: inline-block; }
    form > input { margin-top: 10px; }


@@ 26,20 27,23 @@
        <a href='/'>Back</a>
      </nav>

      <h1>Space: {{ .Space.Name }}</h1>
      <h2>{{ .Space.Name }}</h2>

      <form method=POST action='/contenttype/new' enctype='multipart/form-data'>
        <legend>Create Content Type:</legend>
        <input required type=hidden name=space value="{{ .Space.ID }}" />
        <input required type=text name=name placeholder="content type name" />
        <div id='first-fieldset'>
          <input required type=text name="field_name_1" placeholder="field name" />
          <input required type=text name="field_name_1" placeholder="name field (for searchability)" />
          <select required name="field_type_1">
            <option disabled selected value>Field Type</option>
            <option>String Small</option>
            <option>String Big</option>
            <option>File</option>
            <option>Reference</option>
            <option disabled value>Field Type</option>
            <option selected value="StringSmall">String Small</option>
            <option disabled value="StringBig">String Big</option>
            <option disabled value="InputHTML">HTML</option>
            <option disabled value="InputMarkdown">Markdown</option>
            <option disabled value="File">File</option>
            <option disabled value="Date">Date</option>
            <option disabled value="Reference">Reference</option>
          </select>
        </div>
        <button id='add-fieldbtn'>Add Another Field</button>


@@ 47,29 51,22 @@
      </form>

      {{ if .ContentTypes }}
        <form method=POST action='/content/new' enctype='multipart/form-data'>
          <legend>Create Content</legend>
          <input required type=hidden name=space value="{{ .Space.ID }}" />
          <select required name="contenttype">
            <option disabled selected value>Content Type</option>
            {{ range .ContentTypes }}
              <option>{{ .Name }}</option>
            {{ end }}
          </select>
          <input type=submit value=Go />
        </form>
        <p>Create Content:</p>
        <p>TODO</p>
      {{ else }}
        <p>Create Content:</p>
        <p>You haven't created any content types yet. To begin creating content
        you must have at least one content type.</p>
      {{ end }}


      <p>Browse Content By Type:</p>
      {{ if .ContentTypes }}
        TODO
        <ul>
          {{ range .ContentTypes }}
            <li><a href='/contenttype/{{ $.Space.ID }}/{{ .ID }}'>{{ .Name }}</a></li>
          {{ end }}
        </ul>
      {{ else }}
        <p>You haven't created any content types yet. To begin browsing content
        content must exist.</p>
      {{ end }}



@@ 91,14 88,17 @@
        var el = document.createElement('div')
        el.innerHTML = `
          <div>
            <input required type=text name="field_name_${i}" placeholder="field name" />
            <select required name="field_type_${i}">
              <option disabled selected value>Field Type</option>
              <option>String Small</option>
              <option>String Big</option>
              <option>File</option>
              <option>Reference</option>
              <option value="StringSmall">String Small</option>
              <option value="StringBig">String Big</option>
              <option value="InputHTML">HTML</option>
              <option value="InputMarkdown">Markdown</option>
              <option value="File">File</option>
              <option value="Date">Date</option>
              <option value="Reference">Reference</option>
            </select>
            <input required type=text name="field_name_${i}" placeholder="field name" />
            <button id='remove-fieldbtn_${i}'>Remove Field</button>
          </div>
        `

M internal/s/tmpl/tmpls_embed.go => internal/s/tmpl/tmpls_embed.go +87 -26
@@ 7,6 7,66 @@ var tmpls map[string]string
func init() {
	tmpls = make(map[string]string)

	tmpls["html/contenttype.html"] = `<!DOCTYPE html>
<html lang=en>

<head>
  <meta charset="utf-8">
  <title>CMS | {{ .Space.Name }}</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  <style>
    main { max-width: 600px; margin: 0 auto; }
    form input, form button { display: block; margin: 5px 0; }
    form div input, form div button { display: inline-block; }
    form > input { margin-top: 10px; }
  </style>

  <main>
    <header>
      <h1>CMS</h1>
      <p>A flexible CMS for everyone.</p>
    </header>
    <hr/>
    <article>

      <nav>
        <a href='/space/{{ .Space.ID }}'>Back</a>
      </nav>

      <h2>{{ .Space.Name }}: {{ .ContentType.Name }}</h2>

      <form method=POST action='/content/new' enctype='multipart/form-data'>
        <legend>Create Content:</legend>
        <input required type=hidden name=space value="{{ .Space.ID }}" />
        <input required type=hidden name=contenttype value="{{ .ContentType.ID }}" />
        {{ range .ContentType.Fields }}
          <input required type=text name=name placeholder="{{ .Name }}" />
        {{ end }}
        <input type=submit value=Go />
      </form>

      <p>Browse Content:</p>
      {{ if .Contents }}
        TODO
      {{ else }}
        <p>No content has been created with a content type of 
        {{ .ContentType.Name }}</p>
      {{ end }}

    </article>
    <hr/>
    <footer>
      <center>© 2015-2020 Evan Jones</center>
    </footer>
  </main>
</body>

</html>
`

	tmpls["html/index.html"] = `<!DOCTYPE html>
<html lang=en>



@@ 18,6 78,7 @@ func init() {

<body>
  <style>
    main { max-width: 600px; margin: 0 auto; }
    form input { display: block; margin: 10px 0; }
  </style>



@@ 99,6 160,7 @@ func init() {

<body>
  <style>
    main { max-width: 600px; margin: 0 auto; }
    form input, form button { display: block; margin: 5px 0; }
    form div input, form div button { display: inline-block; }
    form > input { margin-top: 10px; }


@@ 116,20 178,23 @@ func init() {
        <a href='/'>Back</a>
      </nav>

      <h1>Space: {{ .Space.Name }}</h1>
      <h2>{{ .Space.Name }}</h2>

      <form method=POST action='/contenttype/new' enctype='multipart/form-data'>
        <legend>Create Content Type:</legend>
        <input required type=hidden name=space value="{{ .Space.ID }}" />
        <input required type=text name=name placeholder="content type name" />
        <div id='first-fieldset'>
          <input required type=text name="field_name_1" placeholder="field name" />
          <input required type=text name="field_name_1" placeholder="name field (for searchability)" />
          <select required name="field_type_1">
            <option disabled selected value>Field Type</option>
            <option>String Small</option>
            <option>String Big</option>
            <option>File</option>
            <option>Reference</option>
            <option disabled value>Field Type</option>
            <option selected value="StringSmall">String Small</option>
            <option disabled value="StringBig">String Big</option>
            <option disabled value="InputHTML">HTML</option>
            <option disabled value="InputMarkdown">Markdown</option>
            <option disabled value="File">File</option>
            <option disabled value="Date">Date</option>
            <option disabled value="Reference">Reference</option>
          </select>
        </div>
        <button id='add-fieldbtn'>Add Another Field</button>


@@ 137,29 202,22 @@ func init() {
      </form>

      {{ if .ContentTypes }}
        <form method=POST action='/content/new' enctype='multipart/form-data'>
          <legend>Create Content</legend>
          <input required type=hidden name=space value="{{ .Space.ID }}" />
          <select required name="contenttype">
            <option disabled selected value>Content Type</option>
            {{ range .ContentTypes }}
              <option>{{ .Name }}</option>
            {{ end }}
          </select>
          <input type=submit value=Go />
        </form>
        <p>Create Content:</p>
        <p>TODO</p>
      {{ else }}
        <p>Create Content:</p>
        <p>You haven't created any content types yet. To begin creating content
        you must have at least one content type.</p>
      {{ end }}


      <p>Browse Content By Type:</p>
      {{ if .ContentTypes }}
        TODO
        <ul>
          {{ range .ContentTypes }}
            <li><a href='/contenttype/{{ $.Space.ID }}/{{ .ID }}'>{{ .Name }}</a></li>
          {{ end }}
        </ul>
      {{ else }}
        <p>You haven't created any content types yet. To begin browsing content
        content must exist.</p>
      {{ end }}



@@ 181,14 239,17 @@ func init() {
        var el = document.createElement('div')
        el.innerHTML = ` + "`" + `
          <div>
            <input required type=text name="field_name_${i}" placeholder="field name" />
            <select required name="field_type_${i}">
              <option disabled selected value>Field Type</option>
              <option>String Small</option>
              <option>String Big</option>
              <option>File</option>
              <option>Reference</option>
              <option value="StringSmall">String Small</option>
              <option value="StringBig">String Big</option>
              <option value="InputHTML">HTML</option>
              <option value="InputMarkdown">Markdown</option>
              <option value="File">File</option>
              <option value="Date">Date</option>
              <option value="Reference">Reference</option>
            </select>
            <input required type=text name="field_name_${i}" placeholder="field name" />
            <button id='remove-fieldbtn_${i}'>Remove Field</button>
          </div>
        ` + "`" + `

M makefile => makefile +1 -0
@@ 24,3 24,4 @@ dev-client:

dev-server:
	find . -type f -name '*.go' | entr -r sh -c "clear && make build && ./$(BIN)"


A vendor/github.com/bmizerany/assert/.gitignore => vendor/github.com/bmizerany/assert/.gitignore +7 -0
@@ 0,0 1,7 @@
_go_.*
_gotest_.*
_obj
_test
_testmain.go
*.out
*.[568]

A vendor/github.com/bmizerany/assert/README.md => vendor/github.com/bmizerany/assert/README.md +47 -0
@@ 0,0 1,47 @@
# NO LONGER MAINTAINED - Just use Go's testing package.

# Assert (c) Blake Mizerany and Keith Rarick -- MIT LICENCE

## Assertions for Go tests

## Install

    $ go get github.com/bmizerany/assert

## Use

**point.go**

    package point

    type Point struct {
        x, y int
    }

**point_test.go**


    package point

    import (
        "testing"
        "github.com/bmizerany/assert"
    )

    func TestAsserts(t *testing.T) {
        p1 := Point{1, 1}
        p2 := Point{2, 1}

        assert.Equal(t, p1, p2)
    }

**output**
    $ go test
     --- FAIL: TestAsserts (0.00 seconds)
	 assert.go:15: /Users/flavio.barbosa/dev/stewie/src/point_test.go:12
         assert.go:24: ! X: 1 != 2
	 FAIL

## Docs

    http://github.com/bmizerany/assert

A vendor/github.com/bmizerany/assert/assert.go => vendor/github.com/bmizerany/assert/assert.go +76 -0
@@ 0,0 1,76 @@
package assert
// Testing helpers for doozer.

import (
	"github.com/kr/pretty"
	"reflect"
	"testing"
	"runtime"
	"fmt"
)

func assert(t *testing.T, result bool, f func(), cd int) {
	if !result {
		_, file, line, _ := runtime.Caller(cd + 1)
		t.Errorf("%s:%d", file, line)
		f()
		t.FailNow()
	}
}

func equal(t *testing.T, exp, got interface{}, cd int, args ...interface{}) {
	fn := func() {
		for _, desc := range pretty.Diff(exp, got) {
			t.Error("!", desc)
		}
		if len(args) > 0 {
			t.Error("!", " -", fmt.Sprint(args...))
		}
	}
	result := reflect.DeepEqual(exp, got)
	assert(t, result, fn, cd+1)
}

func tt(t *testing.T, result bool, cd int, args ...interface{}) {
	fn := func() {
		t.Errorf("!  Failure")
		if len(args) > 0 {
			t.Error("!", " -", fmt.Sprint(args...))
		}
	}
	assert(t, result, fn, cd+1)
}

func T(t *testing.T, result bool, args ...interface{}) {
	tt(t, result, 1, args...)
}

func Tf(t *testing.T, result bool, format string, args ...interface{}) {
	tt(t, result, 1, fmt.Sprintf(format, args...))
}

func Equal(t *testing.T, exp, got interface{}, args ...interface{}) {
	equal(t, exp, got, 1, args...)
}

func Equalf(t *testing.T, exp, got interface{}, format string, args ...interface{}) {
	equal(t, exp, got, 1, fmt.Sprintf(format, args...))
}

func NotEqual(t *testing.T, exp, got interface{}, args ...interface{}) {
	fn := func() {
		t.Errorf("!  Unexpected: <%#v>", exp)
		if len(args) > 0 {
			t.Error("!", " -", fmt.Sprint(args...))
		}
	}
	result := !reflect.DeepEqual(exp, got)
	assert(t, result, fn, 1)
}

func Panic(t *testing.T, err interface{}, fn func()) {
	defer func() {
		equal(t, err, recover(), 3)
	}()
	fn()
}

A vendor/github.com/kr/pretty/.gitignore => vendor/github.com/kr/pretty/.gitignore +4 -0
@@ 0,0 1,4 @@
[568].out
_go*
_test*
_obj

A vendor/github.com/kr/pretty/License => vendor/github.com/kr/pretty/License +19 -0
@@ 0,0 1,19 @@
Copyright 2012 Keith Rarick

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

A vendor/github.com/kr/pretty/Readme => vendor/github.com/kr/pretty/Readme +9 -0
@@ 0,0 1,9 @@
package pretty

    import "github.com/kr/pretty"

    Package pretty provides pretty-printing for Go values.

Documentation

    http://godoc.org/github.com/kr/pretty

A vendor/github.com/kr/pretty/diff.go => vendor/github.com/kr/pretty/diff.go +265 -0
@@ 0,0 1,265 @@
package pretty

import (
	"fmt"
	"io"
	"reflect"
)

type sbuf []string

func (p *sbuf) Printf(format string, a ...interface{}) {
	s := fmt.Sprintf(format, a...)
	*p = append(*p, s)
}

// Diff returns a slice where each element describes
// a difference between a and b.
func Diff(a, b interface{}) (desc []string) {
	Pdiff((*sbuf)(&desc), a, b)
	return desc
}

// wprintfer calls Fprintf on w for each Printf call
// with a trailing newline.
type wprintfer struct{ w io.Writer }

func (p *wprintfer) Printf(format string, a ...interface{}) {
	fmt.Fprintf(p.w, format+"\n", a...)
}

// Fdiff writes to w a description of the differences between a and b.
func Fdiff(w io.Writer, a, b interface{}) {
	Pdiff(&wprintfer{w}, a, b)
}

type Printfer interface {
	Printf(format string, a ...interface{})
}

// Pdiff prints to p a description of the differences between a and b.
// It calls Printf once for each difference, with no trailing newline.
// The standard library log.Logger is a Printfer.
func Pdiff(p Printfer, a, b interface{}) {
	diffPrinter{w: p}.diff(reflect.ValueOf(a), reflect.ValueOf(b))
}

type Logfer interface {
	Logf(format string, a ...interface{})
}

// logprintfer calls Fprintf on w for each Printf call
// with a trailing newline.
type logprintfer struct{ l Logfer }

func (p *logprintfer) Printf(format string, a ...interface{}) {
	p.l.Logf(format, a...)
}

// Ldiff prints to l a description of the differences between a and b.
// It calls Logf once for each difference, with no trailing newline.
// The standard library testing.T and testing.B are Logfers.
func Ldiff(l Logfer, a, b interface{}) {
	Pdiff(&logprintfer{l}, a, b)
}

type diffPrinter struct {
	w Printfer
	l string // label
}

func (w diffPrinter) printf(f string, a ...interface{}) {
	var l string
	if w.l != "" {
		l = w.l + ": "
	}
	w.w.Printf(l+f, a...)
}

func (w diffPrinter) diff(av, bv reflect.Value) {
	if !av.IsValid() && bv.IsValid() {
		w.printf("nil != %# v", formatter{v: bv, quote: true})
		return
	}
	if av.IsValid() && !bv.IsValid() {
		w.printf("%# v != nil", formatter{v: av, quote: true})
		return
	}
	if !av.IsValid() && !bv.IsValid() {
		return
	}

	at := av.Type()
	bt := bv.Type()
	if at != bt {
		w.printf("%v != %v", at, bt)
		return
	}

	switch kind := at.Kind(); kind {
	case reflect.Bool:
		if a, b := av.Bool(), bv.Bool(); a != b {
			w.printf("%v != %v", a, b)
		}
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		if a, b := av.Int(), bv.Int(); a != b {
			w.printf("%d != %d", a, b)
		}
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		if a, b := av.Uint(), bv.Uint(); a != b {
			w.printf("%d != %d", a, b)
		}
	case reflect.Float32, reflect.Float64:
		if a, b := av.Float(), bv.Float(); a != b {
			w.printf("%v != %v", a, b)
		}
	case reflect.Complex64, reflect.Complex128:
		if a, b := av.Complex(), bv.Complex(); a != b {
			w.printf("%v != %v", a, b)
		}
	case reflect.Array:
		n := av.Len()
		for i := 0; i < n; i++ {
			w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i))
		}
	case reflect.Chan, reflect.Func, reflect.UnsafePointer:
		if a, b := av.Pointer(), bv.Pointer(); a != b {
			w.printf("%#x != %#x", a, b)
		}
	case reflect.Interface:
		w.diff(av.Elem(), bv.Elem())
	case reflect.Map:
		ak, both, bk := keyDiff(av.MapKeys(), bv.MapKeys())
		for _, k := range ak {
			w := w.relabel(fmt.Sprintf("[%#v]", k))
			w.printf("%q != (missing)", av.MapIndex(k))
		}
		for _, k := range both {
			w := w.relabel(fmt.Sprintf("[%#v]", k))
			w.diff(av.MapIndex(k), bv.MapIndex(k))
		}
		for _, k := range bk {
			w := w.relabel(fmt.Sprintf("[%#v]", k))
			w.printf("(missing) != %q", bv.MapIndex(k))
		}
	case reflect.Ptr:
		switch {
		case av.IsNil() && !bv.IsNil():
			w.printf("nil != %# v", formatter{v: bv, quote: true})
		case !av.IsNil() && bv.IsNil():
			w.printf("%# v != nil", formatter{v: av, quote: true})
		case !av.IsNil() && !bv.IsNil():
			w.diff(av.Elem(), bv.Elem())
		}
	case reflect.Slice:
		lenA := av.Len()
		lenB := bv.Len()
		if lenA != lenB {
			w.printf("%s[%d] != %s[%d]", av.Type(), lenA, bv.Type(), lenB)
			break
		}
		for i := 0; i < lenA; i++ {
			w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i))
		}
	case reflect.String:
		if a, b := av.String(), bv.String(); a != b {
			w.printf("%q != %q", a, b)
		}
	case reflect.Struct:
		for i := 0; i < av.NumField(); i++ {
			w.relabel(at.Field(i).Name).diff(av.Field(i), bv.Field(i))
		}
	default:
		panic("unknown reflect Kind: " + kind.String())
	}
}

func (d diffPrinter) relabel(name string) (d1 diffPrinter) {
	d1 = d
	if d.l != "" && name[0] != '[' {
		d1.l += "."
	}
	d1.l += name
	return d1
}

// keyEqual compares a and b for equality.
// Both a and b must be valid map keys.
func keyEqual(av, bv reflect.Value) bool {
	if !av.IsValid() && !bv.IsValid() {
		return true
	}
	if !av.IsValid() || !bv.IsValid() || av.Type() != bv.Type() {
		return false
	}
	switch kind := av.Kind(); kind {
	case reflect.Bool:
		a, b := av.Bool(), bv.Bool()
		return a == b
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		a, b := av.Int(), bv.Int()
		return a == b
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		a, b := av.Uint(), bv.Uint()
		return a == b
	case reflect.Float32, reflect.Float64:
		a, b := av.Float(), bv.Float()
		return a == b
	case reflect.Complex64, reflect.Complex128:
		a, b := av.Complex(), bv.Complex()
		return a == b
	case reflect.Array:
		for i := 0; i < av.Len(); i++ {
			if !keyEqual(av.Index(i), bv.Index(i)) {
				return false
			}
		}
		return true
	case reflect.Chan, reflect.UnsafePointer, reflect.Ptr:
		a, b := av.Pointer(), bv.Pointer()
		return a == b
	case reflect.Interface:
		return keyEqual(av.Elem(), bv.Elem())
	case reflect.String:
		a, b := av.String(), bv.String()
		return a == b
	case reflect.Struct:
		for i := 0; i < av.NumField(); i++ {
			if !keyEqual(av.Field(i), bv.Field(i)) {
				return false
			}
		}
		return true
	default:
		panic("invalid map key type " + av.Type().String())
	}
}

func keyDiff(a, b []reflect.Value) (ak, both, bk []reflect.Value) {
	for _, av := range a {
		inBoth := false
		for _, bv := range b {
			if keyEqual(av, bv) {
				inBoth = true
				both = append(both, av)
				break
			}
		}
		if !inBoth {
			ak = append(ak, av)
		}
	}
	for _, bv := range b {
		inBoth := false
		for _, av := range a {
			if keyEqual(av, bv) {
				inBoth = true
				break
			}
		}
		if !inBoth {
			bk = append(bk, bv)
		}
	}
	return
}

A vendor/github.com/kr/pretty/formatter.go => vendor/github.com/kr/pretty/formatter.go +327 -0
@@ 0,0 1,327 @@
package pretty

import (
	"fmt"
	"io"
	"reflect"
	"strconv"
	"text/tabwriter"

	"github.com/kr/text"
)

type formatter struct {
	v     reflect.Value
	force bool
	quote bool
}

// Formatter makes a wrapper, f, that will format x as go source with line
// breaks and tabs. Object f responds to the "%v" formatting verb when both the
// "#" and " " (space) flags are set, for example:
//
//     fmt.Sprintf("%# v", Formatter(x))
//
// If one of these two flags is not set, or any other verb is used, f will
// format x according to the usual rules of package fmt.
// In particular, if x satisfies fmt.Formatter, then x.Format will be called.
func Formatter(x interface{}) (f fmt.Formatter) {
	return formatter{v: reflect.ValueOf(x), quote: true}
}

func (fo formatter) String() string {
	return fmt.Sprint(fo.v.Interface()) // unwrap it
}

func (fo formatter) passThrough(f fmt.State, c rune) {
	s := "%"
	for i := 0; i < 128; i++ {
		if f.Flag(i) {
			s += string(i)
		}
	}
	if w, ok := f.Width(); ok {
		s += fmt.Sprintf("%d", w)
	}
	if p, ok := f.Precision(); ok {
		s += fmt.Sprintf(".%d", p)
	}
	s += string(c)
	fmt.Fprintf(f, s, fo.v.Interface())
}

func (fo formatter) Format(f fmt.State, c rune) {
	if fo.force || c == 'v' && f.Flag('#') && f.Flag(' ') {
		w := tabwriter.NewWriter(f, 4, 4, 1, ' ', 0)
		p := &printer{tw: w, Writer: w, visited: make(map[visit]int)}
		p.printValue(fo.v, true, fo.quote)
		w.Flush()
		return
	}
	fo.passThrough(f, c)
}

type printer struct {
	io.Writer
	tw      *tabwriter.Writer
	visited map[visit]int
	depth   int
}

func (p *printer) indent() *printer {
	q := *p
	q.tw = tabwriter.NewWriter(p.Writer, 4, 4, 1, ' ', 0)
	q.Writer = text.NewIndentWriter(q.tw, []byte{'\t'})
	return &q
}

func (p *printer) printInline(v reflect.Value, x interface{}, showType bool) {
	if showType {
		io.WriteString(p, v.Type().String())
		fmt.Fprintf(p, "(%#v)", x)
	} else {
		fmt.Fprintf(p, "%#v", x)
	}
}

// printValue must keep track of already-printed pointer values to avoid
// infinite recursion.
type visit struct {
	v   uintptr
	typ reflect.Type
}

func (p *printer) printValue(v reflect.Value, showType, quote bool) {
	if p.depth > 10 {
		io.WriteString(p, "!%v(DEPTH EXCEEDED)")
		return
	}

	switch v.Kind() {
	case reflect.Bool:
		p.printInline(v, v.Bool(), showType)
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		p.printInline(v, v.Int(), showType)
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		p.printInline(v, v.Uint(), showType)
	case reflect.Float32, reflect.Float64:
		p.printInline(v, v.Float(), showType)
	case reflect.Complex64, reflect.Complex128:
		fmt.Fprintf(p, "%#v", v.Complex())
	case reflect.String:
		p.fmtString(v.String(), quote)
	case reflect.Map:
		t := v.Type()
		if showType {
			io.WriteString(p, t.String())
		}
		writeByte(p, '{')
		if nonzero(v) {
			expand := !canInline(v.Type())
			pp := p
			if expand {
				writeByte(p, '\n')
				pp = p.indent()
			}
			keys := v.MapKeys()
			for i := 0; i < v.Len(); i++ {
				k := keys[i]
				mv := v.MapIndex(k)
				pp.printValue(k, false, true)
				writeByte(pp, ':')
				if expand {
					writeByte(pp, '\t')
				}
				showTypeInStruct := t.Elem().Kind() == reflect.Interface
				pp.printValue(mv, showTypeInStruct, true)
				if expand {
					io.WriteString(pp, ",\n")
				} else if i < v.Len()-1 {
					io.WriteString(pp, ", ")
				}
			}
			if expand {
				pp.tw.Flush()
			}
		}
		writeByte(p, '}')
	case reflect.Struct:
		t := v.Type()
		if v.CanAddr() {
			addr := v.UnsafeAddr()
			vis := visit{addr, t}
			if vd, ok := p.visited[vis]; ok && vd < p.depth {
				p.fmtString(t.String()+"{(CYCLIC REFERENCE)}", false)
				break // don't print v again
			}
			p.visited[vis] = p.depth
		}

		if showType {
			io.WriteString(p, t.String())
		}
		writeByte(p, '{')
		if nonzero(v) {
			expand := !canInline(v.Type())
			pp := p
			if expand {
				writeByte(p, '\n')
				pp = p.indent()
			}
			for i := 0; i < v.NumField(); i++ {
				showTypeInStruct := true
				if f := t.Field(i); f.Name != "" {
					io.WriteString(pp, f.Name)
					writeByte(pp, ':')
					if expand {
						writeByte(pp, '\t')
					}
					showTypeInStruct = labelType(f.Type)
				}
				pp.printValue(getField(v, i), showTypeInStruct, true)
				if expand {
					io.WriteString(pp, ",\n")
				} else if i < v.NumField()-1 {
					io.WriteString(pp, ", ")
				}
			}
			if expand {
				pp.tw.Flush()
			}
		}
		writeByte(p, '}')
	case reflect.Interface:
		switch e := v.Elem(); {
		case e.Kind() == reflect.Invalid:
			io.WriteString(p, "nil")
		case e.IsValid():
			pp := *p
			pp.depth++
			pp.printValue(e, showType, true)
		default:
			io.WriteString(p, v.Type().String())
			io.WriteString(p, "(nil)")
		}
	case reflect.Array, reflect.Slice:
		t := v.Type()
		if showType {
			io.WriteString(p, t.String())
		}
		if v.Kind() == reflect.Slice && v.IsNil() && showType {
			io.WriteString(p, "(nil)")
			break
		}
		if v.Kind() == reflect.Slice && v.IsNil() {
			io.WriteString(p, "nil")
			break
		}
		writeByte(p, '{')
		expand := !canInline(v.Type())
		pp := p
		if expand {
			writeByte(p, '\n')
			pp = p.indent()
		}
		for i := 0; i < v.Len(); i++ {
			showTypeInSlice := t.Elem().Kind() == reflect.Interface
			pp.printValue(v.Index(i), showTypeInSlice, true)
			if expand {
				io.WriteString(pp, ",\n")
			} else if i < v.Len()-1 {
				io.WriteString(pp, ", ")
			}
		}
		if expand {
			pp.tw.Flush()
		}
		writeByte(p, '}')
	case reflect.Ptr:
		e := v.Elem()
		if !e.IsValid() {
			writeByte(p, '(')
			io.WriteString(p, v.Type().String())
			io.WriteString(p, ")(nil)")
		} else {
			pp := *p
			pp.depth++
			writeByte(pp, '&')
			pp.printValue(e, true, true)
		}
	case reflect.Chan:
		x := v.Pointer()
		if showType {
			writeByte(p, '(')
			io.WriteString(p, v.Type().String())
			fmt.Fprintf(p, ")(%#v)", x)
		} else {
			fmt.Fprintf(p, "%#v", x)
		}
	case reflect.Func:
		io.WriteString(p, v.Type().String())
		io.WriteString(p, " {...}")
	case reflect.UnsafePointer:
		p.printInline(v, v.Pointer(), showType)
	case reflect.Invalid:
		io.WriteString(p, "nil")
	}
}

func canInline(t reflect.Type) bool {
	switch t.Kind() {
	case reflect.Map:
		return !canExpand(t.Elem())
	case reflect.Struct:
		for i := 0; i < t.NumField(); i++ {
			if canExpand(t.Field(i).Type) {
				return false
			}
		}
		return true
	case reflect.Interface:
		return false
	case reflect.Array, reflect.Slice:
		return !canExpand(t.Elem())
	case reflect.Ptr:
		return false
	case reflect.Chan, reflect.Func, reflect.UnsafePointer:
		return false
	}
	return true
}

func canExpand(t reflect.Type) bool {
	switch t.Kind() {
	case reflect.Map, reflect.Struct,
		reflect.Interface, reflect.Array, reflect.Slice,
		reflect.Ptr:
		return true
	}
	return false
}

func labelType(t reflect.Type) bool {
	switch t.Kind() {
	case reflect.Interface, reflect.Struct:
		return true
	}
	return false
}

func (p *printer) fmtString(s string, quote bool) {
	if quote {
		s = strconv.Quote(s)
	}
	io.WriteString(p, s)
}

func writeByte(w io.Writer, b byte) {
	w.Write([]byte{b})
}

func getField(v reflect.Value, i int) reflect.Value {
	val := v.Field(i)
	if val.Kind() == reflect.Interface && !val.IsNil() {
		val = val.Elem()
	}
	return val
}

A vendor/github.com/kr/pretty/go.mod => vendor/github.com/kr/pretty/go.mod +5 -0
@@ 0,0 1,5 @@
module github.com/kr/pretty

go 1.12

require github.com/kr/text v0.1.0

A vendor/github.com/kr/pretty/go.sum => vendor/github.com/kr/pretty/go.sum +3 -0
@@ 0,0 1,3 @@
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=

A vendor/github.com/kr/pretty/pretty.go => vendor/github.com/kr/pretty/pretty.go +108 -0
@@ 0,0 1,108 @@
// Package pretty provides pretty-printing for Go values. This is
// useful during debugging, to avoid wrapping long output lines in
// the terminal.
//
// It provides a function, Formatter, that can be used with any
// function that accepts a format string. It also provides
// convenience wrappers for functions in packages fmt and log.
package pretty

import (
	"fmt"
	"io"
	"log"
	"reflect"
)

// Errorf is a convenience wrapper for fmt.Errorf.
//
// Calling Errorf(f, x, y) is equivalent to
// fmt.Errorf(f, Formatter(x), Formatter(y)).
func Errorf(format string, a ...interface{}) error {
	return fmt.Errorf(format, wrap(a, false)...)
}

// Fprintf is a convenience wrapper for fmt.Fprintf.
//
// Calling Fprintf(w, f, x, y) is equivalent to
// fmt.Fprintf(w, f, Formatter(x), Formatter(y)).
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, error error) {
	return fmt.Fprintf(w, format, wrap(a, false)...)
}

// Log is a convenience wrapper for log.Printf.
//
// Calling Log(x, y) is equivalent to
// log.Print(Formatter(x), Formatter(y)), but each operand is
// formatted with "%# v".
func Log(a ...interface{}) {
	log.Print(wrap(a, true)...)
}

// Logf is a convenience wrapper for log.Printf.
//
// Calling Logf(f, x, y) is equivalent to
// log.Printf(f, Formatter(x), Formatter(y)).
func Logf(format string, a ...interface{}) {
	log.Printf(format, wrap(a, false)...)
}

// Logln is a convenience wrapper for log.Printf.
//
// Calling Logln(x, y) is equivalent to
// log.Println(Formatter(x), Formatter(y)), but each operand is
// formatted with "%# v".
func Logln(a ...interface{}) {
	log.Println(wrap(a, true)...)
}

// Print pretty-prints its operands and writes to standard output.
//
// Calling Print(x, y) is equivalent to
// fmt.Print(Formatter(x), Formatter(y)), but each operand is
// formatted with "%# v".
func Print(a ...interface{}) (n int, errno error) {
	return fmt.Print(wrap(a, true)...)
}

// Printf is a convenience wrapper for fmt.Printf.
//
// Calling Printf(f, x, y) is equivalent to
// fmt.Printf(f, Formatter(x), Formatter(y)).
func Printf(format string, a ...interface{}) (n int, errno error) {
	return fmt.Printf(format, wrap(a, false)...)
}

// Println pretty-prints its operands and writes to standard output.
//
// Calling Println(x, y) is equivalent to
// fmt.Println(Formatter(x), Formatter(y)), but each operand is
// formatted with "%# v".
func Println(a ...interface{}) (n int, errno error) {
	return fmt.Println(wrap(a, true)...)
}

// Sprint is a convenience wrapper for fmt.Sprintf.
//
// Calling Sprint(x, y) is equivalent to
// fmt.Sprint(Formatter(x), Formatter(y)), but each operand is
// formatted with "%# v".
func Sprint(a ...interface{}) string {
	return fmt.Sprint(wrap(a, true)...)
}

// Sprintf is a convenience wrapper for fmt.Sprintf.
//
// Calling Sprintf(f, x, y) is equivalent to
// fmt.Sprintf(f, Formatter(x), Formatter(y)).
func Sprintf(format string, a ...interface{}) string {
	return fmt.Sprintf(format, wrap(a, false)...)
}

func wrap(a []interface{}, force bool) []interface{} {
	w := make([]interface{}, len(a))
	for i, x := range a {
		w[i] = formatter{v: reflect.ValueOf(x), force: force}
	}
	return w
}

A vendor/github.com/kr/pretty/zero.go => vendor/github.com/kr/pretty/zero.go +41 -0
@@ 0,0 1,41 @@
package pretty

import (
	"reflect"
)

func nonzero(v reflect.Value) bool {
	switch v.Kind() {
	case reflect.Bool:
		return v.Bool()
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return v.Int() != 0
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return v.Uint() != 0
	case reflect.Float32, reflect.Float64:
		return v.Float() != 0
	case reflect.Complex64, reflect.Complex128:
		return v.Complex() != complex(0, 0)
	case reflect.String:
		return v.String() != ""
	case reflect.Struct:
		for i := 0; i < v.NumField(); i++ {
			if nonzero(getField(v, i)) {
				return true
			}
		}
		return false
	case reflect.Array:
		for i := 0; i < v.Len(); i++ {
			if nonzero(v.Index(i)) {
				return true
			}
		}
		return false
	case reflect.Map, reflect.Interface, reflect.Slice, reflect.Ptr, reflect.Chan, reflect.Func:
		return !v.IsNil()
	case reflect.UnsafePointer:
		return v.Pointer() != 0
	}
	return true
}

A vendor/github.com/kr/text/License => vendor/github.com/kr/text/License +19 -0
@@ 0,0 1,19 @@
Copyright 2012 Keith Rarick

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

A vendor/github.com/kr/text/Readme => vendor/github.com/kr/text/Readme +3 -0
@@ 0,0 1,3 @@
This is a Go package for manipulating paragraphs of text.

See http://go.pkgdoc.org/github.com/kr/text for full documentation.

A vendor/github.com/kr/text/doc.go => vendor/github.com/kr/text/doc.go +3 -0
@@ 0,0 1,3 @@
// Package text provides rudimentary functions for manipulating text in
// paragraphs.
package text

A vendor/github.com/kr/text/go.mod => vendor/github.com/kr/text/go.mod +3 -0
@@ 0,0 1,3 @@
module "github.com/kr/text"

require "github.com/kr/pty" v1.1.1

A vendor/github.com/kr/text/indent.go => vendor/github.com/kr/text/indent.go +74 -0
@@ 0,0 1,74 @@
package text

import (
	"io"
)

// Indent inserts prefix at the beginning of each non-empty line of s. The
// end-of-line marker is NL.
func Indent(s, prefix string) string {
	return string(IndentBytes([]byte(s), []byte(prefix)))
}

// IndentBytes inserts prefix at the beginning of each non-empty line of b.
// The end-of-line marker is NL.
func IndentBytes(b, prefix []byte) []byte {
	var res []byte
	bol := true
	for _, c := range b {
		if bol && c != '\n' {
			res = append(res, prefix...)
		}
		res = append(res, c)
		bol = c == '\n'
	}
	return res
}

// Writer indents each line of its input.
type indentWriter struct {
	w   io.Writer
	bol bool
	pre [][]byte
	sel int
	off int
}

// NewIndentWriter makes a new write filter that indents the input
// lines. Each line is prefixed in order with the corresponding
// element of pre. If there are more lines than elements, the last
// element of pre is repeated for each subsequent line.
func NewIndentWriter(w io.Writer, pre ...[]byte) io.Writer {
	return &indentWriter{
		w:   w,
		pre: pre,
		bol: true,
	}
}

// The only errors returned are from the underlying indentWriter.
func (w *indentWriter) Write(p []byte) (n int, err error) {
	for _, c := range p {
		if w.bol {
			var i int
			i, err = w.w.Write(w.pre[w.sel][w.off:])
			w.off += i
			if err != nil {
				return n, err
			}
		}
		_, err = w.w.Write([]byte{c})
		if err != nil {
			return n, err
		}
		n++
		w.bol = c == '\n'
		if w.bol {
			w.off = 0
			if w.sel < len(w.pre)-1 {
				w.sel++
			}
		}
	}
	return n, nil
}

A vendor/github.com/kr/text/wrap.go => vendor/github.com/kr/text/wrap.go +86 -0
@@ 0,0 1,86 @@
package text

import (
	"bytes"
	"math"
)

var (
	nl = []byte{'\n'}
	sp = []byte{' '}
)

const defaultPenalty = 1e5

// Wrap wraps s into a paragraph of lines of length lim, with minimal
// raggedness.
func Wrap(s string, lim int) string {
	return string(WrapBytes([]byte(s), lim))
}

// WrapBytes wraps b into a paragraph of lines of length lim, with minimal
// raggedness.
func WrapBytes(b []byte, lim int) []byte {
	words := bytes.Split(bytes.Replace(bytes.TrimSpace(b), nl, sp, -1), sp)
	var lines [][]byte
	for _, line := range WrapWords(words, 1, lim, defaultPenalty) {
		lines = append(lines, bytes.Join(line, sp))
	}
	return bytes.Join(lines, nl)
}

// WrapWords is the low-level line-breaking algorithm, useful if you need more
// control over the details of the text wrapping process. For most uses, either
// Wrap or WrapBytes will be sufficient and more convenient.
//
// WrapWords splits a list of words into lines with minimal "raggedness",
// treating each byte as one unit, accounting for spc units between adjacent
// words on each line, and attempting to limit lines to lim units. Raggedness
// is the total error over all lines, where error is the square of the
// difference of the length of the line and lim. Too-long lines (which only
// happen when a single word is longer than lim units) have pen penalty units
// added to the error.
func WrapWords(words [][]byte, spc, lim, pen int) [][][]byte {
	n := len(words)

	length := make([][]int, n)
	for i := 0; i < n; i++ {
		length[i] = make([]int, n)
		length[i][i] = len(words[i])
		for j := i + 1; j < n; j++ {
			length[i][j] = length[i][j-1] + spc + len(words[j])
		}
	}

	nbrk := make([]int, n)
	cost := make([]int, n)
	for i := range cost {
		cost[i] = math.MaxInt32
	}
	for i := n - 1; i >= 0; i-- {
		if length[i][n-1] <= lim || i == n-1 {
			cost[i] = 0
			nbrk[i] = n
		} else {
			for j := i + 1; j < n; j++ {
				d := lim - length[i][j-1]
				c := d*d + cost[j]
				if length[i][j-1] > lim {
					c += pen // too-long lines get a worse penalty
				}
				if c < cost[i] {
					cost[i] = c
					nbrk[i] = j
				}
			}
		}
	}

	var lines [][][]byte
	i := 0
	for i < n {
		lines = append(lines, words[i:nbrk[i]])
		i = nbrk[i]
	}
	return lines
}

M vendor/modules.txt => vendor/modules.txt +6 -0
@@ 1,7 1,13 @@
# git.sr.ht/~evanj/security v0.0.0-20200228044358-9b9bc6682997
git.sr.ht/~evanj/security
# github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
github.com/bmizerany/assert
# github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dgrijalva/jwt-go
# github.com/kr/pretty v0.2.0
github.com/kr/pretty
# github.com/kr/text v0.1.0
github.com/kr/text
# github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/mattn/go-sqlite3
# golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d