refactor(ax): enforce v0.8.0 polish rules
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
c147b5650c
commit
551a964fdb
57 changed files with 705 additions and 404 deletions
32
actions.go
32
actions.go
|
|
@ -4,13 +4,17 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// ActionsService handles CI/CD actions operations across repositories and
|
||||
// organisations — secrets, variables, and workflow dispatches.
|
||||
// No Resource embedding — heterogeneous endpoints across repo and org levels.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Actions.ListRepoSecrets(ctx, "core", "go-forge")
|
||||
type ActionsService struct {
|
||||
client *Client
|
||||
}
|
||||
|
|
@ -21,82 +25,82 @@ func newActionsService(c *Client) *ActionsService {
|
|||
|
||||
// ListRepoSecrets returns all secrets for a repository.
|
||||
func (s *ActionsService) ListRepoSecrets(ctx context.Context, owner, repo string) ([]types.Secret, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/actions/secrets", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets", pathParams("owner", owner, "repo", repo))
|
||||
return ListAll[types.Secret](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterRepoSecrets returns an iterator over all secrets for a repository.
|
||||
func (s *ActionsService) IterRepoSecrets(ctx context.Context, owner, repo string) iter.Seq2[types.Secret, error] {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/actions/secrets", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets", pathParams("owner", owner, "repo", repo))
|
||||
return ListIter[types.Secret](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// CreateRepoSecret creates or updates a secret in a repository.
|
||||
// Forgejo expects a PUT with {"data": "secret-value"} body.
|
||||
func (s *ActionsService) CreateRepoSecret(ctx context.Context, owner, repo, name string, data string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/actions/secrets/%s", owner, repo, name)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets/{name}", pathParams("owner", owner, "repo", repo, "name", name))
|
||||
body := map[string]string{"data": data}
|
||||
return s.client.Put(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// DeleteRepoSecret removes a secret from a repository.
|
||||
func (s *ActionsService) DeleteRepoSecret(ctx context.Context, owner, repo, name string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/actions/secrets/%s", owner, repo, name)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets/{name}", pathParams("owner", owner, "repo", repo, "name", name))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListRepoVariables returns all action variables for a repository.
|
||||
func (s *ActionsService) ListRepoVariables(ctx context.Context, owner, repo string) ([]types.ActionVariable, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/actions/variables", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables", pathParams("owner", owner, "repo", repo))
|
||||
return ListAll[types.ActionVariable](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterRepoVariables returns an iterator over all action variables for a repository.
|
||||
func (s *ActionsService) IterRepoVariables(ctx context.Context, owner, repo string) iter.Seq2[types.ActionVariable, error] {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/actions/variables", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables", pathParams("owner", owner, "repo", repo))
|
||||
return ListIter[types.ActionVariable](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// CreateRepoVariable creates a new action variable in a repository.
|
||||
// Forgejo expects a POST with {"value": "var-value"} body.
|
||||
func (s *ActionsService) CreateRepoVariable(ctx context.Context, owner, repo, name, value string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/actions/variables/%s", owner, repo, name)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables/{name}", pathParams("owner", owner, "repo", repo, "name", name))
|
||||
body := types.CreateVariableOption{Value: value}
|
||||
return s.client.Post(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// DeleteRepoVariable removes an action variable from a repository.
|
||||
func (s *ActionsService) DeleteRepoVariable(ctx context.Context, owner, repo, name string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/actions/variables/%s", owner, repo, name)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables/{name}", pathParams("owner", owner, "repo", repo, "name", name))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListOrgSecrets returns all secrets for an organisation.
|
||||
func (s *ActionsService) ListOrgSecrets(ctx context.Context, org string) ([]types.Secret, error) {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/actions/secrets", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/actions/secrets", pathParams("org", org))
|
||||
return ListAll[types.Secret](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterOrgSecrets returns an iterator over all secrets for an organisation.
|
||||
func (s *ActionsService) IterOrgSecrets(ctx context.Context, org string) iter.Seq2[types.Secret, error] {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/actions/secrets", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/actions/secrets", pathParams("org", org))
|
||||
return ListIter[types.Secret](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// ListOrgVariables returns all action variables for an organisation.
|
||||
func (s *ActionsService) ListOrgVariables(ctx context.Context, org string) ([]types.ActionVariable, error) {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/actions/variables", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/actions/variables", pathParams("org", org))
|
||||
return ListAll[types.ActionVariable](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterOrgVariables returns an iterator over all action variables for an organisation.
|
||||
func (s *ActionsService) IterOrgVariables(ctx context.Context, org string) iter.Seq2[types.ActionVariable, error] {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/actions/variables", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/actions/variables", pathParams("org", org))
|
||||
return ListIter[types.ActionVariable](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// DispatchWorkflow triggers a workflow run.
|
||||
func (s *ActionsService) DispatchWorkflow(ctx context.Context, owner, repo, workflow string, opts map[string]any) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/dispatches", owner, repo, workflow)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/workflows/{workflow}/dispatches", pathParams("owner", owner, "repo", repo, "workflow", workflow))
|
||||
return s.client.Post(ctx, path, opts, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestActionsService_Good_ListRepoSecrets(t *testing.T) {
|
||||
func TestActionsService_ListRepoSecrets_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -42,7 +42,7 @@ func TestActionsService_Good_ListRepoSecrets(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestActionsService_Good_CreateRepoSecret(t *testing.T) {
|
||||
func TestActionsService_CreateRepoSecret_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
t.Errorf("expected PUT, got %s", r.Method)
|
||||
|
|
@ -68,7 +68,7 @@ func TestActionsService_Good_CreateRepoSecret(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestActionsService_Good_DeleteRepoSecret(t *testing.T) {
|
||||
func TestActionsService_DeleteRepoSecret_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
|
|
@ -87,7 +87,7 @@ func TestActionsService_Good_DeleteRepoSecret(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestActionsService_Good_ListRepoVariables(t *testing.T) {
|
||||
func TestActionsService_ListRepoVariables_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -115,7 +115,7 @@ func TestActionsService_Good_ListRepoVariables(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestActionsService_Good_CreateRepoVariable(t *testing.T) {
|
||||
func TestActionsService_CreateRepoVariable_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -141,7 +141,7 @@ func TestActionsService_Good_CreateRepoVariable(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestActionsService_Good_DeleteRepoVariable(t *testing.T) {
|
||||
func TestActionsService_DeleteRepoVariable_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
|
|
@ -160,7 +160,7 @@ func TestActionsService_Good_DeleteRepoVariable(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestActionsService_Good_ListOrgSecrets(t *testing.T) {
|
||||
func TestActionsService_ListOrgSecrets_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -188,7 +188,7 @@ func TestActionsService_Good_ListOrgSecrets(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestActionsService_Good_ListOrgVariables(t *testing.T) {
|
||||
func TestActionsService_ListOrgVariables_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -216,7 +216,7 @@ func TestActionsService_Good_ListOrgVariables(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestActionsService_Good_DispatchWorkflow(t *testing.T) {
|
||||
func TestActionsService_DispatchWorkflow_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -244,7 +244,7 @@ func TestActionsService_Good_DispatchWorkflow(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestActionsService_Bad_NotFound(t *testing.T) {
|
||||
func TestActionsService_NotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "not found"})
|
||||
|
|
|
|||
5
admin.go
5
admin.go
|
|
@ -10,6 +10,11 @@ import (
|
|||
// AdminService handles site administration operations.
|
||||
// Unlike other services, AdminService does not embed Resource[T,C,U]
|
||||
// because admin endpoints are heterogeneous.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Admin.ListUsers(ctx)
|
||||
type AdminService struct {
|
||||
client *Client
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestAdminService_Good_ListUsers(t *testing.T) {
|
||||
func TestAdminService_ListUsers_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -39,7 +39,7 @@ func TestAdminService_Good_ListUsers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdminService_Good_CreateUser(t *testing.T) {
|
||||
func TestAdminService_CreateUser_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -78,7 +78,7 @@ func TestAdminService_Good_CreateUser(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdminService_Good_DeleteUser(t *testing.T) {
|
||||
func TestAdminService_DeleteUser_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
|
|
@ -96,7 +96,7 @@ func TestAdminService_Good_DeleteUser(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdminService_Good_RunCron(t *testing.T) {
|
||||
func TestAdminService_RunCron_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -114,7 +114,7 @@ func TestAdminService_Good_RunCron(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdminService_Good_EditUser(t *testing.T) {
|
||||
func TestAdminService_EditUser_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPatch {
|
||||
t.Errorf("expected PATCH, got %s", r.Method)
|
||||
|
|
@ -142,7 +142,7 @@ func TestAdminService_Good_EditUser(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdminService_Good_RenameUser(t *testing.T) {
|
||||
func TestAdminService_RenameUser_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -167,7 +167,7 @@ func TestAdminService_Good_RenameUser(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdminService_Good_ListOrgs(t *testing.T) {
|
||||
func TestAdminService_ListOrgs_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -195,7 +195,7 @@ func TestAdminService_Good_ListOrgs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdminService_Good_ListCron(t *testing.T) {
|
||||
func TestAdminService_ListCron_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -223,7 +223,7 @@ func TestAdminService_Good_ListCron(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdminService_Good_AdoptRepo(t *testing.T) {
|
||||
func TestAdminService_AdoptRepo_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -241,7 +241,7 @@ func TestAdminService_Good_AdoptRepo(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdminService_Good_GenerateRunnerToken(t *testing.T) {
|
||||
func TestAdminService_GenerateRunnerToken_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -263,7 +263,7 @@ func TestAdminService_Good_GenerateRunnerToken(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdminService_Bad_DeleteUser_NotFound(t *testing.T) {
|
||||
func TestAdminService_DeleteUser_NotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "user not found"})
|
||||
|
|
@ -277,7 +277,7 @@ func TestAdminService_Bad_DeleteUser_NotFound(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdminService_Bad_CreateUser_Forbidden(t *testing.T) {
|
||||
func TestAdminService_CreateUser_Forbidden_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "only admins can create users"})
|
||||
|
|
|
|||
18
branches.go
18
branches.go
|
|
@ -4,11 +4,15 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// BranchService handles branch operations within a repository.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Branches.ListBranchProtections(ctx, "core", "go-forge")
|
||||
type BranchService struct {
|
||||
Resource[types.Branch, types.CreateBranchRepoOption, struct{}]
|
||||
}
|
||||
|
|
@ -23,19 +27,19 @@ func newBranchService(c *Client) *BranchService {
|
|||
|
||||
// ListBranchProtections returns all branch protections for a repository.
|
||||
func (s *BranchService) ListBranchProtections(ctx context.Context, owner, repo string) ([]types.BranchProtection, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections", pathParams("owner", owner, "repo", repo))
|
||||
return ListAll[types.BranchProtection](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterBranchProtections returns an iterator over all branch protections for a repository.
|
||||
func (s *BranchService) IterBranchProtections(ctx context.Context, owner, repo string) iter.Seq2[types.BranchProtection, error] {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections", pathParams("owner", owner, "repo", repo))
|
||||
return ListIter[types.BranchProtection](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// GetBranchProtection returns a single branch protection by name.
|
||||
func (s *BranchService) GetBranchProtection(ctx context.Context, owner, repo, name string) (*types.BranchProtection, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections/{name}", pathParams("owner", owner, "repo", repo, "name", name))
|
||||
var out types.BranchProtection
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -45,7 +49,7 @@ func (s *BranchService) GetBranchProtection(ctx context.Context, owner, repo, na
|
|||
|
||||
// CreateBranchProtection creates a new branch protection rule.
|
||||
func (s *BranchService) CreateBranchProtection(ctx context.Context, owner, repo string, opts *types.CreateBranchProtectionOption) (*types.BranchProtection, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections", pathParams("owner", owner, "repo", repo))
|
||||
var out types.BranchProtection
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -55,7 +59,7 @@ func (s *BranchService) CreateBranchProtection(ctx context.Context, owner, repo
|
|||
|
||||
// EditBranchProtection updates an existing branch protection rule.
|
||||
func (s *BranchService) EditBranchProtection(ctx context.Context, owner, repo, name string, opts *types.EditBranchProtectionOption) (*types.BranchProtection, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections/{name}", pathParams("owner", owner, "repo", repo, "name", name))
|
||||
var out types.BranchProtection
|
||||
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -65,6 +69,6 @@ func (s *BranchService) EditBranchProtection(ctx context.Context, owner, repo, n
|
|||
|
||||
// DeleteBranchProtection deletes a branch protection rule.
|
||||
func (s *BranchService) DeleteBranchProtection(ctx context.Context, owner, repo, name string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections/{name}", pathParams("owner", owner, "repo", repo, "name", name))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestBranchService_Good_List(t *testing.T) {
|
||||
func TestBranchService_List_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -36,7 +36,7 @@ func TestBranchService_Good_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBranchService_Good_Get(t *testing.T) {
|
||||
func TestBranchService_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -61,7 +61,7 @@ func TestBranchService_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBranchService_Good_CreateProtection(t *testing.T) {
|
||||
func TestBranchService_CreateProtection_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
|
|||
94
client.go
94
client.go
|
|
@ -3,16 +3,21 @@ package forge
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// APIError represents an error response from the Forgejo API.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// if apiErr, ok := err.(*forge.APIError); ok {
|
||||
// _ = apiErr.StatusCode
|
||||
// }
|
||||
type APIError struct {
|
||||
StatusCode int
|
||||
Message string
|
||||
|
|
@ -20,41 +25,76 @@ type APIError struct {
|
|||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return core.Sprintf("forge: %s %d: %s", e.URL, e.StatusCode, e.Message)
|
||||
return core.Concat("forge: ", e.URL, " ", strconv.Itoa(e.StatusCode), ": ", e.Message)
|
||||
}
|
||||
|
||||
// IsNotFound returns true if the error is a 404 response.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// if forge.IsNotFound(err) {
|
||||
// return nil
|
||||
// }
|
||||
func IsNotFound(err error) bool {
|
||||
var apiErr *APIError
|
||||
return core.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound
|
||||
}
|
||||
|
||||
// IsForbidden returns true if the error is a 403 response.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// if forge.IsForbidden(err) {
|
||||
// return nil
|
||||
// }
|
||||
func IsForbidden(err error) bool {
|
||||
var apiErr *APIError
|
||||
return core.As(err, &apiErr) && apiErr.StatusCode == http.StatusForbidden
|
||||
}
|
||||
|
||||
// IsConflict returns true if the error is a 409 response.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// if forge.IsConflict(err) {
|
||||
// return nil
|
||||
// }
|
||||
func IsConflict(err error) bool {
|
||||
var apiErr *APIError
|
||||
return core.As(err, &apiErr) && apiErr.StatusCode == http.StatusConflict
|
||||
}
|
||||
|
||||
// Option configures the Client.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// opts := []forge.Option{forge.WithUserAgent("go-forge/1.0")}
|
||||
type Option func(*Client)
|
||||
|
||||
// WithHTTPClient sets a custom http.Client.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// c := forge.NewClient(url, token, forge.WithHTTPClient(http.DefaultClient))
|
||||
func WithHTTPClient(hc *http.Client) Option {
|
||||
return func(c *Client) { c.httpClient = hc }
|
||||
}
|
||||
|
||||
// WithUserAgent sets the User-Agent header.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// c := forge.NewClient(url, token, forge.WithUserAgent("go-forge/1.0"))
|
||||
func WithUserAgent(ua string) Option {
|
||||
return func(c *Client) { c.userAgent = ua }
|
||||
}
|
||||
|
||||
// RateLimit represents the rate limit information from the Forgejo API.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// rl := client.RateLimit()
|
||||
// _ = rl.Remaining
|
||||
type RateLimit struct {
|
||||
Limit int
|
||||
Remaining int
|
||||
|
|
@ -62,6 +102,11 @@ type RateLimit struct {
|
|||
}
|
||||
|
||||
// Client is a low-level HTTP client for the Forgejo API.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// c := forge.NewClient("https://forge.lthn.ai", "token")
|
||||
// _ = c
|
||||
type Client struct {
|
||||
baseURL string
|
||||
token string
|
||||
|
|
@ -76,9 +121,14 @@ func (c *Client) RateLimit() RateLimit {
|
|||
}
|
||||
|
||||
// NewClient creates a new Forgejo API client.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// c := forge.NewClient("https://forge.lthn.ai", "token")
|
||||
// _ = c
|
||||
func NewClient(url, token string, opts ...Option) *Client {
|
||||
c := &Client{
|
||||
baseURL: core.TrimSuffix(url, "/"),
|
||||
baseURL: trimTrailingSlashes(url),
|
||||
token: token,
|
||||
httpClient: &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
|
|
@ -137,16 +187,16 @@ func (c *Client) PostRaw(ctx context.Context, path string, body any) ([]byte, er
|
|||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
r := core.JSONMarshal(body)
|
||||
if !r.OK {
|
||||
return nil, coreerr.E("Client.PostRaw", "forge: marshal body", nil)
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, core.E("Client.PostRaw", "forge: marshal body", err)
|
||||
}
|
||||
bodyReader = bytes.NewReader(r.Value.([]byte))
|
||||
bodyReader = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bodyReader)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("Client.PostRaw", "forge: create request", err)
|
||||
return nil, core.E("Client.PostRaw", "forge: create request", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+c.token)
|
||||
|
|
@ -157,7 +207,7 @@ func (c *Client) PostRaw(ctx context.Context, path string, body any) ([]byte, er
|
|||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("Client.PostRaw", "forge: request POST "+path, err)
|
||||
return nil, core.E("Client.PostRaw", "forge: request POST "+path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
|
@ -167,7 +217,7 @@ func (c *Client) PostRaw(ctx context.Context, path string, body any) ([]byte, er
|
|||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("Client.PostRaw", "forge: read response body", err)
|
||||
return nil, core.E("Client.PostRaw", "forge: read response body", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
|
@ -180,7 +230,7 @@ func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error) {
|
|||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("Client.GetRaw", "forge: create request", err)
|
||||
return nil, core.E("Client.GetRaw", "forge: create request", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+c.token)
|
||||
|
|
@ -190,7 +240,7 @@ func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error) {
|
|||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("Client.GetRaw", "forge: request GET "+path, err)
|
||||
return nil, core.E("Client.GetRaw", "forge: request GET "+path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
|
@ -200,7 +250,7 @@ func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error) {
|
|||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("Client.GetRaw", "forge: read response body", err)
|
||||
return nil, core.E("Client.GetRaw", "forge: read response body", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
|
@ -216,16 +266,16 @@ func (c *Client) doJSON(ctx context.Context, method, path string, body, out any)
|
|||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
r := core.JSONMarshal(body)
|
||||
if !r.OK {
|
||||
return nil, coreerr.E("Client.doJSON", "forge: marshal body", nil)
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, core.E("Client.doJSON", "forge: marshal body", err)
|
||||
}
|
||||
bodyReader = bytes.NewReader(r.Value.([]byte))
|
||||
bodyReader = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("Client.doJSON", "forge: create request", err)
|
||||
return nil, core.E("Client.doJSON", "forge: create request", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+c.token)
|
||||
|
|
@ -239,7 +289,7 @@ func (c *Client) doJSON(ctx context.Context, method, path string, body, out any)
|
|||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("Client.doJSON", "forge: request "+method+" "+path, err)
|
||||
return nil, core.E("Client.doJSON", "forge: request "+method+" "+path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
|
@ -251,7 +301,7 @@ func (c *Client) doJSON(ctx context.Context, method, path string, body, out any)
|
|||
|
||||
if out != nil && resp.StatusCode != http.StatusNoContent {
|
||||
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
|
||||
return nil, coreerr.E("Client.doJSON", "forge: decode response", err)
|
||||
return nil, core.E("Client.doJSON", "forge: decode response", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -265,7 +315,7 @@ func (c *Client) parseError(resp *http.Response, path string) error {
|
|||
|
||||
// Read a bit of the body to see if we can get a message
|
||||
data, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
core.JSONUnmarshal(data, &errBody)
|
||||
_ = json.Unmarshal(data, &errBody)
|
||||
|
||||
msg := errBody.Message
|
||||
if msg == "" && len(data) > 0 {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
func TestClient_Good_Get(t *testing.T) {
|
||||
func TestClient_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -36,7 +36,7 @@ func TestClient_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_Good_Post(t *testing.T) {
|
||||
func TestClient_Post_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -63,7 +63,7 @@ func TestClient_Good_Post(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_Good_Delete(t *testing.T) {
|
||||
func TestClient_Delete_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
|
|
@ -79,7 +79,7 @@ func TestClient_Good_Delete(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_Bad_ServerError(t *testing.T) {
|
||||
func TestClient_ServerError_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "internal error"})
|
||||
|
|
@ -100,7 +100,7 @@ func TestClient_Bad_ServerError(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_Bad_NotFound(t *testing.T) {
|
||||
func TestClient_NotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "not found"})
|
||||
|
|
@ -114,7 +114,7 @@ func TestClient_Bad_NotFound(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_Good_ContextCancellation(t *testing.T) {
|
||||
func TestClient_ContextCancellation_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
<-r.Context().Done()
|
||||
}))
|
||||
|
|
@ -129,7 +129,7 @@ func TestClient_Good_ContextCancellation(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_Good_Options(t *testing.T) {
|
||||
func TestClient_Options_Good(t *testing.T) {
|
||||
c := NewClient("https://forge.lthn.ai", "tok",
|
||||
WithUserAgent("go-forge/1.0"),
|
||||
)
|
||||
|
|
@ -138,7 +138,7 @@ func TestClient_Good_Options(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_Good_WithHTTPClient(t *testing.T) {
|
||||
func TestClient_WithHTTPClient_Good(t *testing.T) {
|
||||
custom := &http.Client{}
|
||||
c := NewClient("https://forge.lthn.ai", "tok", WithHTTPClient(custom))
|
||||
if c.httpClient != custom {
|
||||
|
|
@ -146,7 +146,7 @@ func TestClient_Good_WithHTTPClient(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAPIError_Good_Error(t *testing.T) {
|
||||
func TestAPIError_Error_Good(t *testing.T) {
|
||||
e := &APIError{StatusCode: 404, Message: "not found", URL: "/api/v1/repos/x/y"}
|
||||
got := e.Error()
|
||||
want := "forge: /api/v1/repos/x/y 404: not found"
|
||||
|
|
@ -155,28 +155,28 @@ func TestAPIError_Good_Error(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIsConflict_Good(t *testing.T) {
|
||||
func TestIsConflict_Match_Good(t *testing.T) {
|
||||
err := &APIError{StatusCode: http.StatusConflict, Message: "conflict", URL: "/test"}
|
||||
if !IsConflict(err) {
|
||||
t.Error("expected IsConflict to return true for 409")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsConflict_Bad_NotConflict(t *testing.T) {
|
||||
func TestIsConflict_NotConflict_Bad(t *testing.T) {
|
||||
err := &APIError{StatusCode: http.StatusNotFound, Message: "not found", URL: "/test"}
|
||||
if IsConflict(err) {
|
||||
t.Error("expected IsConflict to return false for 404")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsForbidden_Bad_NotForbidden(t *testing.T) {
|
||||
func TestIsForbidden_NotForbidden_Bad(t *testing.T) {
|
||||
err := &APIError{StatusCode: http.StatusNotFound, Message: "not found", URL: "/test"}
|
||||
if IsForbidden(err) {
|
||||
t.Error("expected IsForbidden to return false for 404")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Good_RateLimit(t *testing.T) {
|
||||
func TestClient_RateLimit_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-RateLimit-Limit", "100")
|
||||
w.Header().Set("X-RateLimit-Remaining", "99")
|
||||
|
|
@ -203,7 +203,7 @@ func TestClient_Good_RateLimit(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_Bad_Forbidden(t *testing.T) {
|
||||
func TestClient_Forbidden_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "forbidden"})
|
||||
|
|
@ -217,7 +217,7 @@ func TestClient_Bad_Forbidden(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_Bad_Conflict(t *testing.T) {
|
||||
func TestClient_Conflict_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "already exists"})
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// typeGrouping maps type name prefixes to output file names.
|
||||
|
|
@ -151,7 +150,7 @@ func classifyType(name string) string {
|
|||
// sanitiseLine collapses a multi-line string into a single line,
|
||||
// replacing newlines and consecutive whitespace with a single space.
|
||||
func sanitiseLine(s string) string {
|
||||
return core.Join(" ", strings.Fields(s)...)
|
||||
return core.Join(" ", splitFields(s)...)
|
||||
}
|
||||
|
||||
// enumConstName generates a Go constant name for an enum value.
|
||||
|
|
@ -204,9 +203,14 @@ type templateData struct {
|
|||
}
|
||||
|
||||
// Generate writes Go source files for the extracted types, grouped by logical domain.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// err := Generate(types, pairs, "types")
|
||||
// _ = err
|
||||
func Generate(types map[string]*GoType, pairs []CRUDPair, outDir string) error {
|
||||
if err := coreio.Local.EnsureDir(outDir); err != nil {
|
||||
return coreerr.E("Generate", "create output directory", err)
|
||||
return core.E("Generate", "create output directory", err)
|
||||
}
|
||||
|
||||
// Group types by output file.
|
||||
|
|
@ -219,13 +223,7 @@ func Generate(types map[string]*GoType, pairs []CRUDPair, outDir string) error {
|
|||
// Sort types within each group for deterministic output.
|
||||
for file := range groups {
|
||||
slices.SortFunc(groups[file], func(a, b *GoType) int {
|
||||
if a.Name < b.Name {
|
||||
return -1
|
||||
}
|
||||
if a.Name > b.Name {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -236,7 +234,7 @@ func Generate(types map[string]*GoType, pairs []CRUDPair, outDir string) error {
|
|||
for _, file := range fileNames {
|
||||
outPath := core.JoinPath(outDir, file+".go")
|
||||
if err := writeFile(outPath, groups[file]); err != nil {
|
||||
return coreerr.E("Generate", "write "+outPath, err)
|
||||
return core.E("Generate", "write "+outPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,11 +256,11 @@ func writeFile(path string, types []*GoType) error {
|
|||
|
||||
var buf bytes.Buffer
|
||||
if err := fileHeader.Execute(&buf, data); err != nil {
|
||||
return coreerr.E("writeFile", "execute template", err)
|
||||
return core.E("writeFile", "execute template", err)
|
||||
}
|
||||
|
||||
if err := coreio.Local.Write(path, buf.String()); err != nil {
|
||||
return coreerr.E("writeFile", "write file", err)
|
||||
return core.E("writeFile", "write file", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
func TestGenerate_Good_CreatesFiles(t *testing.T) {
|
||||
func TestGenerate_CreatesFiles_Good(t *testing.T) {
|
||||
spec, err := LoadSpec("../../testdata/swagger.v1.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -22,7 +21,7 @@ func TestGenerate_Good_CreatesFiles(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entries, _ := os.ReadDir(outDir)
|
||||
entries, _ := coreio.Local.List(outDir)
|
||||
goFiles := 0
|
||||
for _, e := range entries {
|
||||
if core.HasSuffix(e.Name(), ".go") {
|
||||
|
|
@ -34,7 +33,7 @@ func TestGenerate_Good_CreatesFiles(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGenerate_Good_ValidGoSyntax(t *testing.T) {
|
||||
func TestGenerate_ValidGoSyntax_Good(t *testing.T) {
|
||||
spec, err := LoadSpec("../../testdata/swagger.v1.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -48,7 +47,7 @@ func TestGenerate_Good_ValidGoSyntax(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entries, _ := os.ReadDir(outDir)
|
||||
entries, _ := coreio.Local.List(outDir)
|
||||
var content string
|
||||
for _, e := range entries {
|
||||
if core.HasSuffix(e.Name(), ".go") {
|
||||
|
|
@ -69,7 +68,7 @@ func TestGenerate_Good_ValidGoSyntax(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGenerate_Good_RepositoryType(t *testing.T) {
|
||||
func TestGenerate_RepositoryType_Good(t *testing.T) {
|
||||
spec, err := LoadSpec("../../testdata/swagger.v1.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -84,7 +83,7 @@ func TestGenerate_Good_RepositoryType(t *testing.T) {
|
|||
}
|
||||
|
||||
var content string
|
||||
entries, _ := os.ReadDir(outDir)
|
||||
entries, _ := coreio.Local.List(outDir)
|
||||
for _, e := range entries {
|
||||
data, _ := coreio.Local.Read(core.JoinPath(outDir, e.Name()))
|
||||
if core.Contains(data, "type Repository struct") {
|
||||
|
|
@ -112,7 +111,7 @@ func TestGenerate_Good_RepositoryType(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGenerate_Good_TimeImport(t *testing.T) {
|
||||
func TestGenerate_TimeImport_Good(t *testing.T) {
|
||||
spec, err := LoadSpec("../../testdata/swagger.v1.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -126,7 +125,7 @@ func TestGenerate_Good_TimeImport(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entries, _ := os.ReadDir(outDir)
|
||||
entries, _ := coreio.Local.List(outDir)
|
||||
for _, e := range entries {
|
||||
content, _ := coreio.Local.Read(core.JoinPath(outDir, e.Name()))
|
||||
if core.Contains(content, "time.Time") && !core.Contains(content, "\"time\"") {
|
||||
|
|
|
|||
41
cmd/forgegen/helpers.go
Normal file
41
cmd/forgegen/helpers.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
func splitFields(s string) []string {
|
||||
return splitFunc(s, unicode.IsSpace)
|
||||
}
|
||||
|
||||
func splitSnakeKebab(s string) []string {
|
||||
return splitFunc(s, func(r rune) bool {
|
||||
return r == '_' || r == '-'
|
||||
})
|
||||
}
|
||||
|
||||
func splitFunc(s string, isDelimiter func(rune) bool) []string {
|
||||
var parts []string
|
||||
buf := core.NewBuilder()
|
||||
|
||||
flush := func() {
|
||||
if buf.Len() == 0 {
|
||||
return
|
||||
}
|
||||
parts = append(parts, buf.String())
|
||||
buf.Reset()
|
||||
}
|
||||
|
||||
for _, r := range s {
|
||||
if isDelimiter(r) {
|
||||
flush()
|
||||
continue
|
||||
}
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
flush()
|
||||
|
||||
return parts
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package main
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
|
@ -14,18 +13,16 @@ func main() {
|
|||
|
||||
spec, err := LoadSpec(*specPath)
|
||||
if err != nil {
|
||||
core.Print(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
panic(core.E("forgegen.main", "load spec", err))
|
||||
}
|
||||
|
||||
types := ExtractTypes(spec)
|
||||
pairs := DetectCRUDPairs(spec)
|
||||
|
||||
core.Print(os.Stdout, "Loaded %d types, %d CRUD pairs\n", len(types), len(pairs))
|
||||
core.Print(os.Stdout, "Output dir: %s\n", *outDir)
|
||||
core.Print(nil, "Loaded %d types, %d CRUD pairs", len(types), len(pairs))
|
||||
core.Print(nil, "Output dir: %s", *outDir)
|
||||
|
||||
if err := Generate(types, pairs, *outDir); err != nil {
|
||||
core.Print(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
panic(core.E("forgegen.main", "generate types", err))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
json "github.com/goccy/go-json"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// Spec represents a Swagger 2.0 specification document.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// spec, err := LoadSpec("testdata/swagger.v1.json")
|
||||
// _ = spec
|
||||
type Spec struct {
|
||||
Swagger string `json:"swagger"`
|
||||
Info SpecInfo `json:"info"`
|
||||
|
|
@ -18,12 +23,20 @@ type Spec struct {
|
|||
}
|
||||
|
||||
// SpecInfo holds metadata about the API specification.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// _ = SpecInfo{Title: "Forgejo API", Version: "1.0"}
|
||||
type SpecInfo struct {
|
||||
Title string `json:"title"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// SchemaDefinition represents a single type definition in the swagger spec.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// _ = SchemaDefinition{Type: "object"}
|
||||
type SchemaDefinition struct {
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
|
|
@ -34,6 +47,10 @@ type SchemaDefinition struct {
|
|||
}
|
||||
|
||||
// SchemaProperty represents a single property within a schema definition.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// _ = SchemaProperty{Type: "string"}
|
||||
type SchemaProperty struct {
|
||||
Type string `json:"type"`
|
||||
Format string `json:"format"`
|
||||
|
|
@ -45,6 +62,10 @@ type SchemaProperty struct {
|
|||
}
|
||||
|
||||
// GoType is the intermediate representation for a Go type to be generated.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// _ = GoType{Name: "Repository"}
|
||||
type GoType struct {
|
||||
Name string
|
||||
Description string
|
||||
|
|
@ -54,6 +75,10 @@ type GoType struct {
|
|||
}
|
||||
|
||||
// GoField is the intermediate representation for a single struct field.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// _ = GoField{GoName: "ID", GoType: "int64"}
|
||||
type GoField struct {
|
||||
GoName string
|
||||
GoType string
|
||||
|
|
@ -63,6 +88,10 @@ type GoField struct {
|
|||
}
|
||||
|
||||
// CRUDPair groups a base type with its corresponding Create and Edit option types.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// _ = CRUDPair{Base: "Repository", Create: "CreateRepoOption", Edit: "EditRepoOption"}
|
||||
type CRUDPair struct {
|
||||
Base string
|
||||
Create string
|
||||
|
|
@ -70,20 +99,29 @@ type CRUDPair struct {
|
|||
}
|
||||
|
||||
// LoadSpec reads and parses a Swagger 2.0 JSON file from the given path.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// spec, err := LoadSpec("testdata/swagger.v1.json")
|
||||
// _ = spec
|
||||
func LoadSpec(path string) (*Spec, error) {
|
||||
content, err := coreio.Local.Read(path)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("LoadSpec", "read spec", err)
|
||||
return nil, core.E("LoadSpec", "read spec", err)
|
||||
}
|
||||
var spec Spec
|
||||
r := core.JSONUnmarshal([]byte(content), &spec)
|
||||
if !r.OK {
|
||||
return nil, coreerr.E("LoadSpec", "parse spec", nil)
|
||||
if err := json.Unmarshal([]byte(content), &spec); err != nil {
|
||||
return nil, core.E("LoadSpec", "parse spec", err)
|
||||
}
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
// ExtractTypes converts all swagger definitions into Go type intermediate representations.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// types := ExtractTypes(spec)
|
||||
// _ = types["Repository"]
|
||||
func ExtractTypes(spec *Spec) map[string]*GoType {
|
||||
result := make(map[string]*GoType)
|
||||
for name, def := range spec.Definitions {
|
||||
|
|
@ -91,7 +129,7 @@ func ExtractTypes(spec *Spec) map[string]*GoType {
|
|||
if len(def.Enum) > 0 {
|
||||
gt.IsEnum = true
|
||||
for _, v := range def.Enum {
|
||||
gt.EnumValues = append(gt.EnumValues, core.Sprintf("%v", v))
|
||||
gt.EnumValues = append(gt.EnumValues, core.Sprint(v))
|
||||
}
|
||||
slices.Sort(gt.EnumValues)
|
||||
result[name] = gt
|
||||
|
|
@ -116,13 +154,7 @@ func ExtractTypes(spec *Spec) map[string]*GoType {
|
|||
gt.Fields = append(gt.Fields, gf)
|
||||
}
|
||||
slices.SortFunc(gt.Fields, func(a, b GoField) int {
|
||||
if a.GoName < b.GoName {
|
||||
return -1
|
||||
}
|
||||
if a.GoName > b.GoName {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
return cmp.Compare(a.GoName, b.GoName)
|
||||
})
|
||||
result[name] = gt
|
||||
}
|
||||
|
|
@ -131,6 +163,11 @@ func ExtractTypes(spec *Spec) map[string]*GoType {
|
|||
|
||||
// DetectCRUDPairs finds Create*Option / Edit*Option pairs in the swagger definitions
|
||||
// and maps them back to the base type name.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// pairs := DetectCRUDPairs(spec)
|
||||
// _ = pairs
|
||||
func DetectCRUDPairs(spec *Spec) []CRUDPair {
|
||||
var pairs []CRUDPair
|
||||
for name := range spec.Definitions {
|
||||
|
|
@ -139,7 +176,7 @@ func DetectCRUDPairs(spec *Spec) []CRUDPair {
|
|||
}
|
||||
inner := core.TrimPrefix(name, "Create")
|
||||
inner = core.TrimSuffix(inner, "Option")
|
||||
editName := "Edit" + inner + "Option"
|
||||
editName := core.Concat("Edit", inner, "Option")
|
||||
pair := CRUDPair{Base: inner, Create: name}
|
||||
if _, ok := spec.Definitions[editName]; ok {
|
||||
pair.Edit = editName
|
||||
|
|
@ -147,13 +184,7 @@ func DetectCRUDPairs(spec *Spec) []CRUDPair {
|
|||
pairs = append(pairs, pair)
|
||||
}
|
||||
slices.SortFunc(pairs, func(a, b CRUDPair) int {
|
||||
if a.Base < b.Base {
|
||||
return -1
|
||||
}
|
||||
if a.Base > b.Base {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
return cmp.Compare(a.Base, b.Base)
|
||||
})
|
||||
return pairs
|
||||
}
|
||||
|
|
@ -208,9 +239,7 @@ func resolveGoType(prop SchemaProperty) string {
|
|||
// with common acronyms kept uppercase.
|
||||
func pascalCase(s string) string {
|
||||
var parts []string
|
||||
for p := range strings.FieldsFuncSeq(s, func(r rune) bool {
|
||||
return r == '_' || r == '-'
|
||||
}) {
|
||||
for _, p := range splitSnakeKebab(s) {
|
||||
if len(p) == 0 {
|
||||
continue
|
||||
}
|
||||
|
|
@ -219,8 +248,8 @@ func pascalCase(s string) string {
|
|||
case "ID", "URL", "HTML", "SSH", "HTTP", "HTTPS", "API", "URI", "GPG", "IP", "CSS", "JS":
|
||||
parts = append(parts, upper)
|
||||
default:
|
||||
parts = append(parts, core.Upper(p[:1])+p[1:])
|
||||
parts = append(parts, core.Concat(core.Upper(p[:1]), p[1:]))
|
||||
}
|
||||
}
|
||||
return core.Join("", parts...)
|
||||
return core.Concat(parts...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestParser_Good_LoadSpec(t *testing.T) {
|
||||
func TestParser_LoadSpec_Good(t *testing.T) {
|
||||
spec, err := LoadSpec("../../testdata/swagger.v1.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -17,7 +17,7 @@ func TestParser_Good_LoadSpec(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParser_Good_ExtractTypes(t *testing.T) {
|
||||
func TestParser_ExtractTypes_Good(t *testing.T) {
|
||||
spec, err := LoadSpec("../../testdata/swagger.v1.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -38,7 +38,7 @@ func TestParser_Good_ExtractTypes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParser_Good_FieldTypes(t *testing.T) {
|
||||
func TestParser_FieldTypes_Good(t *testing.T) {
|
||||
spec, err := LoadSpec("../../testdata/swagger.v1.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -74,7 +74,7 @@ func TestParser_Good_FieldTypes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParser_Good_DetectCreateEditPairs(t *testing.T) {
|
||||
func TestParser_DetectCreateEditPairs_Good(t *testing.T) {
|
||||
spec, err := LoadSpec("../../testdata/swagger.v1.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
|||
14
commits.go
14
commits.go
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
|
|
@ -12,6 +11,11 @@ import (
|
|||
// and git notes.
|
||||
// No Resource embedding — collection and item commit paths differ, and the
|
||||
// remaining endpoints are heterogeneous across status and note paths.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Commits.GetCombinedStatus(ctx, "core", "go-forge", "main")
|
||||
type CommitService struct {
|
||||
client *Client
|
||||
}
|
||||
|
|
@ -51,7 +55,7 @@ func (s *CommitService) Get(ctx context.Context, params Params) (*types.Commit,
|
|||
|
||||
// GetCombinedStatus returns the combined status for a given ref (branch, tag, or SHA).
|
||||
func (s *CommitService) GetCombinedStatus(ctx context.Context, owner, repo, ref string) (*types.CombinedStatus, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/statuses/%s", owner, repo, ref)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/statuses/{ref}", pathParams("owner", owner, "repo", repo, "ref", ref))
|
||||
var out types.CombinedStatus
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -61,7 +65,7 @@ func (s *CommitService) GetCombinedStatus(ctx context.Context, owner, repo, ref
|
|||
|
||||
// ListStatuses returns all commit statuses for a given ref.
|
||||
func (s *CommitService) ListStatuses(ctx context.Context, owner, repo, ref string) ([]types.CommitStatus, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/commits/%s/statuses", owner, repo, ref)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/commits/{ref}/statuses", pathParams("owner", owner, "repo", repo, "ref", ref))
|
||||
var out []types.CommitStatus
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -71,7 +75,7 @@ func (s *CommitService) ListStatuses(ctx context.Context, owner, repo, ref strin
|
|||
|
||||
// CreateStatus creates a new commit status for the given SHA.
|
||||
func (s *CommitService) CreateStatus(ctx context.Context, owner, repo, sha string, opts *types.CreateStatusOption) (*types.CommitStatus, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/statuses/%s", owner, repo, sha)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/statuses/{sha}", pathParams("owner", owner, "repo", repo, "sha", sha))
|
||||
var out types.CommitStatus
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -81,7 +85,7 @@ func (s *CommitService) CreateStatus(ctx context.Context, owner, repo, sha strin
|
|||
|
||||
// GetNote returns the git note for a given commit SHA.
|
||||
func (s *CommitService) GetNote(ctx context.Context, owner, repo, sha string) (*types.Note, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/git/notes/%s", owner, repo, sha)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/git/notes/{sha}", pathParams("owner", owner, "repo", repo, "sha", sha))
|
||||
var out types.Note
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestCommitService_Good_List(t *testing.T) {
|
||||
func TestCommitService_List_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -61,7 +61,7 @@ func TestCommitService_Good_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCommitService_Good_Get(t *testing.T) {
|
||||
func TestCommitService_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -95,7 +95,7 @@ func TestCommitService_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCommitService_Good_ListStatuses(t *testing.T) {
|
||||
func TestCommitService_ListStatuses_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -126,7 +126,7 @@ func TestCommitService_Good_ListStatuses(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCommitService_Good_CreateStatus(t *testing.T) {
|
||||
func TestCommitService_CreateStatus_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -169,7 +169,7 @@ func TestCommitService_Good_CreateStatus(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCommitService_Good_GetNote(t *testing.T) {
|
||||
func TestCommitService_GetNote_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -199,7 +199,7 @@ func TestCommitService_Good_GetNote(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCommitService_Good_GetCombinedStatus(t *testing.T) {
|
||||
func TestCommitService_GetCombinedStatus_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -234,7 +234,7 @@ func TestCommitService_Good_GetCombinedStatus(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCommitService_Bad_NotFound(t *testing.T) {
|
||||
func TestCommitService_NotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "not found"})
|
||||
|
|
|
|||
25
config.go
25
config.go
|
|
@ -1,14 +1,18 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultURL is the fallback Forgejo instance URL when neither flag nor
|
||||
// environment variable is set.
|
||||
//
|
||||
// Usage:
|
||||
// cfgURL, _, _ := forge.ResolveConfig("", "")
|
||||
// _ = cfgURL == forge.DefaultURL
|
||||
DefaultURL = "http://localhost:3000"
|
||||
)
|
||||
|
||||
|
|
@ -18,9 +22,15 @@ const (
|
|||
// Environment variables:
|
||||
// - FORGE_URL — base URL of the Forgejo instance
|
||||
// - FORGE_TOKEN — API token for authentication
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// url, token, err := forge.ResolveConfig("", "")
|
||||
// _ = url
|
||||
// _ = token
|
||||
func ResolveConfig(flagURL, flagToken string) (url, token string, err error) {
|
||||
url = os.Getenv("FORGE_URL")
|
||||
token = os.Getenv("FORGE_TOKEN")
|
||||
url, _ = syscall.Getenv("FORGE_URL")
|
||||
token, _ = syscall.Getenv("FORGE_TOKEN")
|
||||
|
||||
if flagURL != "" {
|
||||
url = flagURL
|
||||
|
|
@ -36,13 +46,18 @@ func ResolveConfig(flagURL, flagToken string) (url, token string, err error) {
|
|||
|
||||
// NewForgeFromConfig creates a new Forge client using resolved configuration.
|
||||
// It returns an error if no API token is available from flags or environment.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f, err := forge.NewForgeFromConfig("", "")
|
||||
// _ = f
|
||||
func NewForgeFromConfig(flagURL, flagToken string, opts ...Option) (*Forge, error) {
|
||||
url, token, err := ResolveConfig(flagURL, flagToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token == "" {
|
||||
return nil, coreerr.E("NewForgeFromConfig", "forge: no API token configured (set FORGE_TOKEN or pass --token)", nil)
|
||||
return nil, core.E("NewForgeFromConfig", "forge: no API token configured (set FORGE_TOKEN or pass --token)", nil)
|
||||
}
|
||||
return NewForge(url, token, opts...), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveConfig_Good_EnvOverrides(t *testing.T) {
|
||||
func TestResolveConfig_EnvOverrides_Good(t *testing.T) {
|
||||
t.Setenv("FORGE_URL", "https://forge.example.com")
|
||||
t.Setenv("FORGE_TOKEN", "env-token")
|
||||
|
||||
|
|
@ -21,7 +20,7 @@ func TestResolveConfig_Good_EnvOverrides(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolveConfig_Good_FlagOverridesEnv(t *testing.T) {
|
||||
func TestResolveConfig_FlagOverridesEnv_Good(t *testing.T) {
|
||||
t.Setenv("FORGE_URL", "https://env.example.com")
|
||||
t.Setenv("FORGE_TOKEN", "env-token")
|
||||
|
||||
|
|
@ -37,9 +36,9 @@ func TestResolveConfig_Good_FlagOverridesEnv(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolveConfig_Good_DefaultURL(t *testing.T) {
|
||||
os.Unsetenv("FORGE_URL")
|
||||
os.Unsetenv("FORGE_TOKEN")
|
||||
func TestResolveConfig_DefaultURL_Good(t *testing.T) {
|
||||
t.Setenv("FORGE_URL", "")
|
||||
t.Setenv("FORGE_TOKEN", "")
|
||||
|
||||
url, _, err := ResolveConfig("", "")
|
||||
if err != nil {
|
||||
|
|
@ -50,9 +49,9 @@ func TestResolveConfig_Good_DefaultURL(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewForgeFromConfig_Bad_NoToken(t *testing.T) {
|
||||
os.Unsetenv("FORGE_URL")
|
||||
os.Unsetenv("FORGE_TOKEN")
|
||||
func TestNewForgeFromConfig_NoToken_Bad(t *testing.T) {
|
||||
t.Setenv("FORGE_URL", "")
|
||||
t.Setenv("FORGE_TOKEN", "")
|
||||
|
||||
_, err := NewForgeFromConfig("", "")
|
||||
if err == nil {
|
||||
|
|
|
|||
16
contents.go
16
contents.go
|
|
@ -3,12 +3,16 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// ContentService handles file read/write operations via the Forgejo API.
|
||||
// No Resource embedding — paths vary by operation.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Contents.GetFile(ctx, "core", "go-forge", "README.md")
|
||||
type ContentService struct {
|
||||
client *Client
|
||||
}
|
||||
|
|
@ -19,7 +23,7 @@ func newContentService(c *Client) *ContentService {
|
|||
|
||||
// GetFile returns metadata and content for a file in a repository.
|
||||
func (s *ContentService) GetFile(ctx context.Context, owner, repo, filepath string) (*types.ContentsResponse, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/contents/{filepath}", pathParams("owner", owner, "repo", repo, "filepath", filepath))
|
||||
var out types.ContentsResponse
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -29,7 +33,7 @@ func (s *ContentService) GetFile(ctx context.Context, owner, repo, filepath stri
|
|||
|
||||
// CreateFile creates a new file in a repository.
|
||||
func (s *ContentService) CreateFile(ctx context.Context, owner, repo, filepath string, opts *types.CreateFileOptions) (*types.FileResponse, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/contents/{filepath}", pathParams("owner", owner, "repo", repo, "filepath", filepath))
|
||||
var out types.FileResponse
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -39,7 +43,7 @@ func (s *ContentService) CreateFile(ctx context.Context, owner, repo, filepath s
|
|||
|
||||
// UpdateFile updates an existing file in a repository.
|
||||
func (s *ContentService) UpdateFile(ctx context.Context, owner, repo, filepath string, opts *types.UpdateFileOptions) (*types.FileResponse, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/contents/{filepath}", pathParams("owner", owner, "repo", repo, "filepath", filepath))
|
||||
var out types.FileResponse
|
||||
if err := s.client.Put(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -49,12 +53,12 @@ func (s *ContentService) UpdateFile(ctx context.Context, owner, repo, filepath s
|
|||
|
||||
// DeleteFile deletes a file from a repository. Uses DELETE with a JSON body.
|
||||
func (s *ContentService) DeleteFile(ctx context.Context, owner, repo, filepath string, opts *types.DeleteFileOptions) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/contents/{filepath}", pathParams("owner", owner, "repo", repo, "filepath", filepath))
|
||||
return s.client.DeleteWithBody(ctx, path, opts)
|
||||
}
|
||||
|
||||
// GetRawFile returns the raw file content as bytes.
|
||||
func (s *ContentService) GetRawFile(ctx context.Context, owner, repo, filepath string) ([]byte, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/raw/%s", owner, repo, filepath)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/raw/{filepath}", pathParams("owner", owner, "repo", repo, "filepath", filepath))
|
||||
return s.client.GetRaw(ctx, path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestContentService_Good_GetFile(t *testing.T) {
|
||||
func TestContentService_GetFile_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -49,7 +49,7 @@ func TestContentService_Good_GetFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestContentService_Good_CreateFile(t *testing.T) {
|
||||
func TestContentService_CreateFile_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -98,7 +98,7 @@ func TestContentService_Good_CreateFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestContentService_Good_UpdateFile(t *testing.T) {
|
||||
func TestContentService_UpdateFile_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
t.Errorf("expected PUT, got %s", r.Method)
|
||||
|
|
@ -138,7 +138,7 @@ func TestContentService_Good_UpdateFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestContentService_Good_DeleteFile(t *testing.T) {
|
||||
func TestContentService_DeleteFile_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
|
|
@ -168,7 +168,7 @@ func TestContentService_Good_DeleteFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestContentService_Good_GetRawFile(t *testing.T) {
|
||||
func TestContentService_GetRawFile_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -192,7 +192,7 @@ func TestContentService_Good_GetRawFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestContentService_Bad_NotFound(t *testing.T) {
|
||||
func TestContentService_NotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "file not found"})
|
||||
|
|
@ -209,7 +209,7 @@ func TestContentService_Bad_NotFound(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestContentService_Bad_GetRawNotFound(t *testing.T) {
|
||||
func TestContentService_GetRawNotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "file not found"})
|
||||
|
|
|
|||
10
forge.go
10
forge.go
|
|
@ -1,6 +1,11 @@
|
|||
package forge
|
||||
|
||||
// Forge is the top-level client for the Forgejo API.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _ = f.Repos
|
||||
type Forge struct {
|
||||
client *Client
|
||||
|
||||
|
|
@ -26,6 +31,11 @@ type Forge struct {
|
|||
}
|
||||
|
||||
// NewForge creates a new Forge client.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _ = f
|
||||
func NewForge(url, token string, opts ...Option) *Forge {
|
||||
c := NewClient(url, token, opts...)
|
||||
f := &Forge{client: c}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestForge_Good_NewForge(t *testing.T) {
|
||||
func TestForge_NewForge_Good(t *testing.T) {
|
||||
f := NewForge("https://forge.lthn.ai", "tok")
|
||||
if f.Repos == nil {
|
||||
t.Fatal("Repos service is nil")
|
||||
|
|
@ -20,7 +20,7 @@ func TestForge_Good_NewForge(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestForge_Good_Client(t *testing.T) {
|
||||
func TestForge_Client_Good(t *testing.T) {
|
||||
f := NewForge("https://forge.lthn.ai", "tok")
|
||||
c := f.Client()
|
||||
if c == nil {
|
||||
|
|
@ -31,7 +31,7 @@ func TestForge_Good_Client(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRepoService_Good_ListOrgRepos(t *testing.T) {
|
||||
func TestRepoService_ListOrgRepos_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -56,7 +56,7 @@ func TestRepoService_Good_ListOrgRepos(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRepoService_Good_Get(t *testing.T) {
|
||||
func TestRepoService_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
|
|
@ -77,7 +77,7 @@ func TestRepoService_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRepoService_Good_Update(t *testing.T) {
|
||||
func TestRepoService_Update_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPatch {
|
||||
t.Errorf("expected PATCH, got %s", r.Method)
|
||||
|
|
@ -105,7 +105,7 @@ func TestRepoService_Good_Update(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRepoService_Good_Delete(t *testing.T) {
|
||||
func TestRepoService_Delete_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
|
|
@ -125,7 +125,7 @@ func TestRepoService_Good_Delete(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRepoService_Bad_Get(t *testing.T) {
|
||||
func TestRepoService_Get_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "not found"})
|
||||
|
|
@ -138,7 +138,7 @@ func TestRepoService_Bad_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRepoService_Good_Fork(t *testing.T) {
|
||||
func TestRepoService_Fork_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -3,9 +3,9 @@ module dappco.re/go/core/forge
|
|||
go 1.26.0
|
||||
|
||||
require (
|
||||
dappco.re/go/core v0.8.0-alpha.1
|
||||
dappco.re/go/core v0.4.7
|
||||
dappco.re/go/core/io v0.2.0
|
||||
dappco.re/go/core/log v0.1.0
|
||||
github.com/goccy/go-json v0.10.6
|
||||
)
|
||||
|
||||
require forge.lthn.ai/core/go-log v0.0.4 // indirect
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -1,13 +1,13 @@
|
|||
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
dappco.re/go/core v0.4.7 h1:KmIA/2lo6rl1NMtLrKqCWfMlUqpDZYH3q0/d10dTtGA=
|
||||
dappco.re/go/core v0.4.7/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
|
||||
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
|
||||
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
||||
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
||||
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
|
||||
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
|
|
|
|||
35
helpers.go
Normal file
35
helpers.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package forge
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
func trimTrailingSlashes(s string) string {
|
||||
for core.HasSuffix(s, "/") {
|
||||
s = core.TrimSuffix(s, "/")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func int64String(v int64) string {
|
||||
return strconv.FormatInt(v, 10)
|
||||
}
|
||||
|
||||
func pathParams(values ...string) Params {
|
||||
params := make(Params, len(values)/2)
|
||||
for i := 0; i+1 < len(values); i += 2 {
|
||||
params[values[i]] = values[i+1]
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func lastIndexByte(s string, b byte) int {
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == b {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
30
issues.go
30
issues.go
|
|
@ -4,11 +4,15 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// IssueService handles issue operations within a repository.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Issues.ListAll(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
|
||||
type IssueService struct {
|
||||
Resource[types.Issue, types.CreateIssueOption, types.EditIssueOption]
|
||||
}
|
||||
|
|
@ -23,77 +27,77 @@ func newIssueService(c *Client) *IssueService {
|
|||
|
||||
// Pin pins an issue.
|
||||
func (s *IssueService) Pin(ctx context.Context, owner, repo string, index int64) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/pin", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// Unpin unpins an issue.
|
||||
func (s *IssueService) Unpin(ctx context.Context, owner, repo string, index int64) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/pin", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// SetDeadline sets or updates the deadline on an issue.
|
||||
func (s *IssueService) SetDeadline(ctx context.Context, owner, repo string, index int64, deadline string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/deadline", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/deadline", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
body := map[string]string{"due_date": deadline}
|
||||
return s.client.Post(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// AddReaction adds a reaction to an issue.
|
||||
func (s *IssueService) AddReaction(ctx context.Context, owner, repo string, index int64, reaction string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/reactions", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
body := map[string]string{"content": reaction}
|
||||
return s.client.Post(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// DeleteReaction removes a reaction from an issue.
|
||||
func (s *IssueService) DeleteReaction(ctx context.Context, owner, repo string, index int64, reaction string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/reactions", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
body := map[string]string{"content": reaction}
|
||||
return s.client.DeleteWithBody(ctx, path, body)
|
||||
}
|
||||
|
||||
// StartStopwatch starts the stopwatch on an issue.
|
||||
func (s *IssueService) StartStopwatch(ctx context.Context, owner, repo string, index int64) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/stopwatch/start", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/stopwatch/start", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// StopStopwatch stops the stopwatch on an issue.
|
||||
func (s *IssueService) StopStopwatch(ctx context.Context, owner, repo string, index int64) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/stopwatch/stop", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/stopwatch/stop", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// AddLabels adds labels to an issue.
|
||||
func (s *IssueService) AddLabels(ctx context.Context, owner, repo string, index int64, labelIDs []int64) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/labels", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
body := types.IssueLabelsOption{Labels: toAnySlice(labelIDs)}
|
||||
return s.client.Post(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// RemoveLabel removes a single label from an issue.
|
||||
func (s *IssueService) RemoveLabel(ctx context.Context, owner, repo string, index int64, labelID int64) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels/%d", owner, repo, index, labelID)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/labels/{labelID}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "labelID", int64String(labelID)))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListComments returns all comments on an issue.
|
||||
func (s *IssueService) ListComments(ctx context.Context, owner, repo string, index int64) ([]types.Comment, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/comments", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
return ListAll[types.Comment](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterComments returns an iterator over all comments on an issue.
|
||||
func (s *IssueService) IterComments(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.Comment, error] {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/comments", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
return ListIter[types.Comment](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// CreateComment creates a comment on an issue.
|
||||
func (s *IssueService) CreateComment(ctx context.Context, owner, repo string, index int64, body string) (*types.Comment, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/comments", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
opts := types.CreateIssueCommentOption{Body: body}
|
||||
var out types.Comment
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestIssueService_Good_List(t *testing.T) {
|
||||
func TestIssueService_List_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -41,7 +41,7 @@ func TestIssueService_Good_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Good_Get(t *testing.T) {
|
||||
func TestIssueService_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -63,7 +63,7 @@ func TestIssueService_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Good_Create(t *testing.T) {
|
||||
func TestIssueService_Create_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -93,7 +93,7 @@ func TestIssueService_Good_Create(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Good_Update(t *testing.T) {
|
||||
func TestIssueService_Update_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPatch {
|
||||
t.Errorf("expected PATCH, got %s", r.Method)
|
||||
|
|
@ -121,7 +121,7 @@ func TestIssueService_Good_Update(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Good_Delete(t *testing.T) {
|
||||
func TestIssueService_Delete_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
|
|
@ -141,7 +141,7 @@ func TestIssueService_Good_Delete(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Good_CreateComment(t *testing.T) {
|
||||
func TestIssueService_CreateComment_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -168,7 +168,7 @@ func TestIssueService_Good_CreateComment(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Good_Pin(t *testing.T) {
|
||||
func TestIssueService_Pin_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -187,7 +187,7 @@ func TestIssueService_Good_Pin(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Bad_List(t *testing.T) {
|
||||
func TestIssueService_List_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "boom"})
|
||||
|
|
@ -200,7 +200,7 @@ func TestIssueService_Bad_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Ugly_ListIgnoresIndexParam(t *testing.T) {
|
||||
func TestIssueService_ListIgnoresIndexParam_Ugly(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge/issues" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
|
|
|
|||
24
labels.go
24
labels.go
|
|
@ -4,12 +4,16 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// LabelService handles repository labels, organisation labels, and issue labels.
|
||||
// No Resource embedding — paths are heterogeneous.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Labels.ListRepoLabels(ctx, "core", "go-forge")
|
||||
type LabelService struct {
|
||||
client *Client
|
||||
}
|
||||
|
|
@ -20,19 +24,19 @@ func newLabelService(c *Client) *LabelService {
|
|||
|
||||
// ListRepoLabels returns all labels for a repository.
|
||||
func (s *LabelService) ListRepoLabels(ctx context.Context, owner, repo string) ([]types.Label, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels", pathParams("owner", owner, "repo", repo))
|
||||
return ListAll[types.Label](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterRepoLabels returns an iterator over all labels for a repository.
|
||||
func (s *LabelService) IterRepoLabels(ctx context.Context, owner, repo string) iter.Seq2[types.Label, error] {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels", pathParams("owner", owner, "repo", repo))
|
||||
return ListIter[types.Label](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// GetRepoLabel returns a single label by ID.
|
||||
func (s *LabelService) GetRepoLabel(ctx context.Context, owner, repo string, id int64) (*types.Label, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
|
||||
var out types.Label
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -42,7 +46,7 @@ func (s *LabelService) GetRepoLabel(ctx context.Context, owner, repo string, id
|
|||
|
||||
// CreateRepoLabel creates a new label in a repository.
|
||||
func (s *LabelService) CreateRepoLabel(ctx context.Context, owner, repo string, opts *types.CreateLabelOption) (*types.Label, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels", pathParams("owner", owner, "repo", repo))
|
||||
var out types.Label
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -52,7 +56,7 @@ func (s *LabelService) CreateRepoLabel(ctx context.Context, owner, repo string,
|
|||
|
||||
// EditRepoLabel updates an existing label in a repository.
|
||||
func (s *LabelService) EditRepoLabel(ctx context.Context, owner, repo string, id int64, opts *types.EditLabelOption) (*types.Label, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
|
||||
var out types.Label
|
||||
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -62,25 +66,25 @@ func (s *LabelService) EditRepoLabel(ctx context.Context, owner, repo string, id
|
|||
|
||||
// DeleteRepoLabel deletes a label from a repository.
|
||||
func (s *LabelService) DeleteRepoLabel(ctx context.Context, owner, repo string, id int64) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListOrgLabels returns all labels for an organisation.
|
||||
func (s *LabelService) ListOrgLabels(ctx context.Context, org string) ([]types.Label, error) {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/labels", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/labels", pathParams("org", org))
|
||||
return ListAll[types.Label](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterOrgLabels returns an iterator over all labels for an organisation.
|
||||
func (s *LabelService) IterOrgLabels(ctx context.Context, org string) iter.Seq2[types.Label, error] {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/labels", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/labels", pathParams("org", org))
|
||||
return ListIter[types.Label](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// CreateOrgLabel creates a new label in an organisation.
|
||||
func (s *LabelService) CreateOrgLabel(ctx context.Context, org string, opts *types.CreateLabelOption) (*types.Label, error) {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/labels", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/labels", pathParams("org", org))
|
||||
var out types.Label
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestLabelService_Good_ListRepoLabels(t *testing.T) {
|
||||
func TestLabelService_ListRepoLabels_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -42,7 +42,7 @@ func TestLabelService_Good_ListRepoLabels(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_CreateRepoLabel(t *testing.T) {
|
||||
func TestLabelService_CreateRepoLabel_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -84,7 +84,7 @@ func TestLabelService_Good_CreateRepoLabel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_GetRepoLabel(t *testing.T) {
|
||||
func TestLabelService_GetRepoLabel_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -106,7 +106,7 @@ func TestLabelService_Good_GetRepoLabel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_EditRepoLabel(t *testing.T) {
|
||||
func TestLabelService_EditRepoLabel_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPatch {
|
||||
t.Errorf("expected PATCH, got %s", r.Method)
|
||||
|
|
@ -135,7 +135,7 @@ func TestLabelService_Good_EditRepoLabel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_DeleteRepoLabel(t *testing.T) {
|
||||
func TestLabelService_DeleteRepoLabel_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
|
|
@ -154,7 +154,7 @@ func TestLabelService_Good_DeleteRepoLabel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_ListOrgLabels(t *testing.T) {
|
||||
func TestLabelService_ListOrgLabels_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -182,7 +182,7 @@ func TestLabelService_Good_ListOrgLabels(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Good_CreateOrgLabel(t *testing.T) {
|
||||
func TestLabelService_CreateOrgLabel_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -214,7 +214,7 @@ func TestLabelService_Good_CreateOrgLabel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLabelService_Bad_NotFound(t *testing.T) {
|
||||
func TestLabelService_NotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "label not found"})
|
||||
|
|
|
|||
|
|
@ -3,11 +3,15 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// MilestoneService handles repository milestones.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Milestones.ListAll(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
|
||||
type MilestoneService struct {
|
||||
client *Client
|
||||
}
|
||||
|
|
@ -18,13 +22,13 @@ func newMilestoneService(c *Client) *MilestoneService {
|
|||
|
||||
// ListAll returns all milestones for a repository.
|
||||
func (s *MilestoneService) ListAll(ctx context.Context, params Params) ([]types.Milestone, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/milestones", params["owner"], params["repo"])
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones", params)
|
||||
return ListAll[types.Milestone](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// Get returns a single milestone by ID.
|
||||
func (s *MilestoneService) Get(ctx context.Context, owner, repo string, id int64) (*types.Milestone, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/milestones/%d", owner, repo, id)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
|
||||
var out types.Milestone
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -34,7 +38,7 @@ func (s *MilestoneService) Get(ctx context.Context, owner, repo string, id int64
|
|||
|
||||
// Create creates a new milestone.
|
||||
func (s *MilestoneService) Create(ctx context.Context, owner, repo string, opts *types.CreateMilestoneOption) (*types.Milestone, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/milestones", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones", pathParams("owner", owner, "repo", repo))
|
||||
var out types.Milestone
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
10
misc.go
10
misc.go
|
|
@ -3,7 +3,6 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
|
|
@ -11,6 +10,11 @@ import (
|
|||
// markdown rendering, licence templates, gitignore templates, and
|
||||
// server metadata.
|
||||
// No Resource embedding — heterogeneous read-only endpoints.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Misc.GetVersion(ctx)
|
||||
type MiscService struct {
|
||||
client *Client
|
||||
}
|
||||
|
|
@ -41,7 +45,7 @@ func (s *MiscService) ListLicenses(ctx context.Context) ([]types.LicensesTemplat
|
|||
|
||||
// GetLicense returns a single licence template by name.
|
||||
func (s *MiscService) GetLicense(ctx context.Context, name string) (*types.LicenseTemplateInfo, error) {
|
||||
path := core.Sprintf("/api/v1/licenses/%s", name)
|
||||
path := ResolvePath("/api/v1/licenses/{name}", pathParams("name", name))
|
||||
var out types.LicenseTemplateInfo
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -60,7 +64,7 @@ func (s *MiscService) ListGitignoreTemplates(ctx context.Context) ([]string, err
|
|||
|
||||
// GetGitignoreTemplate returns a single gitignore template by name.
|
||||
func (s *MiscService) GetGitignoreTemplate(ctx context.Context, name string) (*types.GitignoreTemplateInfo, error) {
|
||||
path := core.Sprintf("/api/v1/gitignore/templates/%s", name)
|
||||
path := ResolvePath("/api/v1/gitignore/templates/{name}", pathParams("name", name))
|
||||
var out types.GitignoreTemplateInfo
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
18
misc_test.go
18
misc_test.go
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestMiscService_Good_RenderMarkdown(t *testing.T) {
|
||||
func TestMiscService_RenderMarkdown_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -44,7 +44,7 @@ func TestMiscService_Good_RenderMarkdown(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_GetVersion(t *testing.T) {
|
||||
func TestMiscService_GetVersion_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -68,7 +68,7 @@ func TestMiscService_Good_GetVersion(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_ListLicenses(t *testing.T) {
|
||||
func TestMiscService_ListLicenses_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -99,7 +99,7 @@ func TestMiscService_Good_ListLicenses(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_GetLicense(t *testing.T) {
|
||||
func TestMiscService_GetLicense_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -128,7 +128,7 @@ func TestMiscService_Good_GetLicense(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_ListGitignoreTemplates(t *testing.T) {
|
||||
func TestMiscService_ListGitignoreTemplates_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -153,7 +153,7 @@ func TestMiscService_Good_ListGitignoreTemplates(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_GetGitignoreTemplate(t *testing.T) {
|
||||
func TestMiscService_GetGitignoreTemplate_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -178,7 +178,7 @@ func TestMiscService_Good_GetGitignoreTemplate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Good_GetNodeInfo(t *testing.T) {
|
||||
func TestMiscService_GetNodeInfo_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -209,7 +209,7 @@ func TestMiscService_Good_GetNodeInfo(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMiscService_Bad_NotFound(t *testing.T) {
|
||||
func TestMiscService_NotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "not found"})
|
||||
|
|
|
|||
|
|
@ -4,12 +4,16 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// NotificationService handles notification operations via the Forgejo API.
|
||||
// No Resource embedding — varied endpoint shapes.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Notifications.List(ctx)
|
||||
type NotificationService struct {
|
||||
client *Client
|
||||
}
|
||||
|
|
@ -30,13 +34,13 @@ func (s *NotificationService) Iter(ctx context.Context) iter.Seq2[types.Notifica
|
|||
|
||||
// ListRepo returns all notifications for a specific repository.
|
||||
func (s *NotificationService) ListRepo(ctx context.Context, owner, repo string) ([]types.NotificationThread, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/notifications", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/notifications", pathParams("owner", owner, "repo", repo))
|
||||
return ListAll[types.NotificationThread](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterRepo returns an iterator over all notifications for a specific repository.
|
||||
func (s *NotificationService) IterRepo(ctx context.Context, owner, repo string) iter.Seq2[types.NotificationThread, error] {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/notifications", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/notifications", pathParams("owner", owner, "repo", repo))
|
||||
return ListIter[types.NotificationThread](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +51,7 @@ func (s *NotificationService) MarkRead(ctx context.Context) error {
|
|||
|
||||
// GetThread returns a single notification thread by ID.
|
||||
func (s *NotificationService) GetThread(ctx context.Context, id int64) (*types.NotificationThread, error) {
|
||||
path := core.Sprintf("/api/v1/notifications/threads/%d", id)
|
||||
path := ResolvePath("/api/v1/notifications/threads/{id}", pathParams("id", int64String(id)))
|
||||
var out types.NotificationThread
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -57,6 +61,6 @@ func (s *NotificationService) GetThread(ctx context.Context, id int64) (*types.N
|
|||
|
||||
// MarkThreadRead marks a single notification thread as read.
|
||||
func (s *NotificationService) MarkThreadRead(ctx context.Context, id int64) error {
|
||||
path := core.Sprintf("/api/v1/notifications/threads/%d", id)
|
||||
path := ResolvePath("/api/v1/notifications/threads/{id}", pathParams("id", int64String(id)))
|
||||
return s.client.Patch(ctx, path, nil, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestNotificationService_Good_List(t *testing.T) {
|
||||
func TestNotificationService_List_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -45,7 +45,7 @@ func TestNotificationService_Good_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNotificationService_Good_ListRepo(t *testing.T) {
|
||||
func TestNotificationService_ListRepo_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -73,7 +73,7 @@ func TestNotificationService_Good_ListRepo(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNotificationService_Good_GetThread(t *testing.T) {
|
||||
func TestNotificationService_GetThread_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -107,7 +107,7 @@ func TestNotificationService_Good_GetThread(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNotificationService_Good_MarkRead(t *testing.T) {
|
||||
func TestNotificationService_MarkRead_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
t.Errorf("expected PUT, got %s", r.Method)
|
||||
|
|
@ -126,7 +126,7 @@ func TestNotificationService_Good_MarkRead(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNotificationService_Good_MarkThreadRead(t *testing.T) {
|
||||
func TestNotificationService_MarkThreadRead_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPatch {
|
||||
t.Errorf("expected PATCH, got %s", r.Method)
|
||||
|
|
@ -145,7 +145,7 @@ func TestNotificationService_Good_MarkThreadRead(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNotificationService_Bad_NotFound(t *testing.T) {
|
||||
func TestNotificationService_NotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "thread not found"})
|
||||
|
|
|
|||
18
orgs.go
18
orgs.go
|
|
@ -4,11 +4,15 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// OrgService handles organisation operations.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Orgs.ListMembers(ctx, "core")
|
||||
type OrgService struct {
|
||||
Resource[types.Organization, types.CreateOrgOption, types.EditOrgOption]
|
||||
}
|
||||
|
|
@ -23,37 +27,37 @@ func newOrgService(c *Client) *OrgService {
|
|||
|
||||
// ListMembers returns all members of an organisation.
|
||||
func (s *OrgService) ListMembers(ctx context.Context, org string) ([]types.User, error) {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/members", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/members", pathParams("org", org))
|
||||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterMembers returns an iterator over all members of an organisation.
|
||||
func (s *OrgService) IterMembers(ctx context.Context, org string) iter.Seq2[types.User, error] {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/members", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/members", pathParams("org", org))
|
||||
return ListIter[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// AddMember adds a user to an organisation.
|
||||
func (s *OrgService) AddMember(ctx context.Context, org, username string) error {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/members/%s", org, username)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/members/{username}", pathParams("org", org, "username", username))
|
||||
return s.client.Put(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// RemoveMember removes a user from an organisation.
|
||||
func (s *OrgService) RemoveMember(ctx context.Context, org, username string) error {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/members/%s", org, username)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/members/{username}", pathParams("org", org, "username", username))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListUserOrgs returns all organisations for a user.
|
||||
func (s *OrgService) ListUserOrgs(ctx context.Context, username string) ([]types.Organization, error) {
|
||||
path := core.Sprintf("/api/v1/users/%s/orgs", username)
|
||||
path := ResolvePath("/api/v1/users/{username}/orgs", pathParams("username", username))
|
||||
return ListAll[types.Organization](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterUserOrgs returns an iterator over all organisations for a user.
|
||||
func (s *OrgService) IterUserOrgs(ctx context.Context, username string) iter.Seq2[types.Organization, error] {
|
||||
path := core.Sprintf("/api/v1/users/%s/orgs", username)
|
||||
path := ResolvePath("/api/v1/users/{username}/orgs", pathParams("username", username))
|
||||
return ListIter[types.Organization](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestOrgService_Good_List(t *testing.T) {
|
||||
func TestOrgService_List_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -36,7 +36,7 @@ func TestOrgService_Good_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOrgService_Good_Get(t *testing.T) {
|
||||
func TestOrgService_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -58,7 +58,7 @@ func TestOrgService_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOrgService_Good_ListMembers(t *testing.T) {
|
||||
func TestOrgService_ListMembers_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
|
|||
18
packages.go
18
packages.go
|
|
@ -4,12 +4,16 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// PackageService handles package registry operations via the Forgejo API.
|
||||
// No Resource embedding — paths vary by operation.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Packages.List(ctx, "core")
|
||||
type PackageService struct {
|
||||
client *Client
|
||||
}
|
||||
|
|
@ -20,19 +24,19 @@ func newPackageService(c *Client) *PackageService {
|
|||
|
||||
// List returns all packages for a given owner.
|
||||
func (s *PackageService) List(ctx context.Context, owner string) ([]types.Package, error) {
|
||||
path := core.Sprintf("/api/v1/packages/%s", owner)
|
||||
path := ResolvePath("/api/v1/packages/{owner}", pathParams("owner", owner))
|
||||
return ListAll[types.Package](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// Iter returns an iterator over all packages for a given owner.
|
||||
func (s *PackageService) Iter(ctx context.Context, owner string) iter.Seq2[types.Package, error] {
|
||||
path := core.Sprintf("/api/v1/packages/%s", owner)
|
||||
path := ResolvePath("/api/v1/packages/{owner}", pathParams("owner", owner))
|
||||
return ListIter[types.Package](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// Get returns a single package by owner, type, name, and version.
|
||||
func (s *PackageService) Get(ctx context.Context, owner, pkgType, name, version string) (*types.Package, error) {
|
||||
path := core.Sprintf("/api/v1/packages/%s/%s/%s/%s", owner, pkgType, name, version)
|
||||
path := ResolvePath("/api/v1/packages/{owner}/{pkgType}/{name}/{version}", pathParams("owner", owner, "pkgType", pkgType, "name", name, "version", version))
|
||||
var out types.Package
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -42,18 +46,18 @@ func (s *PackageService) Get(ctx context.Context, owner, pkgType, name, version
|
|||
|
||||
// Delete removes a package by owner, type, name, and version.
|
||||
func (s *PackageService) Delete(ctx context.Context, owner, pkgType, name, version string) error {
|
||||
path := core.Sprintf("/api/v1/packages/%s/%s/%s/%s", owner, pkgType, name, version)
|
||||
path := ResolvePath("/api/v1/packages/{owner}/{pkgType}/{name}/{version}", pathParams("owner", owner, "pkgType", pkgType, "name", name, "version", version))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListFiles returns all files for a specific package version.
|
||||
func (s *PackageService) ListFiles(ctx context.Context, owner, pkgType, name, version string) ([]types.PackageFile, error) {
|
||||
path := core.Sprintf("/api/v1/packages/%s/%s/%s/%s/files", owner, pkgType, name, version)
|
||||
path := ResolvePath("/api/v1/packages/{owner}/{pkgType}/{name}/{version}/files", pathParams("owner", owner, "pkgType", pkgType, "name", name, "version", version))
|
||||
return ListAll[types.PackageFile](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterFiles returns an iterator over all files for a specific package version.
|
||||
func (s *PackageService) IterFiles(ctx context.Context, owner, pkgType, name, version string) iter.Seq2[types.PackageFile, error] {
|
||||
path := core.Sprintf("/api/v1/packages/%s/%s/%s/%s/files", owner, pkgType, name, version)
|
||||
path := ResolvePath("/api/v1/packages/{owner}/{pkgType}/{name}/{version}/files", pathParams("owner", owner, "pkgType", pkgType, "name", name, "version", version))
|
||||
return ListIter[types.PackageFile](ctx, s.client, path, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestPackageService_Good_List(t *testing.T) {
|
||||
func TestPackageService_List_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -42,7 +42,7 @@ func TestPackageService_Good_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPackageService_Good_Get(t *testing.T) {
|
||||
func TestPackageService_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -75,7 +75,7 @@ func TestPackageService_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPackageService_Good_Delete(t *testing.T) {
|
||||
func TestPackageService_Delete_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
|
|
@ -94,7 +94,7 @@ func TestPackageService_Good_Delete(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPackageService_Good_ListFiles(t *testing.T) {
|
||||
func TestPackageService_ListFiles_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -125,7 +125,7 @@ func TestPackageService_Good_ListFiles(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPackageService_Bad_NotFound(t *testing.T) {
|
||||
func TestPackageService_NotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "package not found"})
|
||||
|
|
|
|||
|
|
@ -7,19 +7,34 @@ import (
|
|||
"net/url"
|
||||
"strconv"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
// ListOptions controls pagination.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// opts := forge.ListOptions{Page: 1, Limit: 50}
|
||||
// _ = opts
|
||||
type ListOptions struct {
|
||||
Page int // 1-based page number
|
||||
Limit int // items per page (default 50)
|
||||
}
|
||||
|
||||
// DefaultList returns sensible default pagination.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// page, err := forge.ListPage[types.Repository](ctx, client, path, nil, forge.DefaultList)
|
||||
// _ = page
|
||||
var DefaultList = ListOptions{Page: 1, Limit: 50}
|
||||
|
||||
// PagedResult holds a single page of results with metadata.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// page, err := forge.ListPage[types.Repository](ctx, client, path, nil, forge.DefaultList)
|
||||
// _ = page
|
||||
type PagedResult[T any] struct {
|
||||
Items []T
|
||||
TotalCount int
|
||||
|
|
@ -29,6 +44,11 @@ type PagedResult[T any] struct {
|
|||
|
||||
// ListPage fetches a single page of results.
|
||||
// Extra query params can be passed via the query map.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// page, err := forge.ListPage[types.Repository](ctx, client, "/api/v1/user/repos", nil, forge.DefaultList)
|
||||
// _ = page
|
||||
func ListPage[T any](ctx context.Context, c *Client, path string, query map[string]string, opts ListOptions) (*PagedResult[T], error) {
|
||||
if opts.Page < 1 {
|
||||
opts.Page = 1
|
||||
|
|
@ -39,7 +59,7 @@ func ListPage[T any](ctx context.Context, c *Client, path string, query map[stri
|
|||
|
||||
u, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("ListPage", "forge: parse path", err)
|
||||
return nil, core.E("ListPage", "forge: parse path", err)
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
|
|
@ -70,6 +90,11 @@ func ListPage[T any](ctx context.Context, c *Client, path string, query map[stri
|
|||
}
|
||||
|
||||
// ListAll fetches all pages of results.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// items, err := forge.ListAll[types.Repository](ctx, client, "/api/v1/user/repos", nil)
|
||||
// _ = items
|
||||
func ListAll[T any](ctx context.Context, c *Client, path string, query map[string]string) ([]T, error) {
|
||||
var all []T
|
||||
page := 1
|
||||
|
|
@ -90,6 +115,12 @@ func ListAll[T any](ctx context.Context, c *Client, path string, query map[strin
|
|||
}
|
||||
|
||||
// ListIter returns an iterator over all resources across all pages.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// for item, err := range forge.ListIter[types.Repository](ctx, client, "/api/v1/user/repos", nil) {
|
||||
// _, _ = item, err
|
||||
// }
|
||||
func ListIter[T any](ctx context.Context, c *Client, path string, query map[string]string) iter.Seq2[T, error] {
|
||||
return func(yield func(T, error) bool) {
|
||||
page := 1
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPagination_Good_SinglePage(t *testing.T) {
|
||||
func TestPagination_SinglePage_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Total-Count", "2")
|
||||
json.NewEncoder(w).Encode([]map[string]int{{"id": 1}, {"id": 2}})
|
||||
|
|
@ -25,7 +25,7 @@ func TestPagination_Good_SinglePage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPagination_Good_MultiPage(t *testing.T) {
|
||||
func TestPagination_MultiPage_Good(t *testing.T) {
|
||||
page := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
page++
|
||||
|
|
@ -48,7 +48,7 @@ func TestPagination_Good_MultiPage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPagination_Good_EmptyResult(t *testing.T) {
|
||||
func TestPagination_EmptyResult_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Total-Count", "0")
|
||||
json.NewEncoder(w).Encode([]map[string]int{})
|
||||
|
|
@ -65,7 +65,7 @@ func TestPagination_Good_EmptyResult(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPagination_Good_Iter(t *testing.T) {
|
||||
func TestPagination_Iter_Good(t *testing.T) {
|
||||
page := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
page++
|
||||
|
|
@ -95,7 +95,7 @@ func TestPagination_Good_Iter(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestListPage_Good_QueryParams(t *testing.T) {
|
||||
func TestListPage_QueryParams_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.URL.Query().Get("page")
|
||||
l := r.URL.Query().Get("limit")
|
||||
|
|
@ -116,7 +116,7 @@ func TestListPage_Good_QueryParams(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPagination_Bad_ServerError(t *testing.T) {
|
||||
func TestPagination_ServerError_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(500)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "fail"})
|
||||
|
|
|
|||
10
params.go
10
params.go
|
|
@ -8,9 +8,19 @@ import (
|
|||
|
||||
// Params maps path variable names to values.
|
||||
// Example: Params{"owner": "core", "repo": "go-forge"}
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// params := forge.Params{"owner": "core", "repo": "go-forge"}
|
||||
// _ = params
|
||||
type Params map[string]string
|
||||
|
||||
// ResolvePath substitutes {placeholders} in path with values from params.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// path := forge.ResolvePath("/api/v1/repos/{owner}/{repo}", forge.Params{"owner": "core", "repo": "go-forge"})
|
||||
// _ = path
|
||||
func ResolvePath(path string, params Params) string {
|
||||
for k, v := range params {
|
||||
path = core.Replace(path, "{"+k+"}", url.PathEscape(v))
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import "testing"
|
||||
|
||||
func TestResolvePath_Good_Simple(t *testing.T) {
|
||||
func TestResolvePath_Simple_Good(t *testing.T) {
|
||||
got := ResolvePath("/api/v1/repos/{owner}/{repo}", Params{"owner": "core", "repo": "go-forge"})
|
||||
want := "/api/v1/repos/core/go-forge"
|
||||
if got != want {
|
||||
|
|
@ -10,14 +10,14 @@ func TestResolvePath_Good_Simple(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolvePath_Good_NoParams(t *testing.T) {
|
||||
func TestResolvePath_NoParams_Good(t *testing.T) {
|
||||
got := ResolvePath("/api/v1/user", nil)
|
||||
if got != "/api/v1/user" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePath_Good_WithID(t *testing.T) {
|
||||
func TestResolvePath_WithID_Good(t *testing.T) {
|
||||
got := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}", Params{
|
||||
"owner": "core", "repo": "go-forge", "index": "42",
|
||||
})
|
||||
|
|
@ -27,7 +27,7 @@ func TestResolvePath_Good_WithID(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolvePath_Good_URLEncoding(t *testing.T) {
|
||||
func TestResolvePath_URLEncoding_Good(t *testing.T) {
|
||||
got := ResolvePath("/api/v1/repos/{owner}/{repo}", Params{"owner": "my org", "repo": "my repo"})
|
||||
want := "/api/v1/repos/my%20org/my%20repo"
|
||||
if got != want {
|
||||
|
|
|
|||
20
pulls.go
20
pulls.go
|
|
@ -4,11 +4,15 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// PullService handles pull request operations within a repository.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Pulls.ListAll(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
|
||||
type PullService struct {
|
||||
Resource[types.PullRequest, types.CreatePullRequestOption, types.EditPullRequestOption]
|
||||
}
|
||||
|
|
@ -23,32 +27,32 @@ func newPullService(c *Client) *PullService {
|
|||
|
||||
// Merge merges a pull request. Method is one of "merge", "rebase", "rebase-merge", "squash", "fast-forward-only", "manually-merged".
|
||||
func (s *PullService) Merge(ctx context.Context, owner, repo string, index int64, method string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/merge", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
body := map[string]string{"Do": method}
|
||||
return s.client.Post(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// Update updates a pull request branch with the base branch.
|
||||
func (s *PullService) Update(ctx context.Context, owner, repo string, index int64) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/pulls/%d/update", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/update", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// ListReviews returns all reviews on a pull request.
|
||||
func (s *PullService) ListReviews(ctx context.Context, owner, repo string, index int64) ([]types.PullReview, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
return ListAll[types.PullReview](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterReviews returns an iterator over all reviews on a pull request.
|
||||
func (s *PullService) IterReviews(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.PullReview, error] {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
return ListIter[types.PullReview](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// SubmitReview creates a new review on a pull request.
|
||||
func (s *PullService) SubmitReview(ctx context.Context, owner, repo string, index int64, review map[string]any) (*types.PullReview, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
|
||||
var out types.PullReview
|
||||
if err := s.client.Post(ctx, path, review, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -58,13 +62,13 @@ func (s *PullService) SubmitReview(ctx context.Context, owner, repo string, inde
|
|||
|
||||
// DismissReview dismisses a pull request review.
|
||||
func (s *PullService) DismissReview(ctx context.Context, owner, repo string, index, reviewID int64, msg string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/dismissals", owner, repo, index, reviewID)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{reviewID}/dismissals", pathParams("owner", owner, "repo", repo, "index", int64String(index), "reviewID", int64String(reviewID)))
|
||||
body := map[string]string{"message": msg}
|
||||
return s.client.Post(ctx, path, body, nil)
|
||||
}
|
||||
|
||||
// UndismissReview undismisses a pull request review.
|
||||
func (s *PullService) UndismissReview(ctx context.Context, owner, repo string, index, reviewID int64) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/undismissals", owner, repo, index, reviewID)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{reviewID}/undismissals", pathParams("owner", owner, "repo", repo, "index", int64String(index), "reviewID", int64String(reviewID)))
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestPullService_Good_List(t *testing.T) {
|
||||
func TestPullService_List_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -41,7 +41,7 @@ func TestPullService_Good_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPullService_Good_Get(t *testing.T) {
|
||||
func TestPullService_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -63,7 +63,7 @@ func TestPullService_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPullService_Good_Create(t *testing.T) {
|
||||
func TestPullService_Create_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -94,7 +94,7 @@ func TestPullService_Good_Create(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPullService_Good_Merge(t *testing.T) {
|
||||
func TestPullService_Merge_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -118,7 +118,7 @@ func TestPullService_Good_Merge(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPullService_Bad_Merge(t *testing.T) {
|
||||
func TestPullService_Merge_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "already merged"})
|
||||
|
|
|
|||
18
releases.go
18
releases.go
|
|
@ -4,11 +4,15 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// ReleaseService handles release operations within a repository.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Releases.ListAll(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
|
||||
type ReleaseService struct {
|
||||
Resource[types.Release, types.CreateReleaseOption, types.EditReleaseOption]
|
||||
}
|
||||
|
|
@ -23,7 +27,7 @@ func newReleaseService(c *Client) *ReleaseService {
|
|||
|
||||
// GetByTag returns a release by its tag name.
|
||||
func (s *ReleaseService) GetByTag(ctx context.Context, owner, repo, tag string) (*types.Release, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner, repo, tag)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/tags/{tag}", pathParams("owner", owner, "repo", repo, "tag", tag))
|
||||
var out types.Release
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -33,25 +37,25 @@ func (s *ReleaseService) GetByTag(ctx context.Context, owner, repo, tag string)
|
|||
|
||||
// DeleteByTag deletes a release by its tag name.
|
||||
func (s *ReleaseService) DeleteByTag(ctx context.Context, owner, repo, tag string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner, repo, tag)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/tags/{tag}", pathParams("owner", owner, "repo", repo, "tag", tag))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListAssets returns all assets for a release.
|
||||
func (s *ReleaseService) ListAssets(ctx context.Context, owner, repo string, releaseID int64) ([]types.Attachment, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner, repo, releaseID)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{releaseID}/assets", pathParams("owner", owner, "repo", repo, "releaseID", int64String(releaseID)))
|
||||
return ListAll[types.Attachment](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterAssets returns an iterator over all assets for a release.
|
||||
func (s *ReleaseService) IterAssets(ctx context.Context, owner, repo string, releaseID int64) iter.Seq2[types.Attachment, error] {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner, repo, releaseID)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{releaseID}/assets", pathParams("owner", owner, "repo", repo, "releaseID", int64String(releaseID)))
|
||||
return ListIter[types.Attachment](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// GetAsset returns a single asset for a release.
|
||||
func (s *ReleaseService) GetAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) (*types.Attachment, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", owner, repo, releaseID, assetID)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{releaseID}/assets/{assetID}", pathParams("owner", owner, "repo", repo, "releaseID", int64String(releaseID), "assetID", int64String(assetID)))
|
||||
var out types.Attachment
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -61,6 +65,6 @@ func (s *ReleaseService) GetAsset(ctx context.Context, owner, repo string, relea
|
|||
|
||||
// DeleteAsset deletes a single asset from a release.
|
||||
func (s *ReleaseService) DeleteAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", owner, repo, releaseID, assetID)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{releaseID}/assets/{assetID}", pathParams("owner", owner, "repo", repo, "releaseID", int64String(releaseID), "assetID", int64String(assetID)))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestReleaseService_Good_List(t *testing.T) {
|
||||
func TestReleaseService_List_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -36,7 +36,7 @@ func TestReleaseService_Good_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestReleaseService_Good_Get(t *testing.T) {
|
||||
func TestReleaseService_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -61,7 +61,7 @@ func TestReleaseService_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestReleaseService_Good_GetByTag(t *testing.T) {
|
||||
func TestReleaseService_GetByTag_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
|
|||
5
repos.go
5
repos.go
|
|
@ -8,6 +8,11 @@ import (
|
|||
)
|
||||
|
||||
// RepoService handles repository operations.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Repos.ListOrgRepos(ctx, "core")
|
||||
type RepoService struct {
|
||||
Resource[types.Repository, types.CreateRepoOption, types.EditRepoOption]
|
||||
}
|
||||
|
|
|
|||
17
resource.go
17
resource.go
|
|
@ -9,6 +9,11 @@ import (
|
|||
|
||||
// Resource provides generic CRUD operations for a Forgejo API resource.
|
||||
// T is the resource type, C is the create options type, U is the update options type.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// r := forge.NewResource[types.Issue, types.CreateIssueOption, types.EditIssueOption](client, "/api/v1/repos/{owner}/{repo}/issues/{index}")
|
||||
// _ = r
|
||||
type Resource[T any, C any, U any] struct {
|
||||
client *Client
|
||||
path string // item path: /api/v1/repos/{owner}/{repo}/issues/{index}
|
||||
|
|
@ -18,15 +23,19 @@ type Resource[T any, C any, U any] struct {
|
|||
// NewResource creates a new Resource for the given path pattern.
|
||||
// The path should be the item path (e.g., /repos/{owner}/{repo}/issues/{index}).
|
||||
// The collection path is derived by stripping the last /{placeholder} segment.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// r := forge.NewResource[types.Issue, types.CreateIssueOption, types.EditIssueOption](client, "/api/v1/repos/{owner}/{repo}/issues/{index}")
|
||||
// _ = r
|
||||
func NewResource[T any, C any, U any](c *Client, path string) *Resource[T, C, U] {
|
||||
collection := path
|
||||
// Strip last segment if it's a pure placeholder like /{index}
|
||||
// Don't strip if mixed like /repos or /{org}/repos
|
||||
parts := core.Split(path, "/")
|
||||
if len(parts) > 0 {
|
||||
lastSeg := parts[len(parts)-1]
|
||||
if i := lastIndexByte(path, '/'); i >= 0 {
|
||||
lastSeg := path[i+1:]
|
||||
if core.HasPrefix(lastSeg, "{") && core.HasSuffix(lastSeg, "}") {
|
||||
collection = path[:len(path)-len(lastSeg)-1]
|
||||
collection = path[:i]
|
||||
}
|
||||
}
|
||||
return &Resource[T, C, U]{client: c, path: path, collection: collection}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -22,7 +22,7 @@ type testUpdate struct {
|
|||
Name *string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
func TestResource_Good_List(t *testing.T) {
|
||||
func TestResource_List_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/orgs/core/repos" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
|
|
@ -44,7 +44,7 @@ func TestResource_Good_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResource_Good_Get(t *testing.T) {
|
||||
func TestResource_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/repos/core/go-forge" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
|
|
@ -65,7 +65,7 @@ func TestResource_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResource_Good_Create(t *testing.T) {
|
||||
func TestResource_Create_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -94,7 +94,7 @@ func TestResource_Good_Create(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResource_Good_Update(t *testing.T) {
|
||||
func TestResource_Update_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPatch {
|
||||
t.Errorf("expected PATCH, got %s", r.Method)
|
||||
|
|
@ -116,7 +116,7 @@ func TestResource_Good_Update(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResource_Good_Delete(t *testing.T) {
|
||||
func TestResource_Delete_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
|
|
@ -134,7 +134,7 @@ func TestResource_Good_Delete(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResource_Good_ListAll(t *testing.T) {
|
||||
func TestResource_ListAll_Good(t *testing.T) {
|
||||
page := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
page++
|
||||
|
|
@ -159,7 +159,7 @@ func TestResource_Good_ListAll(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResource_Good_Iter(t *testing.T) {
|
||||
func TestResource_Iter_Good(t *testing.T) {
|
||||
page := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
page++
|
||||
|
|
@ -190,7 +190,7 @@ func TestResource_Good_Iter(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResource_Bad_IterError(t *testing.T) {
|
||||
func TestResource_IterError_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "server error"})
|
||||
|
|
@ -212,7 +212,7 @@ func TestResource_Bad_IterError(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResource_Good_IterBreakEarly(t *testing.T) {
|
||||
func TestResource_IterBreakEarly_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Total-Count", "100")
|
||||
json.NewEncoder(w).Encode([]testItem{{1, "a"}, {2, "b"}, {3, "c"}})
|
||||
|
|
|
|||
26
teams.go
26
teams.go
|
|
@ -4,11 +4,15 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// TeamService handles team operations.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Teams.ListMembers(ctx, 42)
|
||||
type TeamService struct {
|
||||
Resource[types.Team, types.CreateTeamOption, types.EditTeamOption]
|
||||
}
|
||||
|
|
@ -23,60 +27,60 @@ func newTeamService(c *Client) *TeamService {
|
|||
|
||||
// ListMembers returns all members of a team.
|
||||
func (s *TeamService) ListMembers(ctx context.Context, teamID int64) ([]types.User, error) {
|
||||
path := core.Sprintf("/api/v1/teams/%d/members", teamID)
|
||||
path := ResolvePath("/api/v1/teams/{teamID}/members", pathParams("teamID", int64String(teamID)))
|
||||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterMembers returns an iterator over all members of a team.
|
||||
func (s *TeamService) IterMembers(ctx context.Context, teamID int64) iter.Seq2[types.User, error] {
|
||||
path := core.Sprintf("/api/v1/teams/%d/members", teamID)
|
||||
path := ResolvePath("/api/v1/teams/{teamID}/members", pathParams("teamID", int64String(teamID)))
|
||||
return ListIter[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// AddMember adds a user to a team.
|
||||
func (s *TeamService) AddMember(ctx context.Context, teamID int64, username string) error {
|
||||
path := core.Sprintf("/api/v1/teams/%d/members/%s", teamID, username)
|
||||
path := ResolvePath("/api/v1/teams/{teamID}/members/{username}", pathParams("teamID", int64String(teamID), "username", username))
|
||||
return s.client.Put(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// RemoveMember removes a user from a team.
|
||||
func (s *TeamService) RemoveMember(ctx context.Context, teamID int64, username string) error {
|
||||
path := core.Sprintf("/api/v1/teams/%d/members/%s", teamID, username)
|
||||
path := ResolvePath("/api/v1/teams/{teamID}/members/{username}", pathParams("teamID", int64String(teamID), "username", username))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListRepos returns all repositories managed by a team.
|
||||
func (s *TeamService) ListRepos(ctx context.Context, teamID int64) ([]types.Repository, error) {
|
||||
path := core.Sprintf("/api/v1/teams/%d/repos", teamID)
|
||||
path := ResolvePath("/api/v1/teams/{teamID}/repos", pathParams("teamID", int64String(teamID)))
|
||||
return ListAll[types.Repository](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterRepos returns an iterator over all repositories managed by a team.
|
||||
func (s *TeamService) IterRepos(ctx context.Context, teamID int64) iter.Seq2[types.Repository, error] {
|
||||
path := core.Sprintf("/api/v1/teams/%d/repos", teamID)
|
||||
path := ResolvePath("/api/v1/teams/{teamID}/repos", pathParams("teamID", int64String(teamID)))
|
||||
return ListIter[types.Repository](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// AddRepo adds a repository to a team.
|
||||
func (s *TeamService) AddRepo(ctx context.Context, teamID int64, org, repo string) error {
|
||||
path := core.Sprintf("/api/v1/teams/%d/repos/%s/%s", teamID, org, repo)
|
||||
path := ResolvePath("/api/v1/teams/{teamID}/repos/{org}/{repo}", pathParams("teamID", int64String(teamID), "org", org, "repo", repo))
|
||||
return s.client.Put(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// RemoveRepo removes a repository from a team.
|
||||
func (s *TeamService) RemoveRepo(ctx context.Context, teamID int64, org, repo string) error {
|
||||
path := core.Sprintf("/api/v1/teams/%d/repos/%s/%s", teamID, org, repo)
|
||||
path := ResolvePath("/api/v1/teams/{teamID}/repos/{org}/{repo}", pathParams("teamID", int64String(teamID), "org", org, "repo", repo))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListOrgTeams returns all teams in an organisation.
|
||||
func (s *TeamService) ListOrgTeams(ctx context.Context, org string) ([]types.Team, error) {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/teams", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/teams", pathParams("org", org))
|
||||
return ListAll[types.Team](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterOrgTeams returns an iterator over all teams in an organisation.
|
||||
func (s *TeamService) IterOrgTeams(ctx context.Context, org string) iter.Seq2[types.Team, error] {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/teams", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/teams", pathParams("org", org))
|
||||
return ListIter[types.Team](ctx, s.client, path, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestTeamService_Good_Get(t *testing.T) {
|
||||
func TestTeamService_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -32,7 +32,7 @@ func TestTeamService_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTeamService_Good_ListMembers(t *testing.T) {
|
||||
func TestTeamService_ListMembers_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -61,7 +61,7 @@ func TestTeamService_Good_ListMembers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTeamService_Good_AddMember(t *testing.T) {
|
||||
func TestTeamService_AddMember_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
t.Errorf("expected PUT, got %s", r.Method)
|
||||
|
|
|
|||
26
users.go
26
users.go
|
|
@ -4,11 +4,15 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// UserService handles user operations.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Users.GetCurrent(ctx)
|
||||
type UserService struct {
|
||||
Resource[types.User, struct{}, struct{}]
|
||||
}
|
||||
|
|
@ -32,60 +36,60 @@ func (s *UserService) GetCurrent(ctx context.Context) (*types.User, error) {
|
|||
|
||||
// ListFollowers returns all followers of a user.
|
||||
func (s *UserService) ListFollowers(ctx context.Context, username string) ([]types.User, error) {
|
||||
path := core.Sprintf("/api/v1/users/%s/followers", username)
|
||||
path := ResolvePath("/api/v1/users/{username}/followers", pathParams("username", username))
|
||||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterFollowers returns an iterator over all followers of a user.
|
||||
func (s *UserService) IterFollowers(ctx context.Context, username string) iter.Seq2[types.User, error] {
|
||||
path := core.Sprintf("/api/v1/users/%s/followers", username)
|
||||
path := ResolvePath("/api/v1/users/{username}/followers", pathParams("username", username))
|
||||
return ListIter[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// ListFollowing returns all users that a user is following.
|
||||
func (s *UserService) ListFollowing(ctx context.Context, username string) ([]types.User, error) {
|
||||
path := core.Sprintf("/api/v1/users/%s/following", username)
|
||||
path := ResolvePath("/api/v1/users/{username}/following", pathParams("username", username))
|
||||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterFollowing returns an iterator over all users that a user is following.
|
||||
func (s *UserService) IterFollowing(ctx context.Context, username string) iter.Seq2[types.User, error] {
|
||||
path := core.Sprintf("/api/v1/users/%s/following", username)
|
||||
path := ResolvePath("/api/v1/users/{username}/following", pathParams("username", username))
|
||||
return ListIter[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// Follow follows a user as the authenticated user.
|
||||
func (s *UserService) Follow(ctx context.Context, username string) error {
|
||||
path := core.Sprintf("/api/v1/user/following/%s", username)
|
||||
path := ResolvePath("/api/v1/user/following/{username}", pathParams("username", username))
|
||||
return s.client.Put(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// Unfollow unfollows a user as the authenticated user.
|
||||
func (s *UserService) Unfollow(ctx context.Context, username string) error {
|
||||
path := core.Sprintf("/api/v1/user/following/%s", username)
|
||||
path := ResolvePath("/api/v1/user/following/{username}", pathParams("username", username))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
||||
// ListStarred returns all repositories starred by a user.
|
||||
func (s *UserService) ListStarred(ctx context.Context, username string) ([]types.Repository, error) {
|
||||
path := core.Sprintf("/api/v1/users/%s/starred", username)
|
||||
path := ResolvePath("/api/v1/users/{username}/starred", pathParams("username", username))
|
||||
return ListAll[types.Repository](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterStarred returns an iterator over all repositories starred by a user.
|
||||
func (s *UserService) IterStarred(ctx context.Context, username string) iter.Seq2[types.Repository, error] {
|
||||
path := core.Sprintf("/api/v1/users/%s/starred", username)
|
||||
path := ResolvePath("/api/v1/users/{username}/starred", pathParams("username", username))
|
||||
return ListIter[types.Repository](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// Star stars a repository as the authenticated user.
|
||||
func (s *UserService) Star(ctx context.Context, owner, repo string) error {
|
||||
path := core.Sprintf("/api/v1/user/starred/%s/%s", owner, repo)
|
||||
path := ResolvePath("/api/v1/user/starred/{owner}/{repo}", pathParams("owner", owner, "repo", repo))
|
||||
return s.client.Put(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// Unstar unstars a repository as the authenticated user.
|
||||
func (s *UserService) Unstar(ctx context.Context, owner, repo string) error {
|
||||
path := core.Sprintf("/api/v1/user/starred/%s/%s", owner, repo)
|
||||
path := ResolvePath("/api/v1/user/starred/{owner}/{repo}", pathParams("owner", owner, "repo", repo))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestUserService_Good_Get(t *testing.T) {
|
||||
func TestUserService_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -32,7 +32,7 @@ func TestUserService_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestUserService_Good_GetCurrent(t *testing.T) {
|
||||
func TestUserService_GetCurrent_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -54,7 +54,7 @@ func TestUserService_Good_GetCurrent(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestUserService_Good_ListFollowers(t *testing.T) {
|
||||
func TestUserService_ListFollowers_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
|
|||
12
webhooks.go
12
webhooks.go
|
|
@ -4,12 +4,16 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// WebhookService handles webhook (hook) operations within a repository.
|
||||
// Embeds Resource for standard CRUD on /api/v1/repos/{owner}/{repo}/hooks/{id}.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Webhooks.ListAll(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
|
||||
type WebhookService struct {
|
||||
Resource[types.Hook, types.CreateHookOption, types.EditHookOption]
|
||||
}
|
||||
|
|
@ -24,18 +28,18 @@ func newWebhookService(c *Client) *WebhookService {
|
|||
|
||||
// TestHook triggers a test delivery for a webhook.
|
||||
func (s *WebhookService) TestHook(ctx context.Context, owner, repo string, id int64) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/hooks/%d/tests", owner, repo, id)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/hooks/{id}/tests", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
|
||||
return s.client.Post(ctx, path, nil, nil)
|
||||
}
|
||||
|
||||
// ListOrgHooks returns all webhooks for an organisation.
|
||||
func (s *WebhookService) ListOrgHooks(ctx context.Context, org string) ([]types.Hook, error) {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/hooks", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/hooks", pathParams("org", org))
|
||||
return ListAll[types.Hook](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterOrgHooks returns an iterator over all webhooks for an organisation.
|
||||
func (s *WebhookService) IterOrgHooks(ctx context.Context, org string) iter.Seq2[types.Hook, error] {
|
||||
path := core.Sprintf("/api/v1/orgs/%s/hooks", org)
|
||||
path := ResolvePath("/api/v1/orgs/{org}/hooks", pathParams("org", org))
|
||||
return ListIter[types.Hook](ctx, s.client, path, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestWebhookService_Good_List(t *testing.T) {
|
||||
func TestWebhookService_List_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -36,7 +36,7 @@ func TestWebhookService_Good_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWebhookService_Good_Get(t *testing.T) {
|
||||
func TestWebhookService_Get_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -70,7 +70,7 @@ func TestWebhookService_Good_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWebhookService_Good_Create(t *testing.T) {
|
||||
func TestWebhookService_Create_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -108,7 +108,7 @@ func TestWebhookService_Good_Create(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWebhookService_Good_TestHook(t *testing.T) {
|
||||
func TestWebhookService_TestHook_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -127,7 +127,7 @@ func TestWebhookService_Good_TestHook(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWebhookService_Good_ListOrgHooks(t *testing.T) {
|
||||
func TestWebhookService_ListOrgHooks_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -155,7 +155,7 @@ func TestWebhookService_Good_ListOrgHooks(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWebhookService_Bad_NotFound(t *testing.T) {
|
||||
func TestWebhookService_NotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "hook not found"})
|
||||
|
|
|
|||
16
wiki.go
16
wiki.go
|
|
@ -3,12 +3,16 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
// WikiService handles wiki page operations for a repository.
|
||||
// No Resource embedding — custom endpoints for wiki CRUD.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f := forge.NewForge("https://forge.lthn.ai", "token")
|
||||
// _, err := f.Wiki.ListPages(ctx, "core", "go-forge")
|
||||
type WikiService struct {
|
||||
client *Client
|
||||
}
|
||||
|
|
@ -19,7 +23,7 @@ func newWikiService(c *Client) *WikiService {
|
|||
|
||||
// ListPages returns all wiki page metadata for a repository.
|
||||
func (s *WikiService) ListPages(ctx context.Context, owner, repo string) ([]types.WikiPageMetaData, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/wiki/pages", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/wiki/pages", pathParams("owner", owner, "repo", repo))
|
||||
var out []types.WikiPageMetaData
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -29,7 +33,7 @@ func (s *WikiService) ListPages(ctx context.Context, owner, repo string) ([]type
|
|||
|
||||
// GetPage returns a single wiki page by name.
|
||||
func (s *WikiService) GetPage(ctx context.Context, owner, repo, pageName string) (*types.WikiPage, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/wiki/page/{pageName}", pathParams("owner", owner, "repo", repo, "pageName", pageName))
|
||||
var out types.WikiPage
|
||||
if err := s.client.Get(ctx, path, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -39,7 +43,7 @@ func (s *WikiService) GetPage(ctx context.Context, owner, repo, pageName string)
|
|||
|
||||
// CreatePage creates a new wiki page.
|
||||
func (s *WikiService) CreatePage(ctx context.Context, owner, repo string, opts *types.CreateWikiPageOptions) (*types.WikiPage, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/wiki/new", owner, repo)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/wiki/new", pathParams("owner", owner, "repo", repo))
|
||||
var out types.WikiPage
|
||||
if err := s.client.Post(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -49,7 +53,7 @@ func (s *WikiService) CreatePage(ctx context.Context, owner, repo string, opts *
|
|||
|
||||
// EditPage updates an existing wiki page.
|
||||
func (s *WikiService) EditPage(ctx context.Context, owner, repo, pageName string, opts *types.CreateWikiPageOptions) (*types.WikiPage, error) {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/wiki/page/{pageName}", pathParams("owner", owner, "repo", repo, "pageName", pageName))
|
||||
var out types.WikiPage
|
||||
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -59,6 +63,6 @@ func (s *WikiService) EditPage(ctx context.Context, owner, repo, pageName string
|
|||
|
||||
// DeletePage removes a wiki page.
|
||||
func (s *WikiService) DeletePage(ctx context.Context, owner, repo, pageName string) error {
|
||||
path := core.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
|
||||
path := ResolvePath("/api/v1/repos/{owner}/{repo}/wiki/page/{pageName}", pathParams("owner", owner, "repo", repo, "pageName", pageName))
|
||||
return s.client.Delete(ctx, path)
|
||||
}
|
||||
|
|
|
|||
14
wiki_test.go
14
wiki_test.go
|
|
@ -2,7 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
||||
func TestWikiService_Good_ListPages(t *testing.T) {
|
||||
func TestWikiService_ListPages_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -41,7 +41,7 @@ func TestWikiService_Good_ListPages(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWikiService_Good_GetPage(t *testing.T) {
|
||||
func TestWikiService_GetPage_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
|
|
@ -74,7 +74,7 @@ func TestWikiService_Good_GetPage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWikiService_Good_CreatePage(t *testing.T) {
|
||||
func TestWikiService_CreatePage_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
|
|
@ -118,7 +118,7 @@ func TestWikiService_Good_CreatePage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWikiService_Good_EditPage(t *testing.T) {
|
||||
func TestWikiService_EditPage_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPatch {
|
||||
t.Errorf("expected PATCH, got %s", r.Method)
|
||||
|
|
@ -151,7 +151,7 @@ func TestWikiService_Good_EditPage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWikiService_Good_DeletePage(t *testing.T) {
|
||||
func TestWikiService_DeletePage_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("expected DELETE, got %s", r.Method)
|
||||
|
|
@ -170,7 +170,7 @@ func TestWikiService_Good_DeletePage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWikiService_Bad_NotFound(t *testing.T) {
|
||||
func TestWikiService_NotFound_Bad(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "page not found"})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue