Skip to content

Commit b59df3a

Browse files
authored
Merge pull request #7 from moogar0880/modernization
Modernization
2 parents 39015f2 + 7fa1630 commit b59df3a

File tree

17 files changed

+946
-338
lines changed

17 files changed

+946
-338
lines changed

.github/workflows/test.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: test
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
9+
jobs:
10+
test:
11+
strategy:
12+
matrix:
13+
go-version: [1.22.x, 1.23.x, 1.24.x]
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v3
17+
18+
- name: Set up Go
19+
uses: actions/setup-go@v3
20+
with:
21+
go-version: ${{ matrix.go-version }}
22+
23+
- name: Test
24+
run: make test

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ _testmain.go
2424
*.prof
2525

2626
# Coverage reports
27-
coverage.out
27+
cover.out

.travis.yml

Lines changed: 0 additions & 15 deletions
This file was deleted.

Makefile

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,50 @@
1-
PACKAGE=github.com/moogar0880/problems
1+
###############################################################################
2+
# GNU Make Variables
3+
###############################################################################
4+
export MAKEFLAGS += --warn-undefined-variables
5+
export SHELL := bash
6+
export .SHELLFLAGS := -eu -o pipefail -c
7+
export .DEFAULT_GOAL := all
8+
.DELETE_ON_ERROR:
9+
.SUFFIXES:
210

3-
.PHONY: all test coverage style
11+
###############################################################################
12+
# Location Variables
13+
###############################################################################
14+
export PROJECT_ROOT=$(shell pwd)
415

5-
all: test
16+
###############################################################################
17+
# Go Variables
18+
###############################################################################
19+
GO_MODULE_NAME=github.com/moogar0880/problems
20+
GO_COVERAGE_FILE=cover.out
21+
GO_TEST_OPTS=-coverprofile $(GO_COVERAGE_FILE)
22+
GO_TEST_PKGS=./...
623

7-
build:
8-
go build -v $(PACKAGE)
24+
.PHONY: clean
25+
clean:
26+
@rm $(GO_COVERAGE_FILE)
927

10-
### Testing
11-
test: build
12-
go test -v $(PACKAGE)
13-
14-
coverage:
15-
go test -v -cover -coverprofile=coverage.out $(PACKAGE)
28+
.PHONY: godoc
29+
godoc:
30+
docker run \
31+
--rm \
32+
--detach \
33+
--entrypoint bash \
34+
--expose "6060" \
35+
--name "godoc" \
36+
--publish "6060:6060" \
37+
--volume $(PROJECT_ROOT):/go/src/$(GO_MODULE_NAME) \
38+
--workdir /go/src/$(GO_MODULE_NAME) \
39+
golang:latest \
40+
-c "go install golang.org/x/tools/cmd/godoc@latest && godoc -http=:6060" || true
41+
@open http://localhost:6060
1642

17-
### Style and Linting
18-
lint:
19-
go vet $(PACKAGE) && goimports .
43+
### Testing
44+
.PHONY: test
45+
test:
46+
@go test $(GO_TEST_OPTS) $(GO_TEST_PKGS)
2047

21-
# modify source code if style offences are found
22-
style:
23-
go vet $(PACKAGE) && goimports -e -l -w .
48+
.PHONY: test/coverage
49+
test/coverage: test
50+
@go tool cover -html=cover.out

README.md

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
# problems
2-
Problems is an RFC-7807 compliant library for describing HTTP errors, written
3-
purely in Go. For more information see [RFC-7807](https://tools.ietf.org/html/rfc7807).
2+
Problems is an RFC-7807 and RFC-9457 compliant library for describing HTTP
3+
errors. For more information see [RFC-9457](https://tools.ietf.org/html/rfc9457),
4+
and it's predecessor [RFC-7807](https://tools.ietf.org/html/rfc7807).
45

56
[![Build Status](https://travis-ci.org/moogar0880/problems.svg?branch=master)](https://travis-ci.org/moogar0880/problems)
67
[![Go Report Card](https://goreportcard.com/badge/github.com/moogar0880/problems)](https://goreportcard.com/report/github.com/moogar0880/problems)
78
[![GoDoc](https://godoc.org/github.com/moogar0880/problems?status.svg)](https://godoc.org/github.com/moogar0880/problems)
89

910
## Usage
10-
The problems library exposes an assortment of interfaces, structs, and functions
11-
for defining and using HTTP Problem detail resources.
11+
The problems library exposes an assortment of types to aid HTTP service authors
12+
in defining and using HTTP Problem detail resources.
1213

1314
### Predefined Errors
1415
You can define basic Problem details up front by using the `NewStatusProblem`
1516
function
1617

1718
```go
19+
package main
20+
1821
import "github.com/moogar0880/problems"
1922

2023
var (
@@ -39,6 +42,8 @@ Which, when served over HTTP as JSON will look like the following:
3942
New errors can also be created a head of time, or on the fly like so:
4043

4144
```go
45+
package main
46+
4247
import "github.com/moogar0880/problems"
4348

4449
func NoSuchUser() *problems.DefaultProblem {
@@ -59,31 +64,60 @@ Which, when served over HTTP as JSON will look like the following:
5964
}
6065
```
6166

62-
### Expanded Errors
67+
### Extended Errors
6368
The specification for these HTTP problems was designed to allow for arbitrary
6469
expansion of the problem resources. This can be accomplished through this
65-
library by implementing the `Problem` interface:
70+
library by either embedding the `Problem` struct in your extension type:
6671

6772
```go
73+
package main
74+
6875
import "github.com/moogar0880/problems"
6976

7077
type CreditProblem struct {
71-
problems.DefaultProblem
78+
problems.Problem
79+
80+
Balance float64 `json:"balance"`
81+
Accounts []string `json:"accounts"`
82+
}
83+
```
84+
85+
Which, when served over HTTP as JSON will look like the following:
7286

73-
Balance float64
74-
Accounts []string
87+
```json
88+
{
89+
"type": "about:blank",
90+
"title": "Unauthorized",
91+
"status": 401,
92+
"balance": 30,
93+
"accounts": ["/account/12345", "/account/67890"]
7594
}
95+
```
96+
97+
Or by using the `problems.ExtendedProblem` type:
98+
99+
```go
100+
package main
101+
102+
import (
103+
"net/http"
104+
105+
"github.com/moogar0880/problems"
106+
)
76107

77-
func (cp *CreditProblem) ProblemType() (*url.URL, error) {
78-
u, err := url.Parse(cp.Type)
79-
if err != nil {
80-
return nil, err
81-
}
82-
return u, nil
108+
type CreditProblemExt struct {
109+
Balance float64 `json:"balance"`
110+
Accounts []string `json:"accounts"`
83111
}
84112

85-
func (cp *CreditProblem) ProblemTitle() string {
86-
return cp.Title
113+
func main() {
114+
problems.NewExt[CreditProblemExt]().
115+
WithStatus(http.StatusForbidden).
116+
WithDetail("Your account does not have sufficient funds to complete this transaction").
117+
WithExtension(CreditProblemExt{
118+
Balance: 30,
119+
Accounts: []string{"/account/12345", "/account/67890"},
120+
})
87121
}
88122
```
89123

@@ -93,9 +127,11 @@ Which, when served over HTTP as JSON will look like the following:
93127
{
94128
"type": "about:blank",
95129
"title": "Unauthorized",
96-
"status": 401,
97-
"balance": 30,
98-
"accounts": ["/account/12345", "/account/67890"]
130+
"status": 401,
131+
"extensions": {
132+
"balance": 30,
133+
"accounts": ["/account/12345", "/account/67890"]
134+
}
99135
}
100136
```
101137

doc.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
// Package problems provides an RFC 7807 (https://tools.ietf.org/html/rfc7807)
2-
// compliant implementation of HTTP problem details. Which are defined as a
3-
// means to carry machine-readable details of errors in an HTTP response to
4-
// avoid the need to define new error response formats for HTTP APIs.
1+
// Package problems provides an RFC-9457 (https://tools.ietf.org/html/rfc9457)
2+
// and RFC 7807 (https://tools.ietf.org/html/rfc7807) compliant implementation
3+
// of HTTP problem details. Which are defined as a means to carry
4+
// machine-readable details of errors in an HTTP response to avoid the need to
5+
// define new error response formats for HTTP APIs.
56
//
67
// The problem details specification was designed to allow for schema
7-
// extensions. Because of this the exposed Problem interface only enforces the
8-
// required Type and Title fields be set appropriately.
8+
// extensions. There are two possible ways to create problem extensions:
99
//
10-
// Additionally, this library also ships with default http.HandlerFunc's capable
11-
// of writing problems to http.ResponseWriter's in either of the two standard
12-
// media formats JSON and XML.
10+
// 1. You can embed a problem in your extension problem type.
11+
// 2. You can use the ExtendedProblem to leverage the existing types in this
12+
// library.
13+
//
14+
// See the examples for references on how to use either of these extension
15+
// mechanisms.
16+
//
17+
// Additionally, this library also ships with default http.HandlerFunc
18+
// implementations which are capable of writing problems to a
19+
// http.ResponseWriter in either of the two standard media formats, JSON and
20+
// XML.
1321
package problems

doc_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package problems_test
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
7+
"net/http"
68

79
"github.com/moogar0880/problems"
810
)
@@ -30,3 +32,78 @@ func ExampleNewStatusProblem_detailed() {
3032
// "detail": "The item you've requested either does not exist or has been deleted."
3133
// }
3234
}
35+
36+
func ExampleFromError() {
37+
err := func() error {
38+
// Some fallible function.
39+
return errors.New("something bad happened")
40+
}()
41+
internalServerError := problems.FromError(err).WithStatus(http.StatusInternalServerError)
42+
b, _ := json.MarshalIndent(internalServerError, "", " ")
43+
fmt.Println(string(b))
44+
// Output: {
45+
// "type": "about:blank",
46+
// "title": "Internal Server Error",
47+
// "status": 500,
48+
// "detail": "something bad happened"
49+
// }
50+
}
51+
52+
func ExampleExtendedProblem() {
53+
type CreditProblemExt struct {
54+
Balance float64 `json:"balance"`
55+
Accounts []string `json:"accounts"`
56+
}
57+
problem := problems.NewExt[CreditProblemExt]().
58+
WithStatus(http.StatusForbidden).
59+
WithDetail("You do not have sufficient funds to complete this transaction.").
60+
WithExtension(CreditProblemExt{
61+
Balance: 30,
62+
Accounts: []string{"/account/12345", "/account/67890"},
63+
})
64+
b, _ := json.MarshalIndent(problem, "", " ")
65+
fmt.Println(string(b))
66+
// Output: {
67+
// "type": "about:blank",
68+
// "title": "Forbidden",
69+
// "status": 403,
70+
// "detail": "You do not have sufficient funds to complete this transaction.",
71+
// "extensions": {
72+
// "balance": 30,
73+
// "accounts": [
74+
// "/account/12345",
75+
// "/account/67890"
76+
// ]
77+
// }
78+
// }
79+
}
80+
81+
func ExampleExtendedProblem_embedding() {
82+
type CreditProblem struct {
83+
problems.Problem
84+
85+
Balance float64 `json:"balance"`
86+
Accounts []string `json:"accounts"`
87+
}
88+
problem := &CreditProblem{
89+
Problem: *problems.New().
90+
WithStatus(http.StatusForbidden).
91+
WithDetail("You do not have sufficient funds to complete this transaction."),
92+
Balance: 30,
93+
Accounts: []string{"/account/12345", "/account/67890"},
94+
}
95+
96+
b, _ := json.MarshalIndent(problem, "", " ")
97+
fmt.Println(string(b))
98+
// Output: {
99+
// "type": "about:blank",
100+
// "title": "Forbidden",
101+
// "status": 403,
102+
// "detail": "You do not have sufficient funds to complete this transaction.",
103+
// "balance": 30,
104+
// "accounts": [
105+
// "/account/12345",
106+
// "/account/67890"
107+
// ]
108+
// }
109+
}

errors.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ var ErrTitleMustBeSet = fmt.Errorf("%s: problem title must be set", errPrefix)
1212
// valid URI when it is validated. The inner Err will contain the error
1313
// returned from attempting to parse the invalid URI.
1414
type ErrInvalidProblemType struct {
15-
Err error
15+
Err error
16+
Value string
1617
}
1718

1819
// NewErrInvalidProblemType returns a new ErrInvalidProblemType instance which
1920
// wraps the provided error.
20-
func NewErrInvalidProblemType(e error) error {
21+
func NewErrInvalidProblemType(value string, e error) error {
2122
return &ErrInvalidProblemType{
22-
Err: e,
23+
Err: e,
24+
Value: value,
2325
}
2426
}
2527

0 commit comments

Comments
 (0)