Add API handlers for snap

This commit is contained in:
spsobole 2023-05-29 14:21:08 -06:00
parent a0dbb354d6
commit 15a8fed766
6 changed files with 455 additions and 51 deletions

View File

@ -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
View File

@ -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

158
options.go Normal file
View File

@ -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
}

View File

@ -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
}

108
service.go Normal file
View File

@ -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))
}

136
service_request.go Normal file
View File

@ -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,
}
}