-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
daemon: JSON responses for API (#546)
* chi (https://github.com/go-chi/chi) + chi middleware for routing needs * basic JSON structure for as many things as I could update without demolishing things (package `api` and package `res`) * update CLI and client as much as possible without radical changes for new API responses * minor refactors to package log (rename `Logger` to `Streamer`, update success and error funcs to take render.Renderer)
- Loading branch information
Showing
29 changed files
with
903 additions
and
509 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package api | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
) | ||
|
||
// BaseResponse is the underlying response structure to all responses. | ||
type BaseResponse struct { | ||
// Basic metadata | ||
HTTPStatusCode int `json:"code"` | ||
RequestID string `json:"request_id,omitempty"` | ||
|
||
// Message is included in all responses, and is a summary of the server's response | ||
Message string `json:"message"` | ||
|
||
// Err contains additional context in the event of an error | ||
Err string `json:"error,omitempty"` | ||
|
||
// Data contains information the server wants to return | ||
Data interface{} `json:"data,omitempty"` | ||
} | ||
|
||
// KV is used for defining specific values to be unmarshalled from BaseResponse | ||
// data | ||
type KV struct { | ||
Key string | ||
Value interface{} | ||
} | ||
|
||
// Unmarshal reads the response and unmarshalls the BaseResponse as well any | ||
// requested key-value pairs. | ||
// For example: | ||
// | ||
// var totpResp = &api.TotpResponse{} | ||
// api.Unmarshal(resp.Body, api.KV{Key: "totp", Value: totpResp}) | ||
// | ||
// Values provided in KV.Value MUST be explicit pointers, even if the value is | ||
// a pointer type, ie maps and slices. | ||
func Unmarshal(r io.Reader, kvs ...KV) (*BaseResponse, error) { | ||
bytes, err := ioutil.ReadAll(r) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not read bytes from reader: %s", err.Error()) | ||
} | ||
|
||
// Unmarshal data into a BaseResponse, replacing BaseResponse.Data with a | ||
// map to preserve raw JSON data in the keys | ||
var ( | ||
data = make(map[string]json.RawMessage) | ||
resp = BaseResponse{Data: &data} | ||
) | ||
if err := json.Unmarshal(bytes, &resp); err != nil { | ||
return nil, fmt.Errorf("could not unmarshal data from reader: %s", err.Error()) | ||
} | ||
|
||
// Unmarshal all requested kv-pairs, silently ignoring errors | ||
for _, kv := range kvs { | ||
json.Unmarshal(data[kv.Key], kv.Value) | ||
} | ||
|
||
return &resp, nil | ||
} | ||
|
||
// Error returns a summary of an encountered error. For more details, you may | ||
// want to interrogate Data. Returns nil if StatusCode is not an HTTP error | ||
// code, ie if the code is in 1xx, 2xx, or 3xx | ||
func (b *BaseResponse) Error() error { | ||
if 100 <= b.HTTPStatusCode && b.HTTPStatusCode < 400 { | ||
return nil | ||
} | ||
if b.Err == "" { | ||
return fmt.Errorf("[error %d] %s", b.HTTPStatusCode, b.Message) | ||
} | ||
return fmt.Errorf("[error %d] %s: (%s)", b.HTTPStatusCode, b.Message, b.Err) | ||
} | ||
|
||
// TotpResponse is used for sending users their Totp secret and backup codes | ||
type TotpResponse struct { | ||
TotpSecret string `json:"secret"` | ||
BackupCodes []string `json:"backup_codes"` | ||
} | ||
|
||
// DeploymentStatus lists details about the deployed project | ||
type DeploymentStatus struct { | ||
InertiaVersion string `json:"version"` | ||
Branch string `json:"branch"` | ||
CommitHash string `json:"commit_hash"` | ||
CommitMessage string `json:"commit_message"` | ||
BuildType string `json:"build_type"` | ||
Containers []string `json:"containers"` | ||
BuildContainerActive bool `json:"build_active"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package api | ||
|
||
import ( | ||
"bytes" | ||
"testing" | ||
) | ||
|
||
func TestUnmarshal(t *testing.T) { | ||
type args struct { | ||
bytes []byte | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
wantCode int | ||
wantErr bool | ||
}{ | ||
{"invalid data", | ||
args{nil}, | ||
0, | ||
true}, | ||
{"invalid json", | ||
args{ | ||
[]byte(`{ | ||
"code":200, | ||
"request_id":"bobbook/2Mch7LMzhj-000001", | ||
"mess`), | ||
}, | ||
0, | ||
true}, | ||
{"ok", | ||
args{ | ||
[]byte(`{ | ||
"code":200, | ||
"request_id":"bobbook/2Mch7LMzhj-000001", | ||
"message":"session created", | ||
"data":{ | ||
"token":"blah" | ||
} | ||
}`), | ||
}, | ||
200, | ||
false}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
var gotToken string | ||
var gotKV = []KV{{Key: "token", Value: &gotToken}} | ||
got, err := Unmarshal(bytes.NewReader(tt.args.bytes), gotKV...) | ||
if (err != nil) != tt.wantErr { | ||
t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} | ||
if got != nil && tt.wantCode != got.HTTPStatusCode { | ||
t.Errorf("Unmarshal() code = %v, want %v", got.HTTPStatusCode, tt.wantCode) | ||
} | ||
for _, kv := range gotKV { | ||
if kv.Value == "" { | ||
t.Error("Unmarshal() kv is empty") | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestBaseResponse_Error(t *testing.T) { | ||
type fields struct { | ||
HTTPStatusCode int | ||
Message string | ||
Err string | ||
} | ||
tests := []struct { | ||
name string | ||
fields fields | ||
wantErr bool | ||
}{ | ||
{"not an error", | ||
fields{200, "hi", ""}, | ||
false}, | ||
{"error with only message", | ||
fields{400, "hi", ""}, | ||
true}, | ||
{"error with message and error context", | ||
fields{400, "hi", "oh no"}, | ||
true}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
b := &BaseResponse{ | ||
HTTPStatusCode: tt.fields.HTTPStatusCode, | ||
Message: tt.fields.Message, | ||
Err: tt.fields.Err, | ||
} | ||
if err := b.Error(); (err != nil) != tt.wantErr { | ||
t.Errorf("BaseResponse.Error() error = %v, wantErr %v", err, tt.wantErr) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.