diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..def65d5 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,46 @@ +# Copilot Instructions for `goark/fetch` + +## Project purpose + +`fetch` is a small package focused on downloading data from URL endpoints. +The package should hide common `net/http` idioms and keep calling code simple. + +## Design principles + +- Prefer explicit, small APIs over feature-rich abstractions. +- Keep `context.Context` based methods as the default path. +- Favor composable option functions (`ClientOpts`, `RequestOpts`). +- Preserve compatibility when possible; avoid breaking public symbols. + +## Error handling + +- Use `github.com/goark/errs` as the primary internal error handling package. +- Prefer `errs.Wrap`, `errs.Join`, and `errs.WithContext` over ad-hoc wrapping patterns. +- Keep `errors.Is` compatibility when returning wrapped errors. +- Return wrapped errors so callers can use `errors.Is`. +- Keep sentinel errors stable (`ErrInvalidURL`, `ErrInvalidRequest`, `ErrHTTPStatus`, `ErrNullPointer`). +- Include useful context values when wrapping errors. + +## HTTP behavior + +- Continue to use `net/http` as the base implementation. +- Ensure response body cleanup remains safe and predictable. +- Keep behavior for non-success HTTP status explicit and documented. + +## Coding style + +- Write idiomatic Go and keep implementation straightforward. +- Avoid unnecessary dependencies. +- Keep comments concise and in English. + +## Testing and validation + +- Add or update tests for behavior changes. +- Prefer local validation with Taskfile targets: + - `task test` + - `task govulncheck` + +## Documentation + +- Keep `README.md` in sync with public API changes. +- Include practical examples for GET/POST and error handling. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..43e5071 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: ci + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + test-and-lint: + name: lint and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: latest + args: --enable gosec + + - name: Test module + run: go test -shuffle on ./... + + govulncheck: + name: govulncheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Run govulncheck + uses: golang/govulncheck-action@v1 + with: + go-version-file: go.mod + go-package: ./... + repo-checkout: false diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 5cbe92f..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - # ┌───────────── minute (0 - 59) - # │ ┌───────────── hour (0 - 23) - # │ │ ┌───────────── day of the month (1 - 31) - # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) - # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) - # │ │ │ │ │ - # │ │ │ │ │ - # │ │ │ │ │ - # * * * * * - - cron: '30 1 * * 0' - -jobs: - CodeQL-Build: - # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest - runs-on: ubuntu-latest - - permissions: - # required for all workflows - security-events: write - - # only required for workflows in private repositories - actions: read - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java, ruby - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below). - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # ✏️ If the Autobuild fails above, remove it and uncomment the following - # three lines and modify them (or add more) to build your code if your - # project uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..9db6ba2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,35 @@ +name: CodeQL + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 20 * * 0" + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: go + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index e6ae76f..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: lint -on: - push: - branches: - - main - pull_request: - -permissions: - contents: read - # Optional: allow read access to pull request. Use with `only-new-issues` option. - # pull-requests: read -jobs: - golangci: - name: lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version-file: 'go.mod' - - name: golangci-lint - uses: golangci/golangci-lint-action@v9 - with: - # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: latest - - # Optional: working directory, useful for monorepos - # working-directory: somedir - - # Optional: golangci-lint command line arguments. - args: --enable gosec - - # Optional: show only new issues if it's a pull request. The default value is `false`. - # only-new-issues: true - - # Optional: if set to true then the all caching functionality will be complete disabled, - # takes precedence over all other caching options. - # skip-cache: true - - # Optional: if set to true then the action don't cache or restore ~/go/pkg. - # skip-pkg-cache: true - - # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. - # skip-build-cache: true - - name: testing - run: go test -shuffle on ./... - - name: install govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@latest - - name: running govulncheck - run: govulncheck ./... diff --git a/.github/workflows/vulns.yml b/.github/workflows/vulns.yml deleted file mode 100644 index d1c8732..0000000 --- a/.github/workflows/vulns.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: vulns -on: - push: - branches: - - main - pull_request: -jobs: - vulns: - name: Vulnerability scanner - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-go@v5 - with: - go-version-file: 'go.mod' - - name: WriteGoList - run: go list -json -m all > go.list - - name: Nancy - uses: sonatype-nexus-community/nancy-github-action@main diff --git a/README.md b/README.md index 737af72..a28661e 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,149 @@ # [fetch] -- Fetch Data from URL -[![check vulns](https://github.com/goark/fetch/workflows/vulns/badge.svg)](https://github.com/goark/fetch/actions) -[![lint status](https://github.com/goark/fetch/workflows/lint/badge.svg)](https://github.com/goark/fetch/actions) -[![GitHub license](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://raw.githubusercontent.com/goark/fetch/master/LICENSE) +[![ci status](https://github.com/goark/fetch/workflows/ci/badge.svg)](https://github.com/goark/fetch/actions) +[![codeql status](https://github.com/goark/fetch/workflows/CodeQL/badge.svg)](https://github.com/goark/fetch/actions) +[![GitHub license](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://raw.githubusercontent.com/goark/fetch/main/LICENSE) [![GitHub release](https://img.shields.io/github/release/goark/fetch.svg)](https://github.com/goark/fetch/releases/latest) -This package is required Go 1.25 or later. +`fetch` is a focused helper package for downloading data from URL endpoints. +It wraps common `net/http` idioms so callers can handle download flows with less boilerplate and safer defaults. -**Migrated repository to [github.com/goark/fetch][fetch]** +## Design goals -## Import +- Keep application code simple for one-shot data fetching. +- Hide repetitive `net/http` handling patterns. +- Keep API surface small and composable. +- Use `context.Context` based operations as the default style. + +## Development + +### Requirements + +- Go 1.25 or later +- [Task](https://taskfile.dev/) command (local tool for this repository) + +### Local validation + +```text +task test +task govulncheck +``` + +Run all maintenance tasks: + +```text +task +``` + +## CI Workflows + +- `ci`: lint (`golangci-lint` with `gosec`), tests, and `govulncheck` +- `CodeQL`: scheduled and push/PR static analysis + +## Usage + +### Install and import + +```bash +go get github.com/goark/fetch@latest +``` ```go import "github.com/goark/fetch" ``` -## Usage +### Sample programs + +All sample files under `sample/` use the `run` build tag. + +```text +go run -tags run ./sample/sample.go +``` + +- Basic GET download: [sample/sample.go](sample/sample.go) + +### GET request ```go package main import ( - "context" - "fmt" - "io" - "os" + "context" + "fmt" + "io" + "os" - "github.com/goark/fetch" + "github.com/goark/fetch" ) func main() { - u, err := fetch.URL("https://github.com/spiegel-im-spiegel.gpg") - if err != nil { - fmt.Fprintln(os.Stderr, err) - return - } - resp, err := fetch.New().GetWithContext(context.Background(), u) - if err != nil { - fmt.Fprintln(os.Stderr, err) - return - } - defer resp.Close() - if _, err := io.Copy(os.Stdout, resp.Body()); err != nil { - fmt.Fprintln(os.Stderr, err) - } + u, err := fetch.URL("https://github.com/spiegel-im-spiegel.gpg") + if err != nil { + fmt.Fprintln(os.Stderr, err) + return + } + + resp, err := fetch.New().GetWithContext(context.Background(), u) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return + } + defer resp.Close() + + if _, err := io.Copy(os.Stdout, resp.Body()); err != nil { + fmt.Fprintln(os.Stderr, err) + } } ``` -## Modules Requirement Graph +### POST request -[![dependency.png](./dependency.png)](./dependency.png) +```go +payload := strings.NewReader("name=alice") +resp, err := fetch.New().PostWithContext(ctx, u, payload, + fetch.WithRequestHeaderSet("Content-Type", "application/x-www-form-urlencoded"), +) +if err != nil { + return err +} +defer resp.Close() +``` + +### Public API + +- `fetch.URL(raw string)` parses and validates URL input. +- `fetch.New(opts ...fetch.ClientOpts)` creates a fetch client. +- `Client.GetWithContext(ctx, u, opts...)` performs GET request. +- `Client.PostWithContext(ctx, u, payload, opts...)` performs POST request. +- `fetch.WithHTTPClient(cli)` injects custom `*http.Client`. +- `fetch.WithRequestHeaderAdd(name, value)` and `fetch.WithRequestHeaderSet(name, value)` apply request headers. + +### Error handling + +The package wraps errors, so use `errors.Is` for checks against: -[fetch]: https://github.com/goark/fetch "goark/fetch: Fetch Data from URL" +- `fetch.ErrInvalidURL` +- `fetch.ErrInvalidRequest` +- `fetch.ErrHTTPStatus` + +```go +if err != nil { + switch { + case errors.Is(err, fetch.ErrInvalidURL): + // invalid URL input + case errors.Is(err, fetch.ErrHTTPStatus): + // non-success HTTP status (>= 400) + } +} +``` + +### Response lifecycle + +Always close the response: + +- `defer resp.Close()` for streaming use. +- Use `resp.DumpBodyAndClose()` when you need body bytes at once. + +### Modules Requirement Graph + +[![dependency.png](./dependency.png)](./dependency.png) diff --git a/go.mod b/go.mod index 2fb1bf1..825f6b9 100644 --- a/go.mod +++ b/go.mod @@ -4,4 +4,4 @@ go 1.25 toolchain go1.26.3 -require github.com/goark/errs v1.3.3 +require github.com/goark/errs v1.3.4 diff --git a/go.sum b/go.sum index afafe66..4e63d82 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/goark/errs v1.3.3 h1:vqzm/1aqvh4Ha8JDqIqIU5ZoBtjaRezFcMC55B5ljAM= github.com/goark/errs v1.3.3/go.mod h1:4xM7rorwYQlqh9kUhfKpC5P7VAJW2KfvuQpYnTaU0ek= +github.com/goark/errs v1.3.4 h1:/+/xwF3UwXGxGGLurzBTaMMoryTBeaPfheJ1aW9cglA= +github.com/goark/errs v1.3.4/go.mod h1:4xM7rorwYQlqh9kUhfKpC5P7VAJW2KfvuQpYnTaU0ek=