Add API handlers for snap
This commit is contained in:
parent
a0dbb354d6
commit
15a8fed766
101
context.go
101
context.go
|
@ -1,12 +1,11 @@
|
||||||
package snap
|
package snap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.twelvetwelve.org/library/snap/auth"
|
"git.twelvetwelve.org/library/snap/auth"
|
||||||
)
|
)
|
||||||
|
@ -17,7 +16,8 @@ type Context struct {
|
||||||
|
|
||||||
w http.ResponseWriter
|
w http.ResponseWriter
|
||||||
r *http.Request
|
r *http.Request
|
||||||
vars map[string]string
|
vars *Vars
|
||||||
|
form *Form
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnapContent struct {
|
type SnapContent struct {
|
||||||
|
@ -69,11 +69,21 @@ func (c *Context) ReplyWithHeaders(msg string, kv map[string]string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) ReplyObject(obj interface{}) {
|
func (c *Context) ReplyObject(obj interface{}) {
|
||||||
|
compact := c.r.Header.Get("Compact")
|
||||||
msg, err := json.Marshal(obj)
|
msg, err := json.Marshal(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(http.StatusInternalServerError, "Internal Server Error")
|
c.Error(http.StatusInternalServerError, "Internal Server Error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if compact == "" {
|
||||||
|
var pretty bytes.Buffer
|
||||||
|
err := json.Indent(&pretty, msg, "", " ")
|
||||||
|
if err == nil {
|
||||||
|
msg = pretty.Bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.Reply(string(msg))
|
c.Reply(string(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,57 +143,24 @@ func (c *Context) RenderWithMeta(tmpl string, meta map[string]string, content in
|
||||||
c.srv.render(c.w, tmpl, &cnt)
|
c.srv.render(c.w, tmpl, &cnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) GetVar(k string) (string, bool) {
|
func (c *Context) Form() (*Options, error) {
|
||||||
v, ok := c.vars[k]
|
if c.form != nil {
|
||||||
return v, ok
|
return &Options{
|
||||||
|
kv: c.form,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) GetVarDefault(k string, def string) string {
|
err := c.r.ParseForm()
|
||||||
v, ok := c.vars[k]
|
|
||||||
if !ok {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) GetVarAsSlice(k string, delim string, def []string) []string {
|
|
||||||
v, ok := c.vars[k]
|
|
||||||
if !ok {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
r := strings.Split(v, delim)
|
|
||||||
if len(r) == 1 && r[0] == "" {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) ParseForm() error {
|
|
||||||
return c.r.ParseForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) FormValue(k string) string {
|
|
||||||
return c.r.FormValue(k)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) FormValueUint64(k string) uint64 {
|
|
||||||
str := c.r.FormValue(k)
|
|
||||||
|
|
||||||
val, err := strconv.ParseUint(str, 10, 64)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return nil, err
|
||||||
|
}
|
||||||
|
c.form = &Form{
|
||||||
|
r: c.r,
|
||||||
}
|
}
|
||||||
|
|
||||||
return val
|
return &Options{
|
||||||
}
|
kv: c.form,
|
||||||
|
}, nil
|
||||||
func (c *Context) FormValueAsSlice(k string, delim string) []string {
|
|
||||||
v := c.r.FormValue(k)
|
|
||||||
r := strings.Split(v, delim)
|
|
||||||
if len(r) == 1 && r[0] == "" {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) Redirect(url string) {
|
func (c *Context) Redirect(url string) {
|
||||||
|
@ -206,3 +183,29 @@ func (c *Context) QueryValueWithDefault(key string, def string) string {
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) ReadObject(object interface{}) error {
|
||||||
|
defer c.r.Body.Close()
|
||||||
|
decoder := json.NewDecoder(c.r.Body)
|
||||||
|
return decoder.Decode(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) ParseVars() *Options {
|
||||||
|
if c.form != nil {
|
||||||
|
return &Options{
|
||||||
|
kv: c.form,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.form = &Form{
|
||||||
|
r: c.r,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Options{
|
||||||
|
kv: c.form,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -3,12 +3,12 @@ module git.twelvetwelve.org/library/snap
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.twelvetwelve.org/library/core v0.0.0-20230519041221-8f2da7be661d
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.twelvetwelve.org/library/core v0.0.0-20230519041221-8f2da7be661d // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OptionsProvider interface {
|
||||||
|
Get(key string) (string, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
kv OptionsProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// FormValueProvider we only care about something that can provide us the K/V lookup
|
||||||
|
type FormValueProvider interface {
|
||||||
|
FormValue(string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form wraps utility functions around HTTP Form values
|
||||||
|
type Form struct {
|
||||||
|
r FormValueProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Form) Get(k string) (string, bool) {
|
||||||
|
v := f.r.FormValue(k)
|
||||||
|
if v == "" {
|
||||||
|
return v, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Vars struct {
|
||||||
|
vars map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Vars) Get(k string) (string, bool) {
|
||||||
|
v, ok := c.vars[k]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
Values url.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Query) Get(key string) (string, bool) {
|
||||||
|
value, ok := q.Values[key]
|
||||||
|
if ok {
|
||||||
|
return value[0], true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (o *Options) GetVar(k string) (string, bool) {
|
||||||
|
return o.kv.Get(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) GetVarDefault(k string, def string) string {
|
||||||
|
v, ok := o.kv.Get(k)
|
||||||
|
if !ok {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) GetVarAsSlice(k string, delim string, def []string) []string {
|
||||||
|
v, ok := o.kv.Get(k)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
r := strings.Split(v, delim)
|
||||||
|
if len(r) == 1 && r[0] == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// StringValue returns form value as a string
|
||||||
|
func (o *Options) StringValue(k string) (string,bool) {
|
||||||
|
return o.kv.Get(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint64Value returns the first value in a set as uint64
|
||||||
|
func (o *Options) Uint64Value(k string) (uint64, bool) {
|
||||||
|
str,ok := o.kv.Get(k)
|
||||||
|
if !ok {
|
||||||
|
return 0, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := strconv.ParseUint(str, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return val, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSlice returns multiple values assigned to the same key as a string slice
|
||||||
|
func (o *Options) StringSlice(k string, delim string) ([]string,bool) {
|
||||||
|
str,ok := o.kv.Get(k)
|
||||||
|
if !ok {
|
||||||
|
return nil, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
r := strings.Split(str, delim)
|
||||||
|
if len(r) == 1 && r[0] == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return r, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeValue returns a parsed time value for the specified key,
|
||||||
|
// if the key does not exist the but defaultValue was set it will return the defaultValue
|
||||||
|
// otherwise it will return the passed in time or an error if parsing failed
|
||||||
|
func (o *Options) TimeValue(key, timeFormat string, defaultValue *time.Time) time.Time {
|
||||||
|
str,ok := o.kv.Get(key)
|
||||||
|
if !ok {
|
||||||
|
if defaultValue != nil {
|
||||||
|
return *defaultValue
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := time.Parse(timeFormat, str)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Err: %v", err)
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type CompoundOptions struct {
|
||||||
|
kvs []OptionsProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CompoundOptions) Get(key string) (string, bool) {
|
||||||
|
for _, kv := range c.kvs {
|
||||||
|
if v, ok := kv.Get(key); ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
|
@ -57,7 +57,6 @@ func (s *Server) makeContext(auth *auth.AuthData, w http.ResponseWriter, r *http
|
||||||
w: w,
|
w: w,
|
||||||
srv: s,
|
srv: s,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
vars: mux.Vars(r),
|
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.twelvetwelve.org/library/core/log"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceStatus struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceResponse struct {
|
||||||
|
Code int
|
||||||
|
Message []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceResponse) WriteResponse(w http.ResponseWriter) {
|
||||||
|
fmt.Printf("Response: %d/%s\n", s.Code, s.Message)
|
||||||
|
w.WriteHeader(s.Code)
|
||||||
|
w.Write(s.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) serviceWrapper(handle func(c *ServiceRequest) *ServiceResponse) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.debug {
|
||||||
|
log.Debugf("request: %+v\n", r.RequestURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &ServiceRequest{
|
||||||
|
r: r,
|
||||||
|
}
|
||||||
|
if s.auth != nil {
|
||||||
|
if rec, code := s.auth.DoAuth(w, r); code == http.StatusOK {
|
||||||
|
c.auth = rec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp := handle(c)
|
||||||
|
// discard the rest of the body content
|
||||||
|
io.Copy(ioutil.Discard, r.Body)
|
||||||
|
defer r.Body.Close()
|
||||||
|
resp.WriteResponse(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) serviceWrapperAuthenticated(handle func(c *ServiceRequest) *ServiceResponse) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.debug {
|
||||||
|
log.Debugf("request: %+v\n", r.RequestURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup content of anything sent to us
|
||||||
|
defer func() {
|
||||||
|
io.Copy(ioutil.Discard, r.Body)
|
||||||
|
r.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if s.auth == nil {
|
||||||
|
resp := ServiceResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: []byte("Not Authenticated"),
|
||||||
|
}
|
||||||
|
resp.WriteResponse(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rec, code := s.auth.DoAuth(w, r)
|
||||||
|
switch code {
|
||||||
|
case http.StatusOK:
|
||||||
|
c := &ServiceRequest{
|
||||||
|
r: r,
|
||||||
|
auth: rec,
|
||||||
|
}
|
||||||
|
resp := handle(c)
|
||||||
|
resp.WriteResponse(w)
|
||||||
|
return
|
||||||
|
|
||||||
|
case http.StatusUnauthorized:
|
||||||
|
resp := ServiceResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: []byte("Not Authenticated"),
|
||||||
|
}
|
||||||
|
resp.WriteResponse(w)
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
resp := ServiceResponse{
|
||||||
|
Code: code,
|
||||||
|
Message: []byte(http.StatusText(code)),
|
||||||
|
}
|
||||||
|
resp.WriteResponse(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) HandleServiceFunc(path string, f func(c *ServiceRequest) *ServiceResponse) *mux.Route {
|
||||||
|
return s.router.HandleFunc(path, s.serviceWrapper(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) HandleServiceFuncWithAuth(path string, f func(c *ServiceRequest) *ServiceResponse) *mux.Route {
|
||||||
|
return s.router.HandleFunc(path, s.serviceWrapperAuthenticated(f))
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
package snap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"git.twelvetwelve.org/library/snap/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceRequest struct {
|
||||||
|
r *http.Request
|
||||||
|
auth *auth.AuthData
|
||||||
|
vars *Vars
|
||||||
|
form *Form
|
||||||
|
query *Query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServiceRequest) GetUser() string {
|
||||||
|
if c.auth == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.auth.User
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServiceRequest) ReplyMessage(code int, msg string) *ServiceResponse {
|
||||||
|
return &ServiceResponse{
|
||||||
|
Code: code,
|
||||||
|
Message: []byte(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServiceRequest) ReplyObject(code int, obj interface{}) *ServiceResponse {
|
||||||
|
var err error
|
||||||
|
resp := &ServiceResponse{
|
||||||
|
Code: code,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Message, err = json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
resp.Code = http.StatusInternalServerError
|
||||||
|
resp.Message = []byte("Internal Server Error")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
compact := c.r.Header.Get("Compact")
|
||||||
|
if compact == "" {
|
||||||
|
var pretty bytes.Buffer
|
||||||
|
err := json.Indent(&pretty, resp.Message, "", " ")
|
||||||
|
if err == nil {
|
||||||
|
resp.Message = pretty.Bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServiceRequest) GetObject(object interface{}) error {
|
||||||
|
decoder := json.NewDecoder(c.r.Body)
|
||||||
|
return decoder.Decode(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServiceRequest) Form() (*Options, error) {
|
||||||
|
if c.form != nil {
|
||||||
|
return &Options{
|
||||||
|
kv: c.form,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.form = &Form{
|
||||||
|
r: c.r,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Options{
|
||||||
|
kv: c.form,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServiceRequest) Query() *Options {
|
||||||
|
if c.query != nil {
|
||||||
|
return &Options{
|
||||||
|
kv: c.query,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.query = &Query{
|
||||||
|
Values: c.r.URL.Query(),
|
||||||
|
}
|
||||||
|
return &Options{
|
||||||
|
kv: c.query,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServiceRequest) Vars() *Options {
|
||||||
|
if c.vars != nil {
|
||||||
|
return &Options{
|
||||||
|
kv: c.vars,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.vars = &Vars{
|
||||||
|
vars: mux.Vars(c.r),
|
||||||
|
}
|
||||||
|
return &Options{
|
||||||
|
kv: c.vars,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options get compound options
|
||||||
|
func (c *ServiceRequest) Options() *Options {
|
||||||
|
opts := &CompoundOptions{}
|
||||||
|
|
||||||
|
opts.kvs = append(opts.kvs, &Vars{
|
||||||
|
vars: mux.Vars(c.r),
|
||||||
|
})
|
||||||
|
|
||||||
|
opts.kvs = append(opts.kvs, &Query{
|
||||||
|
Values: c.r.URL.Query(),
|
||||||
|
})
|
||||||
|
|
||||||
|
err := c.r.ParseForm()
|
||||||
|
if err == nil {
|
||||||
|
opts.kvs = append(opts.kvs, &Form{
|
||||||
|
r: c.r,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Options{
|
||||||
|
kv: opts,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue