mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 23:34:38 +03:00
mox!
This commit is contained in:
4
vendor/github.com/mjl-/sherpa/.gitignore
generated
vendored
Normal file
4
vendor/github.com/mjl-/sherpa/.gitignore
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/cover.out
|
||||
/cover.html
|
||||
|
||||
*\.swp
|
7
vendor/github.com/mjl-/sherpa/LICENSE
generated
vendored
Normal file
7
vendor/github.com/mjl-/sherpa/LICENSE
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
Copyright (c) 2016-2018 Mechiel Lukkien
|
||||
|
||||
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.
|
27
vendor/github.com/mjl-/sherpa/LICENSE-go
generated
vendored
Normal file
27
vendor/github.com/mjl-/sherpa/LICENSE-go
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
16
vendor/github.com/mjl-/sherpa/Makefile
generated
vendored
Normal file
16
vendor/github.com/mjl-/sherpa/Makefile
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
build:
|
||||
go build ./...
|
||||
go vet ./...
|
||||
|
||||
test:
|
||||
go test -coverprofile=cover.out ./...
|
||||
go tool cover -html=cover.out -o cover.html
|
||||
golint ./...
|
||||
|
||||
coverage:
|
||||
|
||||
clean:
|
||||
go clean ./...
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
39
vendor/github.com/mjl-/sherpa/README.md
generated
vendored
Normal file
39
vendor/github.com/mjl-/sherpa/README.md
generated
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# Sherpa
|
||||
|
||||
Sherpa is a Go library for creating a [sherpa API](https://www.ueber.net/who/mjl/sherpa/).
|
||||
|
||||
This library makes it trivial to export Go functions as a sherpa API with an http.Handler.
|
||||
|
||||
Your API will automatically be documented: github.com/mjl-/sherpadoc reads your Go source, and exports function and type comments as API documentation.
|
||||
|
||||
See the [documentation](https://godoc.org/github.com/mjl-/sherpa).
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
A public sherpa API: https://www.sherpadoc.org/#https://www.sherpadoc.org/example/
|
||||
|
||||
That web application is [sherpaweb](https://github.com/mjl-/sherpaweb). It shows documentation for any sherpa API but also includes an API called Example for demo purposes.
|
||||
|
||||
[Ding](https://github.com/mjl-/ding/) is a more elaborate web application built with this library.
|
||||
|
||||
|
||||
# About
|
||||
|
||||
Written by Mechiel Lukkien, mechiel@ueber.net.
|
||||
Bug fixes, patches, comments are welcome.
|
||||
MIT-licensed, see LICENSE.
|
||||
|
||||
|
||||
# todo
|
||||
|
||||
- add a toggle for enabling calls by GET request. turn off by default for functions with parameters, people might be making requests with sensitive information in query strings...
|
||||
- include a sherpaweb-like page that displays the documentation
|
||||
- consider adding input & output validation and timestamp conversion to plain js lib
|
||||
- consider using interfaces with functions (instead of direct structs) for server implementations. haven't needed it yet, but could be useful for mocking an api that you want to talk to.
|
||||
- think about way to keep unknown fields. perhaps use a json lib that collects unknown keys in a map (which has to be added to the object for which you want to keep such keys).
|
||||
- sherpajs: make a versionied, minified variant, with license line
|
||||
- tool for comparing two jsons for compatibility, listing added sections/functions/types/fields
|
||||
- be more helpful around errors that functions can generate. perhaps adding a mechanism for listing which errors can occur in the api json.
|
||||
- handler: write tests
|
||||
- client: write tests
|
19
vendor/github.com/mjl-/sherpa/codes.go
generated
vendored
Normal file
19
vendor/github.com/mjl-/sherpa/codes.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
package sherpa
|
||||
|
||||
// Errors generated by both clients and servers
|
||||
const (
|
||||
SherpaBadFunction = "sherpa:badFunction" // Function does not exist at server.
|
||||
)
|
||||
|
||||
// Errors generated by clients
|
||||
const (
|
||||
SherpaBadResponse = "sherpa:badResponse" // Bad response from server, e.g. JSON response body could not be parsed.
|
||||
SherpaHTTPError = "sherpa:http" // Unexpected http response status code from server.
|
||||
SherpaNoAPI = "sherpa:noAPI" // No API was found at this URL.
|
||||
)
|
||||
|
||||
// Errors generated by servers
|
||||
const (
|
||||
SherpaBadRequest = "sherpa:badRequest" // Error parsing JSON request body.
|
||||
SherpaBadParams = "sherpa:badParams" // Wrong number of parameters in function call.
|
||||
)
|
21
vendor/github.com/mjl-/sherpa/collector.go
generated
vendored
Normal file
21
vendor/github.com/mjl-/sherpa/collector.go
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
package sherpa
|
||||
|
||||
// Collector facilitates collection of metrics. Functions are called by the library as such events or errors occur.
|
||||
// See https://github.com/irias/sherpa-prometheus-collector for an implementation for prometheus.
|
||||
type Collector interface {
|
||||
ProtocolError() // Invalid request at protocol-level, e.g. wrong mimetype or request body.
|
||||
BadFunction() // Function does not exist.
|
||||
JavaScript() // Sherpa.js is requested.
|
||||
JSON() // Sherpa.json is requested.
|
||||
|
||||
// Call of function, how long it took, and in case of failure, the error code.
|
||||
FunctionCall(name string, durationSec float64, errorCode string)
|
||||
}
|
||||
|
||||
type ignoreCollector struct{}
|
||||
|
||||
func (ignoreCollector) ProtocolError() {}
|
||||
func (ignoreCollector) BadFunction() {}
|
||||
func (ignoreCollector) JavaScript() {}
|
||||
func (ignoreCollector) JSON() {}
|
||||
func (ignoreCollector) FunctionCall(name string, durationSec float64, errorCode string) {}
|
8
vendor/github.com/mjl-/sherpa/doc.go
generated
vendored
Normal file
8
vendor/github.com/mjl-/sherpa/doc.go
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
// Package sherpa exports your Go functions as fully documented sherpa web API's.
|
||||
//
|
||||
// Sherpa is similar to JSON-RPC, but discoverable and self-documenting.
|
||||
// Read more at https://www.ueber.net/who/mjl/sherpa/.
|
||||
//
|
||||
// Use sherpa.NewHandler to export Go functions using a http.Handler.
|
||||
// An example of how to use NewHandler can be found in https://github.com/mjl-/sherpaweb/
|
||||
package sherpa
|
653
vendor/github.com/mjl-/sherpa/handler.go
generated
vendored
Normal file
653
vendor/github.com/mjl-/sherpa/handler.go
generated
vendored
Normal file
@ -0,0 +1,653 @@
|
||||
package sherpa
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/mjl-/sherpadoc"
|
||||
)
|
||||
|
||||
// SherpaVersion is the version of the Sherpa protocol this package implements. Sherpa is at version 1.
|
||||
const SherpaVersion = 1
|
||||
|
||||
// JSON holds all fields for a request to sherpa.json.
|
||||
type JSON struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Functions []string `json:"functions"`
|
||||
BaseURL string `json:"baseurl"`
|
||||
Version string `json:"version"`
|
||||
SherpaVersion int `json:"sherpaVersion"`
|
||||
SherpadocVersion int `json:"sherpadocVersion"`
|
||||
}
|
||||
|
||||
// HandlerOpts are options for creating a new handler.
|
||||
type HandlerOpts struct {
|
||||
Collector Collector // Holds functions for collecting metrics about function calls and other incoming HTTP requests. May be nil.
|
||||
LaxParameterParsing bool // If enabled, incoming sherpa function calls will ignore unrecognized fields in struct parameters, instead of failing.
|
||||
AdjustFunctionNames string // If empty, only the first character of function names are lower cased. For "lowerWord", the first string of capitals is lowercased, for "none", the function name is left as is.
|
||||
}
|
||||
|
||||
// Raw signals a raw JSON response.
|
||||
// If a handler panics with this type, the raw bytes are sent (with regular
|
||||
// response headers).
|
||||
// Can be used to skip the json encoding from the handler, eg for caching, or
|
||||
// when you read a properly formatted JSON document from a file or database.
|
||||
// By using panic to signal a raw JSON response, the return types stay intact
|
||||
// for sherpadoc to generate documentation from.
|
||||
type Raw []byte
|
||||
|
||||
// handler that responds to all Sherpa-related requests.
|
||||
type handler struct {
|
||||
path string
|
||||
functions map[string]reflect.Value
|
||||
sherpaJSON *JSON
|
||||
opts HandlerOpts
|
||||
}
|
||||
|
||||
// Error returned by a function called through a sherpa API.
|
||||
// Message is a human-readable error message.
|
||||
// Code is optional, it can be used to handle errors programmatically.
|
||||
type Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// InternalServerError is an error that propagates as an HTTP internal server error (HTTP status 500), instead of returning a regular HTTP status 200 OK with the error message in the response body.
|
||||
// Useful for making Sherpa endpoints that can be monitored by simple HTTP monitoring tools.
|
||||
type InternalServerError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *InternalServerError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e *InternalServerError) error() *Error {
|
||||
return &Error{"internalServerError", e.Message}
|
||||
}
|
||||
|
||||
// Sherpa API response type
|
||||
type response struct {
|
||||
Result interface{} `json:"result"`
|
||||
Error *Error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
var htmlTemplate *template.Template
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
htmlTemplate, err = template.New("html").Parse(`<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{{.title}}</title>
|
||||
<style>
|
||||
body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; line-height:1.4; font-size:16px; color: #333; }
|
||||
a { color: #327CCB; }
|
||||
.code { padding: 2px 4px; font-size: 90%; color: #c7254e; background-color: #f9f2f4; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="margin:1em auto 1em; max-width:45em">
|
||||
<h1>{{.title}} <span style="font-weight:normal; font-size:0.7em">- version {{.version}}</span></h1>
|
||||
<p>
|
||||
This is the base URL for {{.title}}. The API has been loaded on this page, under variable <span class="code">{{.id}}</span>. So open your browser's developer console and start calling functions!
|
||||
</p>
|
||||
<p>
|
||||
You can also the <a href="{{.docURL}}">read documentation</a> for this API.</p>
|
||||
</p>
|
||||
<p style="text-align: center; font-size:smaller; margin-top:8ex;">
|
||||
<a href="https://github.com/mjl-/sherpa/">go sherpa code</a> |
|
||||
<a href="https://www.ueber.net/who/mjl/sherpa/">sherpa api's</a> |
|
||||
<a href="https://github.com/mjl-/sherpaweb/">sherpaweb code</a>
|
||||
</p>
|
||||
</div>
|
||||
<script src="{{.jsURL}}"></script>
|
||||
</body>
|
||||
</html>`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getBaseURL(r *http.Request) string {
|
||||
host := r.Header.Get("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = r.Host
|
||||
}
|
||||
scheme := r.Header.Get("X-Forwarded-Proto")
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
return scheme + "://" + host
|
||||
}
|
||||
|
||||
func respondJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
respond(w, status, v, false, "")
|
||||
}
|
||||
|
||||
func respond(w http.ResponseWriter, status int, v interface{}, jsonp bool, callback string) {
|
||||
if jsonp {
|
||||
w.Header().Add("Content-Type", "text/javascript; charset=utf-8")
|
||||
} else {
|
||||
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
var err error
|
||||
if jsonp {
|
||||
_, err = fmt.Fprintf(w, "%s(\n\t", callback)
|
||||
}
|
||||
if raw, ok := v.(Raw); err == nil && ok {
|
||||
_, err = w.Write([]byte(`{"result":`))
|
||||
if err == nil {
|
||||
_, err = w.Write(raw)
|
||||
}
|
||||
if err == nil {
|
||||
_, err = w.Write([]byte("}"))
|
||||
}
|
||||
} else if err == nil && !ok {
|
||||
err = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
if err == nil && jsonp {
|
||||
_, err = fmt.Fprint(w, ");")
|
||||
}
|
||||
if err != nil && !isConnectionClosed(err) {
|
||||
log.Println("writing response:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Call function fn with a json body read from r.
|
||||
// Ctx is from the http.Request, and is canceled when the http connection goes away.
|
||||
//
|
||||
// on success, the returned interface contains:
|
||||
// - nil, if fn has no return value
|
||||
// - single value, if fn had a single return value
|
||||
// - slice of values, if fn had multiple return values
|
||||
// - Raw, for a preformatted JSON response (caught from panic).
|
||||
//
|
||||
// on error, we always return an Error with the Code field set.
|
||||
func (h *handler) call(ctx context.Context, functionName string, fn reflect.Value, r io.Reader) (ret interface{}, ee error) {
|
||||
defer func() {
|
||||
e := recover()
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
|
||||
se, ok := e.(*Error)
|
||||
if ok {
|
||||
ee = se
|
||||
return
|
||||
}
|
||||
ierr, ok := e.(*InternalServerError)
|
||||
if ok {
|
||||
ee = ierr
|
||||
return
|
||||
}
|
||||
if raw, ok := e.(Raw); ok {
|
||||
ret = raw
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}()
|
||||
|
||||
lcheck := func(err error, code, message string) {
|
||||
if err != nil {
|
||||
panic(&Error{Code: code, Message: fmt.Sprintf("function %q: %s: %s", functionName, message, err)})
|
||||
}
|
||||
}
|
||||
|
||||
var request struct {
|
||||
Params json.RawMessage `json:"params"`
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(r)
|
||||
dec.DisallowUnknownFields()
|
||||
err := dec.Decode(&request)
|
||||
lcheck(err, SherpaBadRequest, "invalid JSON request body")
|
||||
|
||||
fnt := fn.Type()
|
||||
|
||||
var params []interface{}
|
||||
err = json.Unmarshal(request.Params, ¶ms)
|
||||
lcheck(err, SherpaBadRequest, "invalid JSON request body")
|
||||
|
||||
needArgs := fnt.NumIn()
|
||||
needValues := needArgs
|
||||
ctxType := reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
needsContext := needValues > 0 && fnt.In(0).Implements(ctxType)
|
||||
if needsContext {
|
||||
needArgs--
|
||||
}
|
||||
if fnt.IsVariadic() {
|
||||
if len(params) != needArgs-1 && len(params) != needArgs {
|
||||
err = fmt.Errorf("got %d, want %d or %d", len(params), needArgs-1, needArgs)
|
||||
}
|
||||
} else {
|
||||
if len(params) != needArgs {
|
||||
err = fmt.Errorf("got %d, want %d", len(params), needArgs)
|
||||
}
|
||||
}
|
||||
lcheck(err, SherpaBadParams, "bad number of parameters")
|
||||
|
||||
values := make([]reflect.Value, needValues)
|
||||
o := 0
|
||||
if needsContext {
|
||||
values[0] = reflect.ValueOf(ctx)
|
||||
o = 1
|
||||
}
|
||||
args := make([]interface{}, needArgs)
|
||||
for i := range args {
|
||||
n := reflect.New(fnt.In(o + i))
|
||||
values[o+i] = n.Elem()
|
||||
args[i] = n.Interface()
|
||||
}
|
||||
|
||||
dec = json.NewDecoder(bytes.NewReader(request.Params))
|
||||
if !h.opts.LaxParameterParsing {
|
||||
dec.DisallowUnknownFields()
|
||||
}
|
||||
err = dec.Decode(&args)
|
||||
lcheck(err, SherpaBadParams, "parsing parameters")
|
||||
|
||||
errorType := reflect.TypeOf((*error)(nil)).Elem()
|
||||
checkError := fnt.NumOut() > 0 && fnt.Out(fnt.NumOut()-1).Implements(errorType)
|
||||
|
||||
var results []reflect.Value
|
||||
if fnt.IsVariadic() {
|
||||
results = fn.CallSlice(values)
|
||||
} else {
|
||||
results = fn.Call(values)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rr := make([]interface{}, len(results))
|
||||
for i, v := range results {
|
||||
rr[i] = v.Interface()
|
||||
}
|
||||
if !checkError {
|
||||
if len(rr) == 1 {
|
||||
return rr[0], nil
|
||||
}
|
||||
return rr, nil
|
||||
}
|
||||
rr, rerr := rr[:len(rr)-1], rr[len(rr)-1]
|
||||
var rv interface{} = rr
|
||||
switch len(rr) {
|
||||
case 0:
|
||||
rv = nil
|
||||
case 1:
|
||||
rv = rr[0]
|
||||
}
|
||||
if rerr == nil {
|
||||
return rv, nil
|
||||
}
|
||||
switch r := rerr.(type) {
|
||||
case *Error:
|
||||
return nil, r
|
||||
case *InternalServerError:
|
||||
return nil, r
|
||||
case error:
|
||||
return nil, &Error{Message: r.Error()}
|
||||
default:
|
||||
panic("checkError while type is not error")
|
||||
}
|
||||
}
|
||||
|
||||
func adjustFunctionNameCapitals(s string, opts HandlerOpts) string {
|
||||
switch opts.AdjustFunctionNames {
|
||||
case "":
|
||||
return strings.ToLower(s[:1]) + s[1:]
|
||||
case "none":
|
||||
return s
|
||||
case "lowerWord":
|
||||
r := ""
|
||||
for i, c := range s {
|
||||
lc := unicode.ToLower(c)
|
||||
if lc == c {
|
||||
r += s[i:]
|
||||
break
|
||||
}
|
||||
r += string(lc)
|
||||
}
|
||||
return r
|
||||
default:
|
||||
panic(fmt.Sprintf("bad value for AdjustFunctionNames: %q", opts.AdjustFunctionNames))
|
||||
}
|
||||
}
|
||||
|
||||
func gatherFunctions(functions map[string]reflect.Value, t reflect.Type, v reflect.Value, opts HandlerOpts) error {
|
||||
if t.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("sherpa sections must be a struct (not a ptr)")
|
||||
}
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
name := adjustFunctionNameCapitals(t.Method(i).Name, opts)
|
||||
m := v.Method(i)
|
||||
if _, ok := functions[name]; ok {
|
||||
return fmt.Errorf("duplicate function %s", name)
|
||||
}
|
||||
functions[name] = m
|
||||
}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
err := gatherFunctions(functions, t.Field(i).Type, v.Field(i), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewHandler returns a new http.Handler that serves all Sherpa API-related requests.
|
||||
//
|
||||
// Path is the path this API is available at.
|
||||
//
|
||||
// Version should be a semantic version.
|
||||
//
|
||||
// API should by a struct. It represents the root section. All methods of a
|
||||
// section are exported as sherpa functions. All fields must be other sections
|
||||
// (structs) whose methods are also exported. recursively. Method names must
|
||||
// start with an uppercase character to be exported, but their exported names
|
||||
// start with a lowercase character by default (but see HandlerOpts.AdjustFunctionNames).
|
||||
//
|
||||
// Doc is documentation for the top-level sherpa section, as generated by sherpadoc.
|
||||
//
|
||||
// Opts allows further configuration of the handler.
|
||||
//
|
||||
// Methods on the exported sections are exported as Sherpa functions.
|
||||
// If the first parameter of a method is a context.Context, the context from the HTTP request is passed.
|
||||
// This lets you abort work if the HTTP request underlying the function call disappears.
|
||||
//
|
||||
// Parameters and return values for exported functions are automatically converted from/to JSON.
|
||||
// If the last element of a return value (if any) is an error,
|
||||
// that error field is taken to indicate whether the call succeeded.
|
||||
// Exported functions can also panic with an *Error or *InternalServerError to indicate a failed function call.
|
||||
// Returning an error with a Code starting with "server" indicates an implementation error, which will be logged through the collector.
|
||||
//
|
||||
// Variadic functions can be called, but in the call (from the client), the variadic parameters must be passed in as an array.
|
||||
//
|
||||
// This handler strips "path" from the request.
|
||||
func NewHandler(path string, version string, api interface{}, doc *sherpadoc.Section, opts *HandlerOpts) (http.Handler, error) {
|
||||
var xopts HandlerOpts
|
||||
if opts != nil {
|
||||
xopts = *opts
|
||||
}
|
||||
if xopts.Collector == nil {
|
||||
// We always want to have a collector, so we don't have to check for nil all the time when calling.
|
||||
xopts.Collector = ignoreCollector{}
|
||||
}
|
||||
|
||||
doc.Version = version
|
||||
doc.SherpaVersion = SherpaVersion
|
||||
functions := map[string]reflect.Value{
|
||||
"_docs": reflect.ValueOf(func() *sherpadoc.Section {
|
||||
return doc
|
||||
}),
|
||||
}
|
||||
err := gatherFunctions(functions, reflect.TypeOf(api), reflect.ValueOf(api), xopts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(functions))
|
||||
for name := range functions {
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
elems := strings.Split(strings.Trim(path, "/"), "/")
|
||||
id := elems[len(elems)-1]
|
||||
sherpaJSON := &JSON{
|
||||
ID: id,
|
||||
Title: doc.Name,
|
||||
Functions: names,
|
||||
BaseURL: "", // filled in during request
|
||||
Version: version,
|
||||
SherpaVersion: SherpaVersion,
|
||||
SherpadocVersion: doc.SherpadocVersion,
|
||||
}
|
||||
h := http.StripPrefix(path, &handler{
|
||||
path: path,
|
||||
functions: functions,
|
||||
sherpaJSON: sherpaJSON,
|
||||
opts: xopts,
|
||||
})
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func badMethod(w http.ResponseWriter) {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
// return whether callback js snippet is valid.
|
||||
// this is a coarse test. we disallow some valid js identifiers, like "\u03c0",
|
||||
// and we allow many invalid ones, such as js keywords, "0intro" and identifiers starting/ending with ".", or having multiple dots.
|
||||
func validCallback(cb string) bool {
|
||||
if cb == "" {
|
||||
return false
|
||||
}
|
||||
for _, c := range cb {
|
||||
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '$' || c == '.' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Serve a HTTP request for this Sherpa API.
|
||||
// ServeHTTP expects the request path is stripped from the path it was mounted at with the http package.
|
||||
//
|
||||
// The following endpoints are handled:
|
||||
// - sherpa.json, describing this API.
|
||||
// - sherpa.js, a small stand-alone client JavaScript library that makes it trivial to start using this API from a browser.
|
||||
// - functionName, for function invocations on this API.
|
||||
//
|
||||
// HTTP response will have CORS-headers set, and support the OPTIONS HTTP method.
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
hdr := w.Header()
|
||||
hdr.Set("Access-Control-Allow-Origin", "*")
|
||||
hdr.Set("Access-Control-Allow-Methods", "GET, POST")
|
||||
hdr.Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
collector := h.opts.Collector
|
||||
|
||||
switch {
|
||||
case r.URL.Path == "":
|
||||
baseURL := getBaseURL(r) + h.path
|
||||
docURL := "https://www.sherpadoc.org/#" + baseURL
|
||||
err := htmlTemplate.Execute(w, map[string]interface{}{
|
||||
"id": h.sherpaJSON.ID,
|
||||
"title": h.sherpaJSON.Title,
|
||||
"version": h.sherpaJSON.Version,
|
||||
"docURL": docURL,
|
||||
"jsURL": baseURL + "sherpa.js",
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
case r.URL.Path == "sherpa.json":
|
||||
switch r.Method {
|
||||
case "OPTIONS":
|
||||
w.WriteHeader(204)
|
||||
case "GET":
|
||||
collector.JSON()
|
||||
hdr.Set("Content-Type", "application/json; charset=utf-8")
|
||||
hdr.Set("Cache-Control", "no-cache")
|
||||
sherpaJSON := &*h.sherpaJSON
|
||||
sherpaJSON.BaseURL = getBaseURL(r) + h.path
|
||||
err := json.NewEncoder(w).Encode(sherpaJSON)
|
||||
if err != nil {
|
||||
log.Println("writing sherpa.json response:", err)
|
||||
}
|
||||
default:
|
||||
badMethod(w)
|
||||
}
|
||||
|
||||
case r.URL.Path == "sherpa.js":
|
||||
if r.Method != "GET" {
|
||||
badMethod(w)
|
||||
return
|
||||
}
|
||||
collector.JavaScript()
|
||||
hdr.Set("Content-Type", "text/javascript; charset=utf-8")
|
||||
hdr.Set("Cache-Control", "no-cache")
|
||||
sherpaJSON := &*h.sherpaJSON
|
||||
sherpaJSON.BaseURL = getBaseURL(r) + h.path
|
||||
buf, err := json.Marshal(sherpaJSON)
|
||||
js := strings.Replace(sherpaJS, "{{.sherpaJSON}}", string(buf), -1)
|
||||
_, err = w.Write([]byte(js))
|
||||
if err != nil {
|
||||
log.Println("writing sherpa.js response:", err)
|
||||
}
|
||||
|
||||
default:
|
||||
name := r.URL.Path
|
||||
fn, ok := h.functions[name]
|
||||
switch r.Method {
|
||||
case "OPTIONS":
|
||||
w.WriteHeader(204)
|
||||
|
||||
case "POST":
|
||||
hdr.Set("Cache-Control", "no-store")
|
||||
|
||||
if !ok {
|
||||
collector.BadFunction()
|
||||
respondJSON(w, 404, &response{Error: &Error{Code: SherpaBadFunction, Message: fmt.Sprintf("function %q does not exist", name)}})
|
||||
return
|
||||
}
|
||||
|
||||
ct := r.Header.Get("Content-Type")
|
||||
if ct == "" {
|
||||
collector.ProtocolError()
|
||||
respondJSON(w, 200, &response{Error: &Error{Code: SherpaBadRequest, Message: fmt.Sprintf("missing content-type")}})
|
||||
return
|
||||
}
|
||||
mt, mtparams, err := mime.ParseMediaType(ct)
|
||||
if err != nil {
|
||||
collector.ProtocolError()
|
||||
respondJSON(w, 200, &response{Error: &Error{Code: SherpaBadRequest, Message: fmt.Sprintf("invalid content-type %q", ct)}})
|
||||
return
|
||||
}
|
||||
if mt != "application/json" {
|
||||
collector.ProtocolError()
|
||||
respondJSON(w, 200, &response{Error: &Error{Code: SherpaBadRequest, Message: fmt.Sprintf(`unrecognized content-type %q, expecting "application/json"`, mt)}})
|
||||
return
|
||||
}
|
||||
charset, ok := mtparams["charset"]
|
||||
if ok && strings.ToLower(charset) != "utf-8" {
|
||||
collector.ProtocolError()
|
||||
respondJSON(w, 200, &response{Error: &Error{Code: SherpaBadRequest, Message: fmt.Sprintf(`unexpected charset %q, expecting "utf-8"`, charset)}})
|
||||
return
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
r, xerr := h.call(r.Context(), name, fn, r.Body)
|
||||
durationSec := float64(time.Now().Sub(t0)) / float64(time.Second)
|
||||
if xerr != nil {
|
||||
switch err := xerr.(type) {
|
||||
case *InternalServerError:
|
||||
collector.FunctionCall(name, durationSec, err.Code)
|
||||
respondJSON(w, 500, &response{Error: err.error()})
|
||||
case *Error:
|
||||
collector.FunctionCall(name, durationSec, err.Code)
|
||||
respondJSON(w, 200, &response{Error: err})
|
||||
default:
|
||||
collector.FunctionCall(name, durationSec, "server:panic")
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
var v interface{}
|
||||
if raw, ok := r.(Raw); ok {
|
||||
v = raw
|
||||
} else {
|
||||
v = &response{Result: r}
|
||||
}
|
||||
collector.FunctionCall(name, durationSec, "")
|
||||
respondJSON(w, 200, v)
|
||||
}
|
||||
|
||||
case "GET":
|
||||
hdr.Set("Cache-Control", "no-store")
|
||||
|
||||
jsonp := false
|
||||
if !ok {
|
||||
collector.BadFunction()
|
||||
respondJSON(w, 404, &response{Error: &Error{Code: SherpaBadFunction, Message: fmt.Sprintf("function %q does not exist", name)}})
|
||||
return
|
||||
}
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
collector.ProtocolError()
|
||||
respondJSON(w, 200, &response{Error: &Error{Code: SherpaBadRequest, Message: fmt.Sprintf("could not parse query string")}})
|
||||
return
|
||||
}
|
||||
|
||||
callback := r.Form.Get("callback")
|
||||
_, ok := r.Form["callback"]
|
||||
if ok {
|
||||
if !validCallback(callback) {
|
||||
collector.ProtocolError()
|
||||
respondJSON(w, 200, &response{Error: &Error{Code: SherpaBadRequest, Message: fmt.Sprintf(`invalid callback name %q`, callback)}})
|
||||
return
|
||||
}
|
||||
jsonp = true
|
||||
}
|
||||
|
||||
// We allow an empty list to be missing to make it cleaner & easier to call health check functions (no ugly urls).
|
||||
body := r.Form.Get("body")
|
||||
_, ok = r.Form["body"]
|
||||
if !ok {
|
||||
body = `{"params": []}`
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
r, xerr := h.call(r.Context(), name, fn, strings.NewReader(body))
|
||||
durationSec := float64(time.Now().Sub(t0)) / float64(time.Second)
|
||||
if xerr != nil {
|
||||
switch err := xerr.(type) {
|
||||
case *InternalServerError:
|
||||
collector.FunctionCall(name, durationSec, err.Code)
|
||||
respond(w, 500, &response{Error: err.error()}, jsonp, callback)
|
||||
case *Error:
|
||||
collector.FunctionCall(name, durationSec, err.Code)
|
||||
respond(w, 200, &response{Error: err}, jsonp, callback)
|
||||
default:
|
||||
collector.FunctionCall(name, durationSec, "server:panic")
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
var v interface{}
|
||||
if raw, ok := r.(Raw); ok {
|
||||
v = raw
|
||||
} else {
|
||||
v = &response{Result: r}
|
||||
}
|
||||
collector.FunctionCall(name, durationSec, "")
|
||||
respond(w, 200, v, jsonp, callback)
|
||||
}
|
||||
|
||||
default:
|
||||
badMethod(w)
|
||||
}
|
||||
}
|
||||
}
|
87
vendor/github.com/mjl-/sherpa/intstr.go
generated
vendored
Normal file
87
vendor/github.com/mjl-/sherpa/intstr.go
generated
vendored
Normal file
@ -0,0 +1,87 @@
|
||||
package sherpa
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Int64s is an int64 that can be read as either a JSON string or JSON number, to
|
||||
// be used in sherpa function parameters for compatibility with JavaScript.
|
||||
// For struct fields, use the "json:,string" struct tag instead.
|
||||
type Int64s int64
|
||||
|
||||
// Int returns the int64 value.
|
||||
func (i Int64s) Int() int64 {
|
||||
return int64(i)
|
||||
}
|
||||
|
||||
// MarshalJSON returns a JSON-string-encoding of the int64.
|
||||
func (i *Int64s) MarshalJSON() ([]byte, error) {
|
||||
var v int64
|
||||
if i != nil {
|
||||
v = int64(*i)
|
||||
}
|
||||
return json.Marshal(fmt.Sprintf("%d", v))
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses JSON into the int64. Both a string encoding as a number
|
||||
// encoding are allowed. JavaScript clients must use the string encoding because
|
||||
// the number encoding loses precision at 1<<53.
|
||||
func (i *Int64s) UnmarshalJSON(buf []byte) error {
|
||||
var s string
|
||||
if len(buf) > 0 && buf[0] == '"' {
|
||||
err := json.Unmarshal(buf, &s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
s = string(buf)
|
||||
}
|
||||
vv, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*i = Int64s(vv)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uint64s is an uint64 that can be read as either a JSON string or JSON number, to
|
||||
// be used in sherpa function parameters for compatibility with JavaScript.
|
||||
// For struct fields, use the "json:,string" struct tag instead.
|
||||
type Uint64s uint64
|
||||
|
||||
// Int returns the uint64 value.
|
||||
func (i Uint64s) Int() uint64 {
|
||||
return uint64(i)
|
||||
}
|
||||
|
||||
// MarshalJSON returns a JSON-string-encoding of the uint64.
|
||||
func (i *Uint64s) MarshalJSON() ([]byte, error) {
|
||||
var v uint64
|
||||
if i != nil {
|
||||
v = uint64(*i)
|
||||
}
|
||||
return json.Marshal(fmt.Sprintf("%d", v))
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses JSON into the uint64. Both a string encoding as a number
|
||||
// encoding are allowed. JavaScript clients must use the string encoding because
|
||||
// the number encoding loses precision at 1<<53.
|
||||
func (i *Uint64s) UnmarshalJSON(buf []byte) error {
|
||||
var s string
|
||||
if len(buf) > 0 && buf[0] == '"' {
|
||||
err := json.Unmarshal(buf, &s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
s = string(buf)
|
||||
}
|
||||
vv, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*i = Uint64s(vv)
|
||||
return nil
|
||||
}
|
13
vendor/github.com/mjl-/sherpa/isclosed.go
generated
vendored
Normal file
13
vendor/github.com/mjl-/sherpa/isclosed.go
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
//go:build !plan9
|
||||
// +build !plan9
|
||||
|
||||
package sherpa
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func isConnectionClosed(err error) bool {
|
||||
return errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET)
|
||||
}
|
6
vendor/github.com/mjl-/sherpa/isclosed_plan9.go
generated
vendored
Normal file
6
vendor/github.com/mjl-/sherpa/isclosed_plan9.go
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
package sherpa
|
||||
|
||||
func isConnectionClosed(err error) bool {
|
||||
// todo: needs a better test
|
||||
return false
|
||||
}
|
136
vendor/github.com/mjl-/sherpa/sherpajs.go
generated
vendored
Normal file
136
vendor/github.com/mjl-/sherpa/sherpajs.go
generated
vendored
Normal file
@ -0,0 +1,136 @@
|
||||
package sherpa
|
||||
|
||||
var sherpaJS = `
|
||||
'use strict';
|
||||
|
||||
(function(undefined) {
|
||||
|
||||
var sherpa = {};
|
||||
|
||||
// prepare basic support for promises.
|
||||
// we return functions with a "then" method only. our "then" isn't chainable. and you don't get other promise-related methods.
|
||||
// but this "then" is enough so your browser's promise library (or a polyfill) can turn it into a real promise.
|
||||
function thenable(fn) {
|
||||
var settled = false;
|
||||
var fulfilled = false;
|
||||
var result = null;
|
||||
|
||||
var goods = [];
|
||||
var bads = [];
|
||||
|
||||
// promise lib will call the returned function, make it the same as our .then function
|
||||
var nfn = function(goodfn, badfn) {
|
||||
if(settled) {
|
||||
if(fulfilled && goodfn) {
|
||||
goodfn(result);
|
||||
}
|
||||
if(!fulfilled && badfn) {
|
||||
badfn(result);
|
||||
}
|
||||
} else {
|
||||
if(goodfn) {
|
||||
goods.push(goodfn);
|
||||
}
|
||||
if(badfn) {
|
||||
bads.push(badfn);
|
||||
}
|
||||
}
|
||||
};
|
||||
nfn.then = nfn;
|
||||
|
||||
function done() {
|
||||
while(fulfilled && goods.length > 0) {
|
||||
goods.shift()(result);
|
||||
}
|
||||
while(!fulfilled && bads.length > 0) {
|
||||
bads.shift()(result);
|
||||
}
|
||||
}
|
||||
|
||||
function makeSettle(xfulfilled) {
|
||||
return function(arg) {
|
||||
if(settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
fulfilled = xfulfilled;
|
||||
result = arg;
|
||||
done();
|
||||
};
|
||||
}
|
||||
var resolve = makeSettle(true);
|
||||
var reject = makeSettle(false);
|
||||
try {
|
||||
fn(resolve, reject);
|
||||
} catch(e) {
|
||||
reject(e);
|
||||
}
|
||||
return nfn;
|
||||
}
|
||||
|
||||
function postJSON(url, param, success, error) {
|
||||
var req = new window.XMLHttpRequest();
|
||||
req.open('POST', url, true);
|
||||
req.onload = function onload() {
|
||||
if(req.status >= 200 && req.status < 400) {
|
||||
success(JSON.parse(req.responseText));
|
||||
} else {
|
||||
if(req.status === 404) {
|
||||
error({code: 'sherpaBadFunction', message: 'function does not exist'});
|
||||
} else {
|
||||
error({code: 'sherpaHttpError', message: 'error calling function, HTTP status: '+req.status});
|
||||
}
|
||||
}
|
||||
};
|
||||
req.onerror = function onerror() {
|
||||
error({code: 'sherpaClientError', message: 'connection failed'});
|
||||
};
|
||||
req.setRequestHeader('Content-Type', 'application/json');
|
||||
req.send(JSON.stringify(param));
|
||||
}
|
||||
|
||||
function makeFunction(api, name) {
|
||||
return function() {
|
||||
var params = Array.prototype.slice.call(arguments, 0);
|
||||
return api._wrapThenable(thenable(function(resolve, reject) {
|
||||
postJSON(api._sherpa.baseurl+name, {params: params}, function(response) {
|
||||
if(response && response.error) {
|
||||
reject(response.error);
|
||||
} else if(response && response.hasOwnProperty('result')) {
|
||||
resolve(response.result);
|
||||
} else {
|
||||
reject({code: 'sherpaBadResponse', message: "invalid sherpa response object, missing 'result'"});
|
||||
}
|
||||
}, reject);
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
sherpa.init = function init(_sherpa) {
|
||||
var api = {};
|
||||
|
||||
function _wrapThenable(thenable) {
|
||||
return thenable;
|
||||
}
|
||||
|
||||
function _call(name) {
|
||||
return makeFunction(api, name).apply(Array.prototype.slice.call(arguments, 1));
|
||||
}
|
||||
|
||||
api._sherpa = _sherpa;
|
||||
api._wrapThenable = _wrapThenable;
|
||||
api._call = _call;
|
||||
for(var i = 0; i < _sherpa.functions.length; i++) {
|
||||
var fn = _sherpa.functions[i];
|
||||
api[fn] = makeFunction(api, fn);
|
||||
}
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
|
||||
var _sherpa = {{.sherpaJSON}};
|
||||
window[_sherpa.id] = sherpa.init(_sherpa);
|
||||
|
||||
})();
|
||||
`
|
Reference in New Issue
Block a user