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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.twelvetwelve.org/library/snap/auth"
|
||||
)
|
||||
|
@ -17,7 +16,8 @@ type Context struct {
|
|||
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
vars map[string]string
|
||||
vars *Vars
|
||||
form *Form
|
||||
}
|
||||
|
||||
type SnapContent struct {
|
||||
|
@ -69,11 +69,21 @@ func (c *Context) ReplyWithHeaders(msg string, kv map[string]string) {
|
|||
}
|
||||
|
||||
func (c *Context) ReplyObject(obj interface{}) {
|
||||
compact := c.r.Header.Get("Compact")
|
||||
msg, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
c.Error(http.StatusInternalServerError, "Internal Server Error")
|
||||
return
|
||||
}
|
||||
|
||||
if compact == "" {
|
||||
var pretty bytes.Buffer
|
||||
err := json.Indent(&pretty, msg, "", " ")
|
||||
if err == nil {
|
||||
msg = pretty.Bytes()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (c *Context) GetVar(k string) (string, bool) {
|
||||
v, ok := c.vars[k]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (c *Context) GetVarDefault(k string, def string) string {
|
||||
v, ok := c.vars[k]
|
||||
if !ok {
|
||||
return def
|
||||
func (c *Context) Form() (*Options, error) {
|
||||
if c.form != nil {
|
||||
return &Options{
|
||||
kv: c.form,
|
||||
}, nil
|
||||
}
|
||||
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)
|
||||
err := c.r.ParseForm()
|
||||
if err != nil {
|
||||
return 0
|
||||
return nil, err
|
||||
}
|
||||
c.form = &Form{
|
||||
r: c.r,
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
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
|
||||
return &Options{
|
||||
kv: c.form,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Context) Redirect(url string) {
|
||||
|
@ -206,3 +183,29 @@ func (c *Context) QueryValueWithDefault(key string, def string) string {
|
|||
}
|
||||
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
|
||||
|
||||
require (
|
||||
git.twelvetwelve.org/library/core v0.0.0-20230519041221-8f2da7be661d
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
git.twelvetwelve.org/library/core v0.0.0-20230519041221-8f2da7be661d // indirect
|
||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // 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,
|
||||
srv: s,
|
||||
auth: auth,
|
||||
vars: mux.Vars(r),
|
||||
}
|
||||
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