porting all github actions from bluesky-social/indigo to tangled CI

switch APIClient from interface to just a struct

Changed files
+127 -162
atproto
+105 -15
atproto/client/api_client.go
··· 1 1 package client 2 2 3 3 import ( 4 + "bytes" 4 5 "context" 5 6 "encoding/json" 7 + "fmt" 6 8 "net/http" 7 9 8 10 "github.com/bluesky-social/indigo/atproto/syntax" 9 11 ) 10 12 11 - // NOTE: this is an interface so it can be wrapped/extended. eg, a variant with a bunch of retries, or caching, or whatever. maybe that is too complex and we should have simple struct type, more like the existing `indigo/xrpc` package? hrm. 13 + type APIClient struct { 14 + HTTPClient *http.Client 15 + Host string 16 + Auth AuthMethod 17 + DefaultHeaders map[string]string 18 + } 12 19 13 - type APIClient interface { 14 - // Full-power method for making atproto API requests. 15 - Do(ctx context.Context, req *APIRequest) (*http.Response, error) 20 + // High-level helper for simple JSON "Query" API calls. 21 + // 22 + // Does not work with all API endpoints. For more control, use the Do() method with APIRequest. 23 + func (c *APIClient) Get(ctx context.Context, endpoint syntax.NSID, params map[string]string) (*json.RawMessage, error) { 24 + hdr := map[string]string{ 25 + "Accept": "application/json", 26 + } 27 + req := APIRequest{ 28 + HTTPVerb: "GET", 29 + Endpoint: endpoint, 30 + Body: nil, 31 + QueryParams: params, 32 + Headers: hdr, 33 + } 34 + resp, err := c.Do(ctx, req) 35 + if err != nil { 36 + return nil, err 37 + } 16 38 17 - // High-level helper for simple JSON "Query" API calls. 18 - // 19 - // Does not work with all API endpoints. For more control, use the Do() method with APIRequest. 20 - Get(ctx context.Context, endpoint syntax.NSID, params map[string]string) (*json.RawMessage, error) 39 + defer resp.Body.Close() 40 + // TODO: duplicate error handling with Post()? 41 + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { 42 + var eb ErrorBody 43 + if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil { 44 + return nil, &APIError{StatusCode: resp.StatusCode} 45 + } 46 + return nil, eb.APIError(resp.StatusCode) 47 + } 21 48 22 - // High-level helper for simple JSON-to-JSON "Procedure" API calls. 23 - // 24 - // Does not work with all API endpoints. For more control, use the Do() method with APIRequest. 25 - // TODO: what is the right type for body, to indicate it can be marshaled as JSON? 26 - Post(ctx context.Context, endpoint syntax.NSID, body any) (*json.RawMessage, error) 49 + var ret json.RawMessage 50 + if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { 51 + return nil, fmt.Errorf("expected JSON response body: %w", err) 52 + } 53 + return &ret, nil 54 + } 27 55 28 - // Returns the currently-authenticated account DID, or empty string if not available. 29 - AuthDID() syntax.DID 56 + // High-level helper for simple JSON-to-JSON "Procedure" API calls. 57 + // 58 + // Does not work with all API endpoints. For more control, use the Do() method with APIRequest. 59 + func (c *APIClient) Post(ctx context.Context, endpoint syntax.NSID, body any) (*json.RawMessage, error) { 60 + bodyJSON, err := json.Marshal(body) 61 + if err != nil { 62 + return nil, err 63 + } 64 + hdr := map[string]string{ 65 + "Accept": "application/json", 66 + "Content-Type": "application/json", 67 + } 68 + req := APIRequest{ 69 + HTTPVerb: "POST", 70 + Endpoint: endpoint, 71 + Body: bytes.NewReader(bodyJSON), 72 + QueryParams: nil, 73 + Headers: hdr, 74 + } 75 + resp, err := c.Do(ctx, req) 76 + if err != nil { 77 + return nil, err 78 + } 79 + 80 + defer resp.Body.Close() 81 + // TODO: duplicate error handling with Get()? 82 + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { 83 + var eb ErrorBody 84 + if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil { 85 + return nil, &APIError{StatusCode: resp.StatusCode} 86 + } 87 + return nil, eb.APIError(resp.StatusCode) 88 + } 89 + 90 + var ret json.RawMessage 91 + if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { 92 + return nil, fmt.Errorf("expected JSON response body: %w", err) 93 + } 94 + return &ret, nil 95 + } 96 + 97 + // Full-power method for atproto API requests. 98 + func (c *APIClient) Do(ctx context.Context, req APIRequest) (*http.Response, error) { 99 + httpReq, err := req.HTTPRequest(ctx, c.Host, c.DefaultHeaders) 100 + if err != nil { 101 + return nil, err 102 + } 103 + 104 + // TODO: thread-safe? 105 + if c.HTTPClient == nil { 106 + c.HTTPClient = http.DefaultClient 107 + } 108 + 109 + var resp *http.Response 110 + if c.Auth != nil { 111 + resp, err = c.Auth.DoWithAuth(ctx, httpReq, c.HTTPClient) 112 + } else { 113 + resp, err = c.HTTPClient.Do(httpReq) 114 + } 115 + if err != nil { 116 + return nil, err 117 + } 118 + // TODO: handle some common response errors: rate-limits, 5xx, auth required, etc 119 + return resp, nil 30 120 }
-120
atproto/client/base_api_client.go
··· 1 - package client 2 - 3 - import ( 4 - "bytes" 5 - "context" 6 - "encoding/json" 7 - "fmt" 8 - "net/http" 9 - 10 - "github.com/bluesky-social/indigo/atproto/syntax" 11 - ) 12 - 13 - type BaseAPIClient struct { 14 - HTTPClient *http.Client 15 - Host string 16 - Auth AuthMethod 17 - DefaultHeaders map[string]string 18 - } 19 - 20 - func (c *BaseAPIClient) Get(ctx context.Context, endpoint syntax.NSID, params map[string]string) (*json.RawMessage, error) { 21 - hdr := map[string]string{ 22 - "Accept": "application/json", 23 - } 24 - req := APIRequest{ 25 - HTTPVerb: "GET", 26 - Endpoint: endpoint, 27 - Body: nil, 28 - QueryParams: params, 29 - Headers: hdr, 30 - } 31 - resp, err := c.Do(ctx, req) 32 - if err != nil { 33 - return nil, err 34 - } 35 - 36 - defer resp.Body.Close() 37 - // TODO: duplicate error handling with Post()? 38 - if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { 39 - var eb ErrorBody 40 - if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil { 41 - return nil, &APIError{StatusCode: resp.StatusCode} 42 - } 43 - return nil, eb.APIError(resp.StatusCode) 44 - } 45 - 46 - var ret json.RawMessage 47 - if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { 48 - return nil, fmt.Errorf("expected JSON response body: %w", err) 49 - } 50 - return &ret, nil 51 - } 52 - 53 - func (c *BaseAPIClient) Post(ctx context.Context, endpoint syntax.NSID, body any) (*json.RawMessage, error) { 54 - bodyJSON, err := json.Marshal(body) 55 - if err != nil { 56 - return nil, err 57 - } 58 - hdr := map[string]string{ 59 - "Accept": "application/json", 60 - "Content-Type": "application/json", 61 - } 62 - req := APIRequest{ 63 - HTTPVerb: "POST", 64 - Endpoint: endpoint, 65 - Body: bytes.NewReader(bodyJSON), 66 - QueryParams: nil, 67 - Headers: hdr, 68 - } 69 - resp, err := c.Do(ctx, req) 70 - if err != nil { 71 - return nil, err 72 - } 73 - 74 - defer resp.Body.Close() 75 - // TODO: duplicate error handling with Get()? 76 - if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { 77 - var eb ErrorBody 78 - if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil { 79 - return nil, &APIError{StatusCode: resp.StatusCode} 80 - } 81 - return nil, eb.APIError(resp.StatusCode) 82 - } 83 - 84 - var ret json.RawMessage 85 - if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { 86 - return nil, fmt.Errorf("expected JSON response body: %w", err) 87 - } 88 - return &ret, nil 89 - } 90 - 91 - func (c *BaseAPIClient) Do(ctx context.Context, req APIRequest) (*http.Response, error) { 92 - httpReq, err := req.HTTPRequest(ctx, c.Host, c.DefaultHeaders) 93 - if err != nil { 94 - return nil, err 95 - } 96 - 97 - // TODO: thread-safe? 98 - if c.HTTPClient == nil { 99 - c.HTTPClient = http.DefaultClient 100 - } 101 - 102 - var resp *http.Response 103 - if c.Auth != nil { 104 - resp, err = c.Auth.DoWithAuth(ctx, httpReq, c.HTTPClient) 105 - } else { 106 - resp, err = c.HTTPClient.Do(httpReq) 107 - } 108 - if err != nil { 109 - return nil, err 110 - } 111 - // TODO: handle some common response errors: rate-limits, 5xx, auth required, etc 112 - return resp, nil 113 - } 114 - 115 - func (c *BaseAPIClient) AuthDID() syntax.DID { 116 - if c.Auth != nil { 117 - return c.Auth.AccountDID() 118 - } 119 - return "" 120 - }
+1 -1
atproto/client/cmd/atclient/main.go
··· 32 32 func runGet(cctx *cli.Context) error { 33 33 ctx := context.Background() 34 34 35 - c := client.BaseAPIClient{ 35 + c := client.APIClient{ 36 36 Host: "https://public.api.bsky.app", 37 37 } 38 38
+21 -26
atproto/client/examples_test.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 + "context" 6 + "encoding/json" 5 7 6 - atdata "github.com/bluesky-social/indigo/atproto/data" 8 + appbsky "github.com/bluesky-social/indigo/api/bsky" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 7 10 ) 8 11 9 - func ExampleGetRequest() { 12 + func ExampleGet() { 10 13 11 - // First load Lexicon schema JSON files from local disk. 12 - cat := NewBaseCatalog() 13 - if err := cat.LoadDirectory("testdata/catalog"); err != nil { 14 - panic("failed to load lexicons") 14 + ctx := context.Background() 15 + 16 + c := APIClient{ 17 + Host: "https://public.api.bsky.app", 15 18 } 16 19 17 - // Parse record JSON data using atproto/data helper 18 - recordJSON := `{ 19 - "$type": "example.lexicon.record", 20 - "integer": 123, 21 - "formats": { 22 - "did": "did:web:example.com", 23 - "aturi": "at://handle.example.com/com.example.nsid/asdf123", 24 - "datetime": "2023-10-30T22:25:23Z", 25 - "language": "en", 26 - "tid": "3kznmn7xqxl22" 27 - } 28 - }` 29 - 30 - recordData, err := atdata.UnmarshalJSON([]byte(recordJSON)) 20 + endpoint := syntax.NSID("app.bsky.actor.getProfile") 21 + params := map[string]string{ 22 + "actor": "atproto.com", 23 + } 24 + b, err := c.Get(ctx, endpoint, params) 31 25 if err != nil { 32 - panic("failed to parse record JSON") 26 + panic(err) 33 27 } 34 28 35 - if err := ValidateRecord(&cat, recordData, "example.lexicon.record", 0); err != nil { 36 - fmt.Printf("Schema validation failed: %v\n", err) 37 - } else { 38 - fmt.Println("Success!") 29 + var profile appbsky.ActorDefs_ProfileViewDetailed 30 + if err := json.Unmarshal(*b, &profile); err != nil { 31 + panic(err) 39 32 } 40 - // Output: Success! 33 + 34 + fmt.Println(profile.Handle) 35 + // Output: atproto.com 41 36 }