Repository: mercari/go-httpdoc
Branch: main
Commit: 76d5c80539b9
Files: 26
Total size: 96.8 KB
Directory structure:
gitextract_wz5xessz/
├── .github/
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── _example/
│ ├── README.md
│ ├── doc/
│ │ ├── protobuf.md
│ │ ├── simple.md
│ │ └── validate.md
│ ├── handler.go
│ ├── handler_proto_test.go
│ ├── handler_simple_test.go
│ ├── handler_validate_test.go
│ └── message.pb.go
├── doc.go
├── httpdoc.go
├── httpdoc_test.go
├── message_pb_test.go
├── proto/
│ └── message.proto
├── static/
│ ├── bindata.go
│ ├── static.go
│ └── tmpl/
│ └── doc.md.tmpl
├── template.go
├── template_test.go
├── validate.go
└── validate_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
Please read the CLA carefully before submitting your contribution to Mercari.
Under any circumstances, by submitting your contribution, you are deemed to accept and agree to be bound by the terms and conditions of the CLA.
https://www.mercari.com/cla/
================================================
FILE: .gitignore
================================================
httpdoc.md
coverage.txt
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [0.2.0] - 2018-02-13
In this release, we added breaking changes by [#18](https://github.com/mercari/go-httpdoc/pull/18). Now user can set custom asset function to each test cases. Since this added new field named `AssertFunc` to `TestCase` struct, the code which uses it without specifying field name will be broken. To migrate to new version easily, we add `NewTestCase` function. Check [#18](https://github.com/mercari/go-httpdoc/pull/18) and see how our example migrate to new `TestCase` by it.
### Added
- Add MkdirAll if not exist output directory [#14](https://github.com/mercari/go-httpdoc/pull/14)
### Changed
- Allow to provide custom assert function to `TestCase` [#18](https://github.com/mercari/go-httpdoc/pull/18)
### Removed
- Stop support Go 1.6.x [#20](https://github.com/mercari/go-httpdoc/pull/20)
## [0.1.0] - 2017-06-01
Initial release.
### Added
- Fundamental features
================================================
FILE: LICENSE
================================================
Copyright (c) 2017 Mercari, Inc.
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# go-httpdoc [][godoc]
[godoc]: http://godoc.org/go.mercari.io/go-httpdoc
`go-httpdoc` is a Golang package to generate API documentation from [`httptest`](https://golang.org/pkg/net/http/httptest/) test cases.
It provides a simple http middleware which records http requests and responses from tests and generates documentation automatically in markdown format. See [Sample Documentation](/_example/doc/validate.md). It also provides a way to validate values are equal to what you expect with annotation (e.g., you can add a description for headers, params or response fields). If you write proper tests, it will generate usable documentation (namely, it forces you to write good tests).
Not only JSON request and response but it also supports [protocol buffer](https://developers.google.com/protocol-buffers/). See [Sample ProtoBuf Documentation](/_example/doc/protobuf.md)).
See usage and example in [GoDoc](https://godoc.org/go.mercari.io/go-httpdoc).
*NOTE*: This package is experimental and may make backward-incompatible changes.
## Prerequisites
go-httpdoc requires Go 1.7 or later.
## Install
Use go get:
```
$ go get -u go.mercari.io/go-httpdoc
```
## Usage
All usage are described in [GoDoc](https://godoc.org/go.mercari.io/go-httpdoc).
To generate documentation, set the following env var:
```bash
$ export HTTPDOC=1
```
## Reference
The original idea came from [r7kamura/autodoc](https://github.com/r7kamura/autodoc) (rack middleware).
For struct inspection in validator, it uses [tenntenn/gpath](https://github.com/tenntenn/gpath) package.
================================================
FILE: _example/README.md
================================================
# httpdoc example
This directory contains some examples of `httpdoc`.
- [`handler_simple_test.go`](/_example/handler_simple_test.go) generates [`doc/simple.md`](/_example/doc/simple.md)
- [`handler_validate_test.go`](/_example/handler_validate_test.go) generates [`doc/validate.md`](/_example/doc/validate.md)
- [`handler_proto_test.go`](/_example/handler_proto_test.go) generates [`doc/protobuf.md`](/_example/doc/protobuf.md)
To generate documentation, run the following command:
```bash
$ env HTTPDOC=1 go test -v
```
One example uses protocol buffer, message definition is in [`../proto`](../proto) directory. To generate code from that, run the following command:
```bash
# Install protoc-gen-go if you don't have it
$ go get -u github.com/golang/protobuf/protoc-gen-go
$ protoc -I=./../proto --gofast_out=./ ../proto/message.proto
```
================================================
FILE: _example/doc/protobuf.md
================================================
# API doc
This is API documentation for Example API (with protobuf). This is generated by `httpdoc`. Don't edit by hand.
## Table of contents
- [[200] GET /v2/user/169743](#200-get-v2user169743)
## [200] GET /v2/user/169743
Get a user
### Request
### Response
Headers
| Name | Value | Description |
| ----- | :----- | :--------- |
| Content-Type | application/protobuf | |
Response fields
| Name | Value | Description |
| ----- | :----- | :--------- |
| Name | Immortan Joe | User name |
| Setting.Email | immortan@madmax.com | User email |
Response example
Click to expand code.
```javascript
{
"id": 169743,
"name": "Immortan Joe",
"active": true,
"setting": {
"email": "immortan@madmax.com"
}
}
```
================================================
FILE: _example/doc/simple.md
================================================
# API doc
This is API documentation for Example API (simple). This is generated by `httpdoc`. Don't edit by hand.
## Table of contents
- [[200] POST /v1/user](#200-post-v1user)
## [200] POST /v1/user
Create a new user
### Request
Parameters
| Name | Value | Description |
| ----- | :----- | :--------- |
| token | 12345 | |
Headers
| Name | Value | Description |
| ----- | :----- | :--------- |
| Accept-Encoding | gzip | |
| User-Agent | Go-http-client/1.1 | |
| X-Version | 2 | |
Request example
Click to expand code.
```javascript
{
"name": "tcnksm",
"email": "tcnksm@mercari.com",
"attribute": {
"birthday": "1988-11-24"
}
}
```
### Response
Headers
| Name | Value | Description |
| ----- | :----- | :--------- |
| Content-Type | application/json | |
Response example
Click to expand code.
```javascript
{
"id": 11241988,
"name": "tcnksm"
}
```
================================================
FILE: _example/doc/validate.md
================================================
# API doc
This is API documentation for Example API (with validation). This is generated by `httpdoc`. Don't edit by hand.
## Table of contents
- [[200] POST /v1/user](#200-post-v1user)
## [200] POST /v1/user
Create a new user
### Request
Parameters
| Name | Value | Description |
| ----- | :----- | :--------- |
| pretty | | Pretty print response message |
| token | 12345 | Request token |
Headers
| Name | Value | Description |
| ----- | :----- | :--------- |
| X-Version | 2 | Request API version |
Request fields
| Name | Value | Description |
| ----- | :----- | :--------- |
| Name | tcnksm | User Name |
| Email | tcnksm@mercari.com | User email address |
| Attribute.Birthday | 1988-11-24 | User birthday YYYY-MM-DD format |
Request example
Click to expand code.
```javascript
{
"name": "tcnksm",
"email": "tcnksm@mercari.com",
"attribute": {
"birthday": "1988-11-24"
}
}
```
### Response
Headers
| Name | Value | Description |
| ----- | :----- | :--------- |
| Content-Type | application/json | |
Response fields
| Name | Value | Description |
| ----- | :----- | :--------- |
| ID | 11241988 | User ID assigned |
Response example
Click to expand code.
```javascript
{
"id": 11241988,
"name": "tcnksm"
}
```
================================================
FILE: _example/handler.go
================================================
package main
import (
"encoding/json"
"net/http"
)
type createUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Attribute attribute `json:"attribute"`
}
type attribute struct {
Birthday string `json:"birthday,omitempty"`
Gender string `json:"gender,omitempty"`
}
type createUserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
}
type userHandler struct {
}
type userProtoHandler struct {
}
func (h *userHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if v := r.URL.Query().Get("token"); v != "12345" {
w.WriteHeader(http.StatusUnauthorized)
return
}
var request createUserRequest
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&request); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
// Some process...
response := createUserResponse{
ID: 11241988,
Name: request.Name,
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
encoder.Encode(&response)
}
func (h *userProtoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
response := &UserProtoResponse{
Id: 169743,
Name: "Immortan Joe",
Active: true,
Setting: &UserProtoResponse_Setting{
Email: "immortan@madmax.com",
},
}
buf, err := response.Marshal()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/protobuf")
w.Write(buf)
}
================================================
FILE: _example/handler_proto_test.go
================================================
package main
import (
"net/http"
"net/http/httptest"
"testing"
httpdoc "go.mercari.io/go-httpdoc"
)
func TestUserHandlerWithProtobuf(t *testing.T) {
document := &httpdoc.Document{
Name: "Example API (with protobuf)",
ExcludeHeaders: []string{
"Accept-Encoding",
},
}
defer func() {
if err := document.Generate("doc/protobuf.md"); err != nil {
t.Fatalf("err: %s", err)
}
}()
mux := http.NewServeMux()
mux.Handle("/v2/user/", httpdoc.Record(&userProtoHandler{}, document, &httpdoc.RecordOption{
Description: "Get a user",
ExcludeHeaders: []string{
"User-Agent",
"Content-Length",
},
WithValidate: func(validator *httpdoc.Validator) {
validator.ResponseBody(t, []httpdoc.TestCase{
httpdoc.NewTestCase("Name", "Immortan Joe", "User name"),
httpdoc.NewTestCase("Setting.Email", "immortan@madmax.com", "User email")},
&UserProtoResponse{},
)
},
WithProtoBuffer: &httpdoc.ProtoBufferOption{
ResponseUnmarshaler: &UserProtoResponse{},
},
}))
testServer := httptest.NewServer(mux)
defer testServer.Close()
req, err := http.NewRequest("GET", testServer.URL+"/v2/user/169743", nil)
if err != nil {
t.Fatal(err)
}
if _, err := http.DefaultClient.Do(req); err != nil {
t.Fatal(err)
}
}
================================================
FILE: _example/handler_simple_test.go
================================================
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
httpdoc "go.mercari.io/go-httpdoc"
)
func TestUserHandlerSimple(t *testing.T) {
document := &httpdoc.Document{
Name: "Example API (simple)",
ExcludeHeaders: []string{"Content-Length"},
}
defer func() {
if err := document.Generate("doc/simple.md"); err != nil {
t.Fatalf("err: %s", err)
}
}()
mux := http.NewServeMux()
mux.Handle("/v1/user", httpdoc.Record(&userHandler{}, document, &httpdoc.RecordOption{
Description: "Create a new user",
}))
testServer := httptest.NewServer(mux)
defer testServer.Close()
req := testNewRequest(t, testServer.URL+"/v1/user?token=12345")
if _, err := http.DefaultClient.Do(req); err != nil {
t.Fatalf("err: %s", err)
}
}
func testNewRequest(t *testing.T, urlStr string) *http.Request {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetIndent("", " ")
encoder.Encode(&createUserRequest{
Name: "tcnksm",
Email: "tcnksm@mercari.com",
Attribute: attribute{
Birthday: "1988-11-24",
},
})
req, err := http.NewRequest("POST", urlStr, &buf)
if err != nil {
t.Fatalf("err: %s", err)
}
req.Header.Add("X-Version", "2")
return req
}
================================================
FILE: _example/handler_validate_test.go
================================================
package main
import (
"net/http"
"net/http/httptest"
"testing"
httpdoc "go.mercari.io/go-httpdoc"
)
func TestUserHandlerWithValidate(t *testing.T) {
document := &httpdoc.Document{
Name: "Example API (with validation)",
ExcludeHeaders: []string{
"Accept-Encoding",
},
}
defer func() {
if err := document.Generate("doc/validate.md"); err != nil {
t.Fatalf("err: %s", err)
}
}()
mux := http.NewServeMux()
mux.Handle("/v1/user", httpdoc.Record(&userHandler{}, document, &httpdoc.RecordOption{
Description: "Create a new user",
ExcludeHeaders: []string{
"User-Agent",
"Content-Length",
},
// WithValidate option, you can validate various http request & parameter values.
// It checks handler gets the expected value or not and assert when it's different.
// You can annotate what kind of value you expect (description) in each validation
// and it will be the document.
WithValidate: func(validator *httpdoc.Validator) {
validator.RequestParams(t, []httpdoc.TestCase{
httpdoc.NewTestCase("token", "12345", "Request token"),
httpdoc.NewTestCase("pretty", "", "Pretty print response message"),
})
validator.RequestHeaders(t, []httpdoc.TestCase{
httpdoc.NewTestCase("X-Version", "2", "Request API version"),
})
validator.RequestBody(t, []httpdoc.TestCase{
httpdoc.NewTestCase("Name", "tcnksm", "User Name"),
httpdoc.NewTestCase("Email", "tcnksm@mercari.com", "User email address"),
httpdoc.NewTestCase("Attribute.Birthday", "1988-11-24", "User birthday YYYY-MM-DD format")},
&createUserRequest{},
)
validator.ResponseStatusCode(t, http.StatusOK)
validator.ResponseBody(t, []httpdoc.TestCase{
httpdoc.NewTestCase("ID", 11241988, "User ID assigned")},
&createUserResponse{},
)
},
}))
testServer := httptest.NewServer(mux)
defer testServer.Close()
req := testNewRequest(t, testServer.URL+"/v1/user?token=12345")
if _, err := http.DefaultClient.Do(req); err != nil {
t.Fatalf("err: %s", err)
}
}
================================================
FILE: _example/message.pb.go
================================================
// Code generated by protoc-gen-gogo.
// source: message.proto
// DO NOT EDIT!
/*
Package httpdoc is a generated protocol buffer package.
It is generated from these files:
message.proto
It has these top-level messages:
UserProtoRequest
UserProtoResponse
*/
package main
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import io "io"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type UserProtoRequest struct {
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
}
func (m *UserProtoRequest) Reset() { *m = UserProtoRequest{} }
func (m *UserProtoRequest) String() string { return proto.CompactTextString(m) }
func (*UserProtoRequest) ProtoMessage() {}
func (*UserProtoRequest) Descriptor() ([]byte, []int) { return fileDescriptorMessage, []int{0} }
func (m *UserProtoRequest) GetId() int32 {
if m != nil {
return m.Id
}
return 0
}
func (m *UserProtoRequest) GetName() string {
if m != nil {
return m.Name
}
return ""
}
type UserProtoResponse struct {
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Active bool `protobuf:"varint,3,opt,name=active,proto3" json:"active,omitempty"`
Setting *UserProtoResponse_Setting `protobuf:"bytes,4,opt,name=setting" json:"setting,omitempty"`
}
func (m *UserProtoResponse) Reset() { *m = UserProtoResponse{} }
func (m *UserProtoResponse) String() string { return proto.CompactTextString(m) }
func (*UserProtoResponse) ProtoMessage() {}
func (*UserProtoResponse) Descriptor() ([]byte, []int) { return fileDescriptorMessage, []int{1} }
func (m *UserProtoResponse) GetId() int32 {
if m != nil {
return m.Id
}
return 0
}
func (m *UserProtoResponse) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *UserProtoResponse) GetActive() bool {
if m != nil {
return m.Active
}
return false
}
func (m *UserProtoResponse) GetSetting() *UserProtoResponse_Setting {
if m != nil {
return m.Setting
}
return nil
}
type UserProtoResponse_Setting struct {
Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"`
}
func (m *UserProtoResponse_Setting) Reset() { *m = UserProtoResponse_Setting{} }
func (m *UserProtoResponse_Setting) String() string { return proto.CompactTextString(m) }
func (*UserProtoResponse_Setting) ProtoMessage() {}
func (*UserProtoResponse_Setting) Descriptor() ([]byte, []int) {
return fileDescriptorMessage, []int{1, 0}
}
func (m *UserProtoResponse_Setting) GetEmail() string {
if m != nil {
return m.Email
}
return ""
}
func init() {
proto.RegisterType((*UserProtoRequest)(nil), "httpdoc.UserProtoRequest")
proto.RegisterType((*UserProtoResponse)(nil), "httpdoc.UserProtoResponse")
proto.RegisterType((*UserProtoResponse_Setting)(nil), "httpdoc.UserProtoResponse.Setting")
}
func (m *UserProtoRequest) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *UserProtoRequest) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if m.Id != 0 {
dAtA[i] = 0x8
i++
i = encodeVarintMessage(dAtA, i, uint64(m.Id))
}
if len(m.Name) > 0 {
dAtA[i] = 0x12
i++
i = encodeVarintMessage(dAtA, i, uint64(len(m.Name)))
i += copy(dAtA[i:], m.Name)
}
return i, nil
}
func (m *UserProtoResponse) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *UserProtoResponse) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if m.Id != 0 {
dAtA[i] = 0x8
i++
i = encodeVarintMessage(dAtA, i, uint64(m.Id))
}
if len(m.Name) > 0 {
dAtA[i] = 0x12
i++
i = encodeVarintMessage(dAtA, i, uint64(len(m.Name)))
i += copy(dAtA[i:], m.Name)
}
if m.Active {
dAtA[i] = 0x18
i++
if m.Active {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i++
}
if m.Setting != nil {
dAtA[i] = 0x22
i++
i = encodeVarintMessage(dAtA, i, uint64(m.Setting.Size()))
n1, err := m.Setting.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n1
}
return i, nil
}
func (m *UserProtoResponse_Setting) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *UserProtoResponse_Setting) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.Email) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintMessage(dAtA, i, uint64(len(m.Email)))
i += copy(dAtA[i:], m.Email)
}
return i, nil
}
func encodeFixed64Message(dAtA []byte, offset int, v uint64) int {
dAtA[offset] = uint8(v)
dAtA[offset+1] = uint8(v >> 8)
dAtA[offset+2] = uint8(v >> 16)
dAtA[offset+3] = uint8(v >> 24)
dAtA[offset+4] = uint8(v >> 32)
dAtA[offset+5] = uint8(v >> 40)
dAtA[offset+6] = uint8(v >> 48)
dAtA[offset+7] = uint8(v >> 56)
return offset + 8
}
func encodeFixed32Message(dAtA []byte, offset int, v uint32) int {
dAtA[offset] = uint8(v)
dAtA[offset+1] = uint8(v >> 8)
dAtA[offset+2] = uint8(v >> 16)
dAtA[offset+3] = uint8(v >> 24)
return offset + 4
}
func encodeVarintMessage(dAtA []byte, offset int, v uint64) int {
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
}
func (m *UserProtoRequest) Size() (n int) {
var l int
_ = l
if m.Id != 0 {
n += 1 + sovMessage(uint64(m.Id))
}
l = len(m.Name)
if l > 0 {
n += 1 + l + sovMessage(uint64(l))
}
return n
}
func (m *UserProtoResponse) Size() (n int) {
var l int
_ = l
if m.Id != 0 {
n += 1 + sovMessage(uint64(m.Id))
}
l = len(m.Name)
if l > 0 {
n += 1 + l + sovMessage(uint64(l))
}
if m.Active {
n += 2
}
if m.Setting != nil {
l = m.Setting.Size()
n += 1 + l + sovMessage(uint64(l))
}
return n
}
func (m *UserProtoResponse_Setting) Size() (n int) {
var l int
_ = l
l = len(m.Email)
if l > 0 {
n += 1 + l + sovMessage(uint64(l))
}
return n
}
func sovMessage(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
}
func sozMessage(x uint64) (n int) {
return sovMessage(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *UserProtoRequest) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: UserProtoRequest: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: UserProtoRequest: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType)
}
m.Id = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Id |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthMessage
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Name = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipMessage(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthMessage
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *UserProtoResponse) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: UserProtoResponse: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: UserProtoResponse: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType)
}
m.Id = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Id |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthMessage
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Name = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Active", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
m.Active = bool(v != 0)
case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Setting", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthMessage
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Setting == nil {
m.Setting = &UserProtoResponse_Setting{}
}
if err := m.Setting.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipMessage(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthMessage
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *UserProtoResponse_Setting) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: Setting: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: Setting: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Email", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthMessage
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Email = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipMessage(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthMessage
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipMessage(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowMessage
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowMessage
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowMessage
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthMessage
}
return iNdEx, nil
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowMessage
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipMessage(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
case 4:
return iNdEx, nil
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
}
panic("unreachable")
}
var (
ErrInvalidLengthMessage = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowMessage = fmt.Errorf("proto: integer overflow")
)
func init() { proto.RegisterFile("message.proto", fileDescriptorMessage) }
var fileDescriptorMessage = []byte{
// 211 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xcd, 0x4d, 0x2d, 0x2e,
0x4e, 0x4c, 0x4f, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xcf, 0x28, 0x29, 0x29, 0x48,
0xc9, 0x4f, 0x56, 0x32, 0xe3, 0x12, 0x08, 0x2d, 0x4e, 0x2d, 0x0a, 0x00, 0x89, 0x06, 0xa5, 0x16,
0x96, 0xa6, 0x16, 0x97, 0x08, 0xf1, 0x71, 0x31, 0x65, 0xa6, 0x48, 0x30, 0x2a, 0x30, 0x6a, 0xb0,
0x06, 0x31, 0x65, 0xa6, 0x08, 0x09, 0x71, 0xb1, 0xe4, 0x25, 0xe6, 0xa6, 0x4a, 0x30, 0x29, 0x30,
0x6a, 0x70, 0x06, 0x81, 0xd9, 0x4a, 0xeb, 0x18, 0xb9, 0x04, 0x91, 0x34, 0x16, 0x17, 0xe4, 0xe7,
0x15, 0xa7, 0x12, 0xa3, 0x53, 0x48, 0x8c, 0x8b, 0x2d, 0x31, 0xb9, 0x24, 0xb3, 0x2c, 0x55, 0x82,
0x59, 0x81, 0x51, 0x83, 0x23, 0x08, 0xca, 0x13, 0xb2, 0xe1, 0x62, 0x2f, 0x4e, 0x2d, 0x29, 0xc9,
0xcc, 0x4b, 0x97, 0x60, 0x51, 0x60, 0xd4, 0xe0, 0x36, 0x52, 0xd2, 0x83, 0x3a, 0x52, 0x0f, 0xc3,
0x22, 0xbd, 0x60, 0x88, 0xca, 0x20, 0x98, 0x16, 0x29, 0x79, 0x2e, 0x76, 0xa8, 0x98, 0x90, 0x08,
0x17, 0x6b, 0x6a, 0x6e, 0x62, 0x66, 0x0e, 0xd8, 0x1d, 0x9c, 0x41, 0x10, 0x8e, 0x93, 0xc0, 0x89,
0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe3, 0xb1, 0x1c, 0x43,
0x12, 0x1b, 0x38, 0x28, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xc5, 0x23, 0xb1, 0xd2, 0x1b,
0x01, 0x00, 0x00,
}
================================================
FILE: doc.go
================================================
// Package httpdoc is a Golang package for generating API documentation from httptest test cases.
//
// It provides a simple http middleware for recording various http requst & response values you use in your tests
// and automatically arranges and generates them as usable documentation in markdown format. It also provides a way
// to validate values are equal to what you expect with annotation (e.g., you can add a description for headers,
// params or response fields).
//
// See example document output, https://github.com/mercari/go-httpdoc/blob/master/_example/doc/validate.md
package httpdoc // import "go.mercari.io/go-httpdoc"
================================================
FILE: httpdoc.go
================================================
package httpdoc
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"sort"
"github.com/golang/protobuf/proto"
)
const (
// EnvHTTPDoc is the environmental variable that determines if Generate func generates documentation
// to the given file or not. By default, it does not generate. If this variable is not empty, then it does.
EnvHTTPDoc = "HTTPDOC"
)
// Document stores recorded results by Record middleware.
type Document struct {
// Name is API documentation name.
Name string
// ExcludeHeaders is list of headers to exclude from documentation.
// For example, you may do not need `Content-Length` header. This is applied all entries (endpoints).
// If you want to exclude header only in specific endpoint, then use `RecordOption.ExcludeHeaders`.
ExcludeHeaders []string
// Entries stores all recorded results by Record middleware. Normally, you don't need to modify this.
// This is exported just for templating.
Entries []Entry
// tmpl is template file to use. Currently this is only static/tmpl/doc.md.tmpl
tmpl string
logger *log.Logger
}
// Entry is recorded results by Record middleware. Normally, you don't need to modify this.
// All fields are exported just for templating.
type Entry struct {
// Description is description of endpoint.
Description string
// Method is HTTP method.
Method string
// Path is request path.
Path string
RequestParams []Data
RequestHeaders []Data
RequestFields []Data
// RequestExample is request body example. If you use plain text for json for response body
// it uses it here without modification. If you use protocol buffer format for your request body
// it unmarshals it in the given struct and encodes it into json format.
RequestExample string
ResponseStatusCode int
ResponseHeaders []Data
ResponseFields []Data
// ResponseExample is response body example. If you use plain text for json for response body
// it uses it here without modification. If you use protocol buffer format for your response body
// it unmarshals it in the given struct and encodes it into json format.
ResponseExample string
}
// RecordOption is option for Record middleware.
type RecordOption struct {
// Description is description of endpoint. This is used for Entry.Description.
Description string
// ExcludeHeaders is list of headers to exclude from documentation.
// This is applied only one entry (endpoint). If you want to exclude header in all endpoints
// use `Document.ExcludeHeaders`.
ExcludeHeaders []string
// WithValidate option, you can validate various http request & response parameter values.
// It inspects values which handler receives and checks it's expected or not.
// If not it asserts and fails the test. If ok, uses it for documentation entry.
//
// Not only validate, you can add an annotation to each values (e.g., what does the header
// means?) and it's used for documentation.
//
// See more usage in Validator methods.
WithValidate func(*Validator)
// WithProtoBuffer option is used for protocol buffer request & response.
WithProtoBuffer *ProtoBufferOption
}
// ProtoBufferOption is option for protocol buffer.
type ProtoBufferOption struct {
// RequestUnmarshaler is used to unmarshal protocol buffer encoded request body.
// This is used for generating human readable request example (json format).
RequestUnmarshaler proto.Unmarshaler
// ResponseUnmarshaler is used to unmarshal protocol buffer encoded response body.
// This is used for generating human readable response example (json format).
ResponseUnmarshaler proto.Unmarshaler
}
// Data represents a request or response parameter value. Normally, you don't need to modify this.
// All fields are exported just for templating.
type Data struct {
// Name is header or params, field name.
Name string
// Value is actual value handler receives.
Value interface{}
// Description is description for this data. You can provide this via a validator.
Description string
}
type byName []Data
func (n byName) Len() int { return len(n) }
func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name }
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
// Record is a http middleware. It records all request & response values which the given http handler
// receives & response and save it in the given Document.
func Record(next http.Handler, document *Document, opt *RecordOption) http.Handler {
// If not option is provided, initialize it.
if opt == nil {
opt = &RecordOption{}
}
if document.logger == nil {
document.logger = log.New(os.Stderr, "", log.LstdFlags)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create a new responseWriter it captures status code and response body.
rw := responseWriter{
ResponseWriter: w,
}
// Create a tee reader and stores request body.
// Because of this, handler must read request body to record.
var requestBody bytes.Buffer
r.Body = ioutil.NopCloser(io.TeeReader(r.Body, &requestBody))
next.ServeHTTP(&rw, r)
// If protobuffer option is provided, use protoUnmarshalFunc for
// validator, by default, use json unmashal func.
unmarshalFunc := defaultUnmarshalFunc
if opt.WithProtoBuffer != nil {
unmarshalFunc = protoUnmarshalFunc
}
validator := &Validator{
record: &record{
requestParams: r.URL.Query(),
requestHeaders: r.Header,
requestBody: requestBody.Bytes(),
responseStatusCode: rw.statusCode,
responseHeaders: rw.Header(),
responseBody: rw.responseBody,
},
unmarshalFunc: unmarshalFunc,
assertFunc: defaultAssertFunc,
}
if opt.WithValidate != nil {
opt.WithValidate(validator)
}
excludeHeaders := append(opt.ExcludeHeaders, document.ExcludeHeaders...)
requestParams := mergeData(validator.requestParams, convertHeaders(r.URL.Query()))
requestHeaders := mergeData(validator.requestHeaders, convertHeaders(r.Header))
requestHeaders = excludeData(requestHeaders, excludeHeaders)
responseHeaders := mergeData(validator.responseHeaders, convertHeaders(rw.Header()))
responseHeaders = excludeData(responseHeaders, excludeHeaders)
requestExample := requestBody.String()
responseExample := string(rw.responseBody)
if opt.WithProtoBuffer != nil {
// FIXME(tcnksm): Want to use jsonpb but sometimes panic happens while marshalling....
if unmarshaler := opt.WithProtoBuffer.RequestUnmarshaler; unmarshaler != nil {
unmarshaler.Unmarshal(requestBody.Bytes())
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.Encode(unmarshaler)
s := buf.String()
buf.Reset()
json.Indent(&buf, []byte(s), "", " ")
requestExample = buf.String()
}
if unmarshaler := opt.WithProtoBuffer.ResponseUnmarshaler; unmarshaler != nil {
unmarshaler.Unmarshal(rw.responseBody)
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.Encode(unmarshaler)
s := buf.String()
buf.Reset()
json.Indent(&buf, []byte(s), "", " ")
responseExample = buf.String()
}
}
entry := Entry{
Description: opt.Description,
Method: r.Method,
Path: r.URL.Path,
RequestHeaders: requestHeaders,
RequestParams: requestParams,
RequestFields: validator.requestFields,
RequestExample: requestExample,
ResponseStatusCode: rw.statusCode,
ResponseHeaders: responseHeaders,
ResponseFields: validator.responseFields,
ResponseExample: responseExample,
}
entry.format()
document.Entries = append(document.Entries, entry)
})
}
// format sorts entry data to prevent results updated everytime.
func (e *Entry) format() error {
sort.Sort(byName(e.RequestHeaders))
sort.Sort(byName(e.RequestParams))
return nil
}
type responseWriter struct {
statusCode int
responseBody []byte
http.ResponseWriter
}
func (w *responseWriter) Write(buf []byte) (int, error) {
w.responseBody = buf
return w.ResponseWriter.Write(buf)
}
func (w *responseWriter) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
// convertHeaders convert HTTP header to httpdoc description format.
func convertHeaders(headers map[string][]string) []Data {
d := make([]Data, 0, len(headers))
for k, v := range headers {
data := Data{
Name: k,
Value: v[0],
}
d = append(d, data)
}
return d
}
// mergeData merges 2 Data slice into 1 slice without duplication.
// If duplicated, item in 1st slice is used.
func mergeData(a, b []Data) []Data {
newData := make([]Data, len(a))
copy(newData, a)
for _, d1 := range b {
var contain bool
for _, d2 := range a {
if d1.Name == d2.Name {
contain = true
}
}
if !contain {
newData = append(newData, d1)
}
}
return newData
}
// excludeData excludes data which is given.
func excludeData(target []Data, excludes ...[]string) []Data {
newData := make([]Data, 0, len(target))
for _, d := range target {
var contain bool
for _, exclude := range excludes {
for _, name := range exclude {
if d.Name == name {
contain = true
}
}
}
if !contain {
newData = append(newData, d)
}
}
return newData
}
================================================
FILE: httpdoc_test.go
================================================
package httpdoc
import (
"bytes"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"reflect"
"sort"
"strings"
"testing"
"github.com/golang/protobuf/proto"
)
var (
testExcludeHeaders = []string{"User-Agent", "Accept-Encoding", "Content-Length"}
testHandler = func(w http.ResponseWriter, r *http.Request) {
// To record, request example, request body must be read in handler
io.Copy(ioutil.Discard, r.Body)
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "text/plain")
w.Write([]byte("hello"))
}
testHandlerProto = func(w http.ResponseWriter, r *http.Request) {
// To record, request example, request body must be read in handler
io.Copy(ioutil.Discard, r.Body)
response := &UserProtoResponse{
Id: 7089,
Name: "tcnksm",
Active: true,
}
buf, _ := response.Marshal()
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/protobuf")
w.Write(buf)
}
)
func TestRecord(t *testing.T) {
cases := []struct {
path string
handler http.HandlerFunc
recordOption *RecordOption
requestMethod string
requestParam string
requestBody io.Reader
want Entry
}{
{
"/v1/hello",
testHandler,
nil,
"GET",
"?token=123456&pretty=true",
strings.NewReader("hello"),
Entry{
Description: "",
Method: "GET",
Path: "/v1/hello",
RequestParams: []Data{
{"pretty", "true", ""},
{"token", "123456", ""},
},
RequestHeaders: []Data{
{"Accept-Encoding", "gzip", ""},
{"Content-Length", "5", ""},
{"User-Agent", "Go-http-client/1.1", ""},
},
RequestFields: nil,
RequestExample: "hello",
ResponseStatusCode: http.StatusOK,
ResponseHeaders: []Data{
{"Content-Type", "text/plain", ""},
},
ResponseExample: "hello",
},
},
{
"/v1/hello",
testHandler,
&RecordOption{
ExcludeHeaders: testExcludeHeaders,
},
"GET",
"",
strings.NewReader("hello"),
Entry{
Description: "",
Method: "GET",
Path: "/v1/hello",
RequestParams: []Data{},
RequestHeaders: []Data{},
RequestFields: nil,
RequestExample: "hello",
ResponseStatusCode: http.StatusOK,
ResponseHeaders: []Data{
{"Content-Type", "text/plain", ""},
},
ResponseExample: "hello",
},
},
{
"/v1/hello",
testHandler,
&RecordOption{
ExcludeHeaders: testExcludeHeaders,
WithValidate: func(v *Validator) {
v.RequestParams(t, []TestCase{
NewTestCase("token", "123456", "Test token"),
})
},
},
"GET",
"?token=123456",
strings.NewReader("hello"),
Entry{
Description: "",
Method: "GET",
Path: "/v1/hello",
RequestParams: []Data{
{"token", "123456", "Test token"},
},
RequestHeaders: []Data{},
RequestFields: nil,
RequestExample: "hello",
ResponseStatusCode: http.StatusOK,
ResponseHeaders: []Data{
{"Content-Type", "text/plain", ""},
},
ResponseExample: "hello",
},
},
}
for _, tc := range cases {
document := &Document{}
mux := http.NewServeMux()
mux.Handle(tc.path, Record(tc.handler, document, tc.recordOption))
testServer := httptest.NewServer(mux)
client := http.DefaultClient
req, err := http.NewRequest(tc.requestMethod, testServer.URL+tc.path+tc.requestParam, tc.requestBody)
if err != nil {
t.Fatal(err)
}
if _, err := client.Do(req); err != nil {
t.Fatal(err)
}
if len(document.Entries) != 1 {
t.Fatalf("expect doc records 1 entry")
}
got := document.Entries[0]
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("\ngot %#v\nwant %#v", got, tc.want)
}
testServer.Close()
}
}
func TestRecord_Proto(t *testing.T) {
cases := []struct {
path string
handler http.HandlerFunc
recordOption *RecordOption
requestMethod string
requestParam string
requestBody proto.Marshaler
want Entry
}{
{
"/v1/hello_proto",
testHandlerProto,
&RecordOption{
ExcludeHeaders: testExcludeHeaders,
WithProtoBuffer: &ProtoBufferOption{
RequestUnmarshaler: &UserProtoRequest{},
ResponseUnmarshaler: &UserProtoResponse{},
},
},
"GET",
"",
&UserProtoRequest{
Id: 7089,
Name: "tcnksm",
},
Entry{
Description: "",
Method: "GET",
Path: "/v1/hello_proto",
RequestParams: []Data{},
RequestHeaders: []Data{},
RequestFields: nil,
RequestExample: `{
"id": 7089,
"name": "tcnksm"
}
`,
ResponseStatusCode: http.StatusOK,
ResponseHeaders: []Data{
{"Content-Type", "application/protobuf", ""},
},
ResponseExample: `{
"id": 7089,
"name": "tcnksm",
"active": true
}
`,
},
},
}
for _, tc := range cases {
document := &Document{}
mux := http.NewServeMux()
mux.Handle(tc.path, Record(tc.handler, document, tc.recordOption))
testServer := httptest.NewServer(mux)
client := http.DefaultClient
buf, err := tc.requestBody.Marshal()
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest(tc.requestMethod, testServer.URL+tc.path+tc.requestParam, bytes.NewReader(buf))
if err != nil {
t.Fatal(err)
}
if _, err := client.Do(req); err != nil {
t.Fatal(err)
}
if len(document.Entries) != 1 {
t.Fatalf("expect doc records 1 entry")
}
got := document.Entries[0]
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("\ngot %#v\nwant %#v", got, tc.want)
}
testServer.Close()
}
}
func TestConvertHeaders(t *testing.T) {
input := map[string][]string{
"Content-Type": []string{"application/json"},
"X-API-Version": []string{"1.1.2"},
}
got := convertHeaders(input)
want := []Data{
{
Name: "Content-Type",
Value: "application/json",
Description: "",
},
{
Name: "X-API-Version",
Value: "1.1.2",
Description: "",
},
}
sort.Sort(byName(got))
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
}
func TestMergeData(t *testing.T) {
a := []Data{
{Name: "1"},
{Name: "2"},
{Name: "3", Description: "this is 3"},
{Name: "4", Description: "this is 4"},
}
b := []Data{
{Name: "3"},
{Name: "4"},
{Name: "5"},
}
got := mergeData(a, b)
want := []Data{
{Name: "1"},
{Name: "2"},
{Name: "3", Description: "this is 3"},
{Name: "4", Description: "this is 4"},
{Name: "5"},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
}
func TestExcludeData(t *testing.T) {
input := []Data{
{Name: "1"},
{Name: "2"},
{Name: "3"},
{Name: "4"},
{Name: "5"},
}
got := excludeData(input, []string{"1", "2"}, []string{"3"}, []string{"4"})
want := []Data{
{
Name: "5",
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
}
func TestResponseWriter_Write(t *testing.T) {
}
func TestResponseWriter_WriteHeader(t *testing.T) {
}
================================================
FILE: message_pb_test.go
================================================
// Code generated by protoc-gen-gogo.
// source: message.proto
// DO NOT EDIT!
/*
Package httpdoc is a generated protocol buffer package.
It is generated from these files:
message.proto
It has these top-level messages:
UserProtoRequest
UserProtoResponse
*/
package httpdoc
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import io "io"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type UserProtoRequest struct {
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
}
func (m *UserProtoRequest) Reset() { *m = UserProtoRequest{} }
func (m *UserProtoRequest) String() string { return proto.CompactTextString(m) }
func (*UserProtoRequest) ProtoMessage() {}
func (*UserProtoRequest) Descriptor() ([]byte, []int) { return fileDescriptorMessage, []int{0} }
func (m *UserProtoRequest) GetId() int32 {
if m != nil {
return m.Id
}
return 0
}
func (m *UserProtoRequest) GetName() string {
if m != nil {
return m.Name
}
return ""
}
type UserProtoResponse struct {
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Active bool `protobuf:"varint,3,opt,name=active,proto3" json:"active,omitempty"`
Setting *UserProtoResponse_Setting `protobuf:"bytes,4,opt,name=setting" json:"setting,omitempty"`
}
func (m *UserProtoResponse) Reset() { *m = UserProtoResponse{} }
func (m *UserProtoResponse) String() string { return proto.CompactTextString(m) }
func (*UserProtoResponse) ProtoMessage() {}
func (*UserProtoResponse) Descriptor() ([]byte, []int) { return fileDescriptorMessage, []int{1} }
func (m *UserProtoResponse) GetId() int32 {
if m != nil {
return m.Id
}
return 0
}
func (m *UserProtoResponse) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *UserProtoResponse) GetActive() bool {
if m != nil {
return m.Active
}
return false
}
func (m *UserProtoResponse) GetSetting() *UserProtoResponse_Setting {
if m != nil {
return m.Setting
}
return nil
}
type UserProtoResponse_Setting struct {
Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"`
}
func (m *UserProtoResponse_Setting) Reset() { *m = UserProtoResponse_Setting{} }
func (m *UserProtoResponse_Setting) String() string { return proto.CompactTextString(m) }
func (*UserProtoResponse_Setting) ProtoMessage() {}
func (*UserProtoResponse_Setting) Descriptor() ([]byte, []int) {
return fileDescriptorMessage, []int{1, 0}
}
func (m *UserProtoResponse_Setting) GetEmail() string {
if m != nil {
return m.Email
}
return ""
}
func init() {
proto.RegisterType((*UserProtoRequest)(nil), "httpdoc.UserProtoRequest")
proto.RegisterType((*UserProtoResponse)(nil), "httpdoc.UserProtoResponse")
proto.RegisterType((*UserProtoResponse_Setting)(nil), "httpdoc.UserProtoResponse.Setting")
}
func (m *UserProtoRequest) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *UserProtoRequest) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if m.Id != 0 {
dAtA[i] = 0x8
i++
i = encodeVarintMessage(dAtA, i, uint64(m.Id))
}
if len(m.Name) > 0 {
dAtA[i] = 0x12
i++
i = encodeVarintMessage(dAtA, i, uint64(len(m.Name)))
i += copy(dAtA[i:], m.Name)
}
return i, nil
}
func (m *UserProtoResponse) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *UserProtoResponse) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if m.Id != 0 {
dAtA[i] = 0x8
i++
i = encodeVarintMessage(dAtA, i, uint64(m.Id))
}
if len(m.Name) > 0 {
dAtA[i] = 0x12
i++
i = encodeVarintMessage(dAtA, i, uint64(len(m.Name)))
i += copy(dAtA[i:], m.Name)
}
if m.Active {
dAtA[i] = 0x18
i++
if m.Active {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i++
}
if m.Setting != nil {
dAtA[i] = 0x22
i++
i = encodeVarintMessage(dAtA, i, uint64(m.Setting.Size()))
n1, err := m.Setting.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n1
}
return i, nil
}
func (m *UserProtoResponse_Setting) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *UserProtoResponse_Setting) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.Email) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintMessage(dAtA, i, uint64(len(m.Email)))
i += copy(dAtA[i:], m.Email)
}
return i, nil
}
func encodeFixed64Message(dAtA []byte, offset int, v uint64) int {
dAtA[offset] = uint8(v)
dAtA[offset+1] = uint8(v >> 8)
dAtA[offset+2] = uint8(v >> 16)
dAtA[offset+3] = uint8(v >> 24)
dAtA[offset+4] = uint8(v >> 32)
dAtA[offset+5] = uint8(v >> 40)
dAtA[offset+6] = uint8(v >> 48)
dAtA[offset+7] = uint8(v >> 56)
return offset + 8
}
func encodeFixed32Message(dAtA []byte, offset int, v uint32) int {
dAtA[offset] = uint8(v)
dAtA[offset+1] = uint8(v >> 8)
dAtA[offset+2] = uint8(v >> 16)
dAtA[offset+3] = uint8(v >> 24)
return offset + 4
}
func encodeVarintMessage(dAtA []byte, offset int, v uint64) int {
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
}
func (m *UserProtoRequest) Size() (n int) {
var l int
_ = l
if m.Id != 0 {
n += 1 + sovMessage(uint64(m.Id))
}
l = len(m.Name)
if l > 0 {
n += 1 + l + sovMessage(uint64(l))
}
return n
}
func (m *UserProtoResponse) Size() (n int) {
var l int
_ = l
if m.Id != 0 {
n += 1 + sovMessage(uint64(m.Id))
}
l = len(m.Name)
if l > 0 {
n += 1 + l + sovMessage(uint64(l))
}
if m.Active {
n += 2
}
if m.Setting != nil {
l = m.Setting.Size()
n += 1 + l + sovMessage(uint64(l))
}
return n
}
func (m *UserProtoResponse_Setting) Size() (n int) {
var l int
_ = l
l = len(m.Email)
if l > 0 {
n += 1 + l + sovMessage(uint64(l))
}
return n
}
func sovMessage(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
}
func sozMessage(x uint64) (n int) {
return sovMessage(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *UserProtoRequest) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: UserProtoRequest: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: UserProtoRequest: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType)
}
m.Id = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Id |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthMessage
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Name = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipMessage(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthMessage
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *UserProtoResponse) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: UserProtoResponse: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: UserProtoResponse: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType)
}
m.Id = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Id |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthMessage
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Name = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Active", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
m.Active = bool(v != 0)
case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Setting", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthMessage
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Setting == nil {
m.Setting = &UserProtoResponse_Setting{}
}
if err := m.Setting.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipMessage(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthMessage
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *UserProtoResponse_Setting) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: Setting: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: Setting: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Email", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowMessage
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthMessage
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Email = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipMessage(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthMessage
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipMessage(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowMessage
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowMessage
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowMessage
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthMessage
}
return iNdEx, nil
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowMessage
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipMessage(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
case 4:
return iNdEx, nil
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
}
panic("unreachable")
}
var (
ErrInvalidLengthMessage = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowMessage = fmt.Errorf("proto: integer overflow")
)
func init() { proto.RegisterFile("message.proto", fileDescriptorMessage) }
var fileDescriptorMessage = []byte{
// 211 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xcd, 0x4d, 0x2d, 0x2e,
0x4e, 0x4c, 0x4f, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xcf, 0x28, 0x29, 0x29, 0x48,
0xc9, 0x4f, 0x56, 0x32, 0xe3, 0x12, 0x08, 0x2d, 0x4e, 0x2d, 0x0a, 0x00, 0x89, 0x06, 0xa5, 0x16,
0x96, 0xa6, 0x16, 0x97, 0x08, 0xf1, 0x71, 0x31, 0x65, 0xa6, 0x48, 0x30, 0x2a, 0x30, 0x6a, 0xb0,
0x06, 0x31, 0x65, 0xa6, 0x08, 0x09, 0x71, 0xb1, 0xe4, 0x25, 0xe6, 0xa6, 0x4a, 0x30, 0x29, 0x30,
0x6a, 0x70, 0x06, 0x81, 0xd9, 0x4a, 0xeb, 0x18, 0xb9, 0x04, 0x91, 0x34, 0x16, 0x17, 0xe4, 0xe7,
0x15, 0xa7, 0x12, 0xa3, 0x53, 0x48, 0x8c, 0x8b, 0x2d, 0x31, 0xb9, 0x24, 0xb3, 0x2c, 0x55, 0x82,
0x59, 0x81, 0x51, 0x83, 0x23, 0x08, 0xca, 0x13, 0xb2, 0xe1, 0x62, 0x2f, 0x4e, 0x2d, 0x29, 0xc9,
0xcc, 0x4b, 0x97, 0x60, 0x51, 0x60, 0xd4, 0xe0, 0x36, 0x52, 0xd2, 0x83, 0x3a, 0x52, 0x0f, 0xc3,
0x22, 0xbd, 0x60, 0x88, 0xca, 0x20, 0x98, 0x16, 0x29, 0x79, 0x2e, 0x76, 0xa8, 0x98, 0x90, 0x08,
0x17, 0x6b, 0x6a, 0x6e, 0x62, 0x66, 0x0e, 0xd8, 0x1d, 0x9c, 0x41, 0x10, 0x8e, 0x93, 0xc0, 0x89,
0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe3, 0xb1, 0x1c, 0x43,
0x12, 0x1b, 0x38, 0x28, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xc5, 0x23, 0xb1, 0xd2, 0x1b,
0x01, 0x00, 0x00,
}
================================================
FILE: proto/message.proto
================================================
syntax = "proto3";
package httpdoc;
message UserProtoRequest {
int32 id = 1;
string name = 2;
}
message UserProtoResponse {
message Setting {
string email = 1;
}
int32 id = 1;
string name = 2;
bool active = 3;
Setting setting = 4;
}
================================================
FILE: static/bindata.go
================================================
// Code generated by go-bindata.
// sources:
// tmpl/api-blueprint.tmpl
// tmpl/doc.md.tmpl
// DO NOT EDIT!
package static
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info os.FileInfo
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}
func (fi bindataFileInfo) Name() string {
return fi.name
}
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi bindataFileInfo) IsDir() bool {
return false
}
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var _tmplApiBlueprintTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x90\x3d\x4f\xc3\x30\x10\x86\xf7\xfc\x8a\x93\xb2\xb4\xaa\x1a\x76\x36\x3e\x8a\x60\x00\x55\x20\xb1\x1f\xf5\x4b\x31\x4a\x6c\x63\x3b\x12\x28\xca\x7f\x47\x89\xdd\xc6\x4d\xca\xc7\xd0\x2d\xe7\x3c\xf7\xde\x73\x97\xd3\xc5\xfa\x2e\xcb\xf2\x9c\x9a\x86\x8a\x07\xae\x40\x6d\x9b\x65\x4d\x43\x96\xd5\x16\x54\xac\x94\xb7\x12\x8e\x96\xdd\x73\x1e\xb9\x6b\xb8\x8d\x95\xc6\x4b\xad\xa8\x6d\xfb\xa7\x7b\xf8\x37\x2d\x76\xcd\xf2\x95\x8a\x47\x7c\xd4\x70\xfe\x46\xa2\x14\xa1\x7f\x48\x9d\xfe\x5b\xa4\x02\x34\x73\xde\x4a\xb5\x9d\xd3\xf2\xc8\xbc\x2e\x07\x4a\x1c\x7e\x8d\xa6\xae\xd9\x72\xb5\x4b\xee\x0b\x78\x58\x37\x55\x48\x40\xa2\x91\x84\xaa\xab\x17\xd8\xbf\x24\x12\x87\x05\xc5\x54\x9a\xb1\x31\xa5\xdc\x70\x47\x9f\xbd\x3b\xad\xe6\x23\xc1\x5b\xb0\x80\x1d\x06\xc7\x7a\x2a\x78\x08\x52\x6a\x78\xde\x17\xcf\x5c\xd6\xf8\xe9\x2c\x5d\xf4\xa5\x16\x5f\xfb\xd6\x98\xba\xfa\xe4\xca\x94\xd8\x5b\x3b\xa3\x95\x43\x24\x42\xf1\xe4\xd9\xd7\xee\x4a\x8b\x70\x8c\x5f\x16\x0a\xfc\x7f\x36\x3a\x46\x9e\x60\xa5\x10\x9b\xee\x34\xe0\xdf\x01\x00\x00\xff\xff\x6d\xf2\xb7\x0d\xe2\x02\x00\x00")
func tmplApiBlueprintTmplBytes() ([]byte, error) {
return bindataRead(
_tmplApiBlueprintTmpl,
"tmpl/api-blueprint.tmpl",
)
}
func tmplApiBlueprintTmpl() (*asset, error) {
bytes, err := tmplApiBlueprintTmplBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "tmpl/api-blueprint.tmpl", size: 738, mode: os.FileMode(420), modTime: time.Unix(1496893135, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _tmplDocMdTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x53\xc1\x6e\xd4\x30\x10\xbd\xcf\x57\x8c\x94\x03\x70\xd8\xf4\x8e\x56\x95\x50\x5b\x04\x07\xd0\xaa\x54\x5c\x2a\xa4\x4c\xe3\xd9\xc6\x90\xd8\xc1\x9e\x40\xab\x4d\xfe\x1d\xd9\xde\xad\x1c\x6d\xf7\x42\x9b\x9c\x66\x9e\xc7\xf3\xde\x9b\x8c\x0b\xfc\xb0\xf9\x8c\xca\xd6\x00\x37\x8d\xf6\xa8\xfd\x01\x18\x3a\x36\x42\xa2\xad\xc1\xad\x75\xb8\xdb\x61\xf9\x95\x3a\xc6\x69\x2a\xf1\x50\x7a\xcf\x86\x1d\x09\x2b\xbc\x7b\xc4\xaa\x11\xe9\x95\xad\xab\x12\x2f\xad\x79\x23\xc8\x4a\x4b\x38\x68\xc8\xa8\x12\xa0\x28\xf0\x86\xee\x5a\x46\xbb\xc5\xda\x1a\x61\x23\x1e\x60\xb7\x43\x47\xe6\x9e\xb1\xbc\x32\xe2\x34\x7b\x5c\x4d\x13\xac\xf0\xf6\x36\x30\x5e\xb3\xef\xad\xf1\xfc\x4d\x48\x06\x7f\x61\x55\xe0\xff\x11\xc5\x7c\x61\x69\xac\xc2\x69\x8a\xd9\x86\xa4\x09\x47\x6f\x8b\x93\xd7\x56\xd9\xad\x11\x5b\xfb\x97\xdd\x01\x8d\xb7\x47\xf4\xe2\x74\xef\x5b\xf2\x4d\x56\xf0\x2e\x48\x64\x13\x98\x4e\xa9\x2d\x0a\xfc\x2f\xb5\xb1\x5f\x79\xc9\xbe\x76\xba\x8f\x93\x0e\x58\x51\x14\x78\xcd\xbf\x07\xf6\x12\x0b\xf4\x36\x74\x8e\xf9\x86\x1c\x75\x89\x33\x86\x2c\xec\x3c\xc0\x88\xf1\xcf\xe0\x88\xdf\xa9\x1d\x62\x90\x37\x1d\x61\xc4\x55\xf8\x70\xc4\xf7\xf3\x20\x25\x99\xad\x63\xa2\x31\xff\xf5\x98\xb2\x44\xf3\x94\xce\x1d\xa4\x7e\x69\x62\xf3\xd1\x65\x4e\x3e\x31\x29\x76\x89\x61\x1f\x2f\xe1\x23\xa7\x79\x91\x11\x38\xe9\xe4\xa3\xe6\x56\x25\x86\x3d\x82\xdb\x08\x2d\xe1\x27\x23\x5b\xc8\xce\xd5\x03\x75\x7d\xcb\x33\x3f\x9c\x30\x80\xb5\x62\x21\xdd\xfa\x73\x58\xfb\xa1\xeb\xc8\x3d\x9e\x5f\xb4\xba\xfe\x85\x62\x91\x1f\x7a\x32\x0a\x6b\xab\xb8\x5c\x9f\x1d\x8e\x01\xaa\xaa\xfa\x49\x7f\x28\x29\x81\xf4\x4c\x66\x4c\xd3\x14\x6a\x00\xd6\x67\x4f\xdd\x33\x75\xe9\x35\xa4\x77\x95\x69\x4d\xc0\xf2\x5b\x74\xcc\xf3\xfa\x73\x4f\x1c\xb3\x3d\x4a\xd0\x32\x8b\x74\x44\xb7\x94\xa3\xf9\x2a\xed\x2d\xbd\xfa\x2e\xcd\xb9\x9e\x59\xa6\x67\xb5\xfe\x0b\x00\x00\xff\xff\x2f\x57\xc7\x87\xf8\x06\x00\x00")
func tmplDocMdTmplBytes() ([]byte, error) {
return bindataRead(
_tmplDocMdTmpl,
"tmpl/doc.md.tmpl",
)
}
func tmplDocMdTmpl() (*asset, error) {
bytes, err := tmplDocMdTmplBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "tmpl/doc.md.tmpl", size: 1784, mode: os.FileMode(420), modTime: time.Unix(1496894277, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"tmpl/api-blueprint.tmpl": tmplApiBlueprintTmpl,
"tmpl/doc.md.tmpl": tmplDocMdTmpl,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"tmpl": &bintree{nil, map[string]*bintree{
"api-blueprint.tmpl": &bintree{tmplApiBlueprintTmpl, map[string]*bintree{}},
"doc.md.tmpl": &bintree{tmplDocMdTmpl, map[string]*bintree{}},
}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}
================================================
FILE: static/static.go
================================================
package static
//go:generate go-bindata -pkg=static ./tmpl/...
================================================
FILE: static/tmpl/doc.md.tmpl
================================================
# API doc
This is API documentation for {{ .Name }}. This is generated by `httpdoc`. Don't edit by hand.
## Table of contents
{{ range .Entries -}}
- [[{{ .ResponseStatusCode }}] {{ .Method }} {{ .Path }}](#{{ .ResponseStatusCode }}-{{ .Method | lower }}-{{ .Path | stripslash | lower }})
{{ end }}
{{ range .Entries -}}
## [{{ .ResponseStatusCode }}] {{ .Method }} {{ .Path }}
{{ .Description }}
### Request
{{ if .RequestParams -}}
Parameters
| Name | Value | Description |
| ----- | :----- | :--------- |
{{ range .RequestParams -}}
| {{ .Name }} | {{ .Value }} | {{ .Description }} |
{{ end }}{{ end }}
{{ if .RequestHeaders -}}
Headers
| Name | Value | Description |
| ----- | :----- | :--------- |
{{ range .RequestHeaders -}}
| {{ .Name }} | {{ .Value }} | {{ .Description }} |
{{ end }}
{{ end }}
{{ if .RequestFields -}}
Request fields
| Name | Value | Description |
| ----- | :----- | :--------- |
{{ range .RequestFields -}}
| {{ .Name }} | {{ .Value }} | {{ .Description }} |
{{ end }}
{{ end }}
{{ if .RequestExample -}}
Request example
Click to expand code.
```javascript
{{ .RequestExample }}
```
{{ end }}
### Response
{{ if .ResponseHeaders -}}
Headers
| Name | Value | Description |
| ----- | :----- | :--------- |
{{ range .ResponseHeaders -}}
| {{ .Name }} | {{ .Value }} | {{ .Description }} |
{{ end }}
{{ end }}
{{ if .ResponseFields -}}
Response fields
| Name | Value | Description |
| ----- | :----- | :--------- |
{{ range .ResponseFields -}}
| {{ .Name }} | {{ .Value }} | {{ .Description }} |
{{ end }}
{{ end }}
{{ if .ResponseExample -}}
Response example
Click to expand code.
```javascript
{{ .ResponseExample }}
```
{{ end }}
{{ end }}
================================================
FILE: template.go
================================================
package httpdoc
import (
"io"
"os"
"path/filepath"
"strings"
"text/template"
"go.mercari.io/go-httpdoc/static"
)
// defaultTmpl is default template file to use.
var defaultTmpl = "tmpl/doc.md.tmpl"
// Generate writes documentation into the given file. Generation is skipped
// if EnvHTTPDoc is empty. If directory does not exist or any, it returns error.
func (d *Document) Generate(path string) error {
// Only generate documentation when EnvHttpDoc has non-empty value
if os.Getenv(EnvHTTPDoc) == "" {
return nil
}
path, _ = filepath.Abs(path)
if _, err := os.Stat(filepath.Dir(path)); err != nil && os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
}
f, err := os.Create(path)
if err != nil {
return err
}
return d.generate(f)
}
func (d *Document) generate(w io.Writer) error {
if d.tmpl == "" {
d.tmpl = defaultTmpl
}
buf, err := static.Asset(d.tmpl)
if err != nil {
return err
}
return d.tmplExecute(w, string(buf))
}
func (d *Document) tmplExecute(w io.Writer, text string) error {
tmpl, err := template.New("httpdoc").Funcs(funcMap()).Parse(text)
if err != nil {
return err
}
if err := tmpl.Execute(w, d); err != nil {
return err
}
return nil
}
func funcMap() template.FuncMap {
return template.FuncMap{
"lower": strings.ToLower,
"stripslash": func(s string) string {
return strings.Replace(s, "/", "", -1)
},
}
}
================================================
FILE: template_test.go
================================================
package httpdoc
import (
"io/ioutil"
"os"
"testing"
)
func setEnv(t *testing.T, k, v string) func() {
preV := os.Getenv(k)
if err := os.Setenv(k, v); err != nil {
t.Fatal(err)
}
return func() {
if err := os.Setenv(k, preV); err != nil {
t.Fatal(err)
}
}
}
func TestDocument_Generate(t *testing.T) {
resetF := setEnv(t, EnvHTTPDoc, "test")
defer resetF()
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
doc := &Document{}
if err := doc.Generate(f.Name()); err != nil {
t.Fatal(err)
}
fi, err := os.Stat(f.Name())
if err != nil {
t.Fatal(err)
}
if fi.Size() == 0 {
t.Fatalf("expect doc to be generated")
}
}
func TestDocument_Generate_noEnv(t *testing.T) {
resetF := setEnv(t, EnvHTTPDoc, "")
defer resetF()
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
doc := &Document{}
if err := doc.Generate(f.Name()); err != nil {
t.Fatal(err)
}
fi, err := os.Stat(f.Name())
if err != nil {
t.Fatal(err)
}
if fi.Size() > 0 {
t.Fatalf("expect doc not to be generated")
}
}
func TestFuncMap(t *testing.T) {
m := funcMap()
lower := m["lower"].(func(s string) string)
if got, want := lower("DOC"), "doc"; got != want {
t.Fatalf("got %q, want %q", got, want)
}
stripslash := m["stripslash"].(func(s string) string)
if got, want := stripslash("/v2/user/contact"), "v2usercontact"; got != want {
t.Fatalf("got %q, want %q", got, want)
}
}
func TestTemplateGenerate_NotExistDir(t *testing.T) {
resetF := setEnv(t, EnvHTTPDoc, "1")
defer resetF()
doc := &Document{}
if err := doc.Generate("/tmp/httpdoc/no-such-file-or-directory"); err != nil {
t.Fatalf("expect to be failed")
}
defer os.RemoveAll("/tmp/httpdoc")
}
func TestTemplateGenerate_InvalidTmpl(t *testing.T) {
doc := &Document{
tmpl: "no-such-template",
}
if err := doc.generate(ioutil.Discard); err == nil {
t.Fatalf("expect to be failed")
}
}
func TestTmplExecute_InvalidTemplate(t *testing.T) {
cases := []struct {
text string
}{
{
`{{ .Name }`,
},
{
`{{ .Name.NoSuchField }}`,
},
}
doc := &Document{}
for _, tc := range cases {
if err := doc.tmplExecute(ioutil.Discard, tc.text); err == nil {
t.Fatalf("expect to be failed")
}
}
}
================================================
FILE: validate.go
================================================
package httpdoc
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"reflect"
"testing"
"github.com/golang/protobuf/proto"
"github.com/tenntenn/gpath"
)
var (
defaultUnmarshalFunc = json.Unmarshal
defaultAssertFunc = func(t *testing.T, expected, actual interface{}, desc string) {
if !reflect.DeepEqual(expected, actual) {
tFatalf(t, "%s: got %#v(%T), want %#v(%T)", desc, actual, actual, expected, expected)
}
}
defaultFatalFunc = func(t *testing.T, format string, args ...interface{}) {
t.Fatalf(format, args...)
}
protoUnmarshalFunc = func(data []byte, v interface{}) error {
unmashaler, ok := v.(proto.Unmarshaler)
if !ok {
return fmt.Errorf("failed to type assert to Unmashaler: %T must implement proto.Unmarshaler interface", v)
}
return unmashaler.Unmarshal(data)
}
)
var tFatalf fatalFunc = defaultFatalFunc
type (
assertFunc func(t *testing.T, expected, actual interface{}, desc string)
fatalFunc func(t *testing.T, format string, args ...interface{})
unmarshalFunc func(data []byte, v interface{}) error
)
// Validator takes test cases and checks whether recorded values are equal to the given expected values.
// If not, it fails in the given test context. If ok, it uses the result for documentation.
type Validator struct {
record *record
unmarshalFunc unmarshalFunc
assertFunc assertFunc
requestParams []Data
requestHeaders []Data
requestFields []Data
responseHeaders []Data
responseFields []Data
}
type record struct {
requestParams url.Values
requestHeaders http.Header
requestBody []byte
responseStatusCode int
responseHeaders http.Header
responseBody []byte
}
// TestCase is test case validator uses. Validator inspects and extract request & response value based on
// Target (e.g, when testing request params, target is parameter name. when testing response
// body, target is filed name) and asserts with Expected value.
//
// TestCase can be used like table-driven way.
//
// validator.RequestParams(t, []httpdoc.TestCase{
// NewTestCase("token","12345","Request token"),
// NewTestCase("pretty","true","Pretty print response message"),
// })
//
type TestCase struct {
Target string
Expected interface{}
Description string
AssertFunc assertFunc
}
// NewTestCase returns new TestCase.
func NewTestCase(target string, expected interface{}, description string) TestCase {
return TestCase{Target: target, Expected: expected, Description: description}
}
func newValidator() *Validator {
return &Validator{
unmarshalFunc: defaultUnmarshalFunc,
assertFunc: defaultAssertFunc,
record: &record{},
}
}
// ResponseStatusCode validates response status code is expected or not.
func (v *Validator) ResponseStatusCode(t *testing.T, expected int) {
v.assertFunc(t, expected, v.record.responseStatusCode, "response status code")
}
// RequestParams validated request params are expected or not.
func (v *Validator) RequestParams(t *testing.T, cases []TestCase) {
for _, tc := range cases {
data := Data{
Name: tc.Target,
Value: tc.Expected,
Description: tc.Description,
}
v.requestParams = append(v.requestParams, data)
pickAssertFunc(&tc, v)(t, tc.Expected, v.record.requestParams.Get(tc.Target), tc.Description)
}
}
// RequestHeaders validates request headers are expected or not.
func (v *Validator) RequestHeaders(t *testing.T, cases []TestCase) {
for _, tc := range cases {
data := Data{
Name: tc.Target,
Value: tc.Expected,
Description: tc.Description,
}
v.requestHeaders = append(v.requestHeaders, data)
actual := v.record.requestHeaders.Get(tc.Target)
if actual == "" {
h, ok := v.record.requestHeaders[tc.Target]
if !ok || len(h) == 0 {
tFatalf(t, "request header %q is not found", tc.Target)
return
}
actual = h[0]
}
pickAssertFunc(&tc, v)(t, tc.Expected, actual, tc.Description)
}
}
// ResponseHeaders validates response headers are expected or not.
func (v *Validator) ResponseHeaders(t *testing.T, cases []TestCase) {
for _, tc := range cases {
data := Data{
Name: tc.Target,
Value: tc.Expected,
Description: tc.Description,
}
v.responseHeaders = append(v.responseHeaders, data)
actual := v.record.responseHeaders.Get(tc.Target)
if actual == "" {
h, ok := v.record.responseHeaders[tc.Target]
if !ok || len(h) == 0 {
tFatalf(t, "request header %q is not found", tc.Target)
return
}
actual = h[0]
}
pickAssertFunc(&tc, v)(t, tc.Expected, actual, tc.Description)
}
}
// RequestBody validates request body's fileds are expected or not. The request body
// is unmarshaled to the given struct. To extract a filed to validate, this uses dot-seprated
// expression in TestCase.Target. For example, if you want to access `Email` value in the
// following struct use `Setting.Name` in Target.
//
// type User struct {
// Setting Setting
// }
//
// type Setting struct {
// Email string
// }
//
func (v *Validator) RequestBody(t *testing.T, cases []TestCase, request interface{}) {
// Unmarshal request body into the given struct
if err := v.unmarshalFunc(v.record.requestBody, request); err != nil {
tFatalf(t, "Failed to unmarshal request body: %s", err)
return
}
v.validateFields(t, cases, request, &v.requestFields)
}
// ResponseBody validates response body's fields are expected or not. The response body
// is unmarshaled to the given struct. To extract a filed to validate, this uses dot-seprated
// expression in TestCase.Target. For example, if you want to access `Email` value in the
// following struct use `Setting.Name` in Target.
//
// type User struct {
// Setting Setting
// }
//
// type Setting struct {
// Email string
// }
//
func (v *Validator) ResponseBody(t *testing.T, cases []TestCase, response interface{}) {
// Unmarshal request body into the given struct
if err := v.unmarshalFunc(v.record.responseBody, response); err != nil {
tFatalf(t, "Failed to unmarshal response body: %s", err)
}
v.validateFields(t, cases, response, &v.responseFields)
}
func (vl *Validator) validateFields(t *testing.T, cases []TestCase, v interface{}, fields *[]Data) {
for _, tc := range cases {
data := Data{
Name: tc.Target,
Value: tc.Expected,
Description: tc.Description,
}
*fields = append(*fields, data)
actual, _ := gpath.At(v, tc.Target)
pickAssertFunc(&tc, vl)(t, tc.Expected, actual, tc.Description)
}
}
func pickAssertFunc(tc *TestCase, v *Validator) assertFunc {
if tc.AssertFunc != nil {
return tc.AssertFunc
}
return v.assertFunc
}
================================================
FILE: validate_test.go
================================================
package httpdoc
import (
"bytes"
"fmt"
"io"
"math/rand"
"reflect"
"strconv"
"strings"
"testing"
"time"
"github.com/golang/protobuf/proto"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Active bool `json:"active"`
Setting *Setting `json:"setting"`
Permission []string `json:"permission"`
Preference map[string]int `json:"preference"`
}
type Setting struct {
Email string `json:"email"`
SNS SNS `json:"sns"`
}
type SNS struct {
Twitter string `json:"twitter"`
Facebook string `json:"facebook"`
}
// testAssertWithCount returns assertFunc it counts failed test instead of fail.
func testAssertWithCount(fails *int) assertFunc {
return func(t *testing.T, expected, actual interface{}, desc string) {
if !reflect.DeepEqual(expected, actual) {
*fails++
}
}
}
func fprintFatalFunc(w io.Writer) fatalFunc {
return func(t *testing.T, format string, args ...interface{}) {
fmt.Fprintf(w, format, args...)
}
}
func TestValidator_ResponseStatusCode(t *testing.T) {
validator := newValidator()
validator.record.responseStatusCode = 200
validator.ResponseStatusCode(t, 200)
validator.record.responseStatusCode = 500
validator.ResponseStatusCode(t, 500)
var got int
validator.assertFunc = testAssertWithCount(&got)
validator.record.responseStatusCode = 500
validator.ResponseStatusCode(t, 200)
if want := 1; got != want {
t.Fatalf("expect valiate fails %d, got %d", want, got)
}
}
func TestValidator_RequestParams(t *testing.T) {
validator := newValidator()
validator.record.requestParams = map[string][]string{
"token": []string{"12345"},
"pretty": []string{"true"},
"year": []string{strconv.Itoa(time.Now().Year())},
}
thisYearcalledAssertFunc := false
validator.RequestParams(t, []TestCase{
NewTestCase("token", "12345", ""),
NewTestCase("pretty", "true", ""),
{"year", "thisyear", "", func(t *testing.T, expected, actual interface{}, desc string) {
if expected != "thisyear" {
t.Fatal("expected is not thisyear")
}
thisYearcalledAssertFunc = true
}},
})
if thisYearcalledAssertFunc == false {
t.Fatal("thisYear AssertFunc should be called.")
}
var got int
validator.assertFunc = testAssertWithCount(&got)
validator.RequestParams(t, []TestCase{
NewTestCase("token", "8976", ""),
NewTestCase("pretty", "", ""),
NewTestCase("id", "u8988", ""),
})
if want := 3; got != want {
t.Fatalf("expect valiate fails %d, got %d", want, got)
}
}
func TestValidator_RequestHeaders(t *testing.T) {
validator := newValidator()
validator.record.requestHeaders = map[string][]string{
"User-Agent": []string{"Googlebot/2.1"},
"Content-Type": []string{"application/json"},
"X-API-Version": []string{"1.1.2"},
}
validator.RequestHeaders(t, []TestCase{
NewTestCase("User-Agent", "Googlebot/2.1", ""),
NewTestCase("Content-Type", "application/json", ""),
NewTestCase("X-API-Version", "1.1.2", ""),
})
var got int
validator.assertFunc = testAssertWithCount(&got)
validator.RequestHeaders(t, []TestCase{
NewTestCase("User-Agent", []string{"curl"}, ""),
NewTestCase("Content-Type", []string{"application/protobuf"}, ""),
NewTestCase("X-API-Version", []string{"3.0"}, ""),
})
if want := 3; got != want {
t.Fatalf("expect valiate fails %d, got %d", want, got)
}
var buf bytes.Buffer
tFatalf = fprintFatalFunc(&buf)
validator.RequestHeaders(t, []TestCase{
NewTestCase("Not-Found", []string{""}, ""),
})
if got, want := buf.String(), "not found"; !strings.Contains(got, want) {
t.Fatalf("expect %q to contain %q", got, want)
}
}
func TestValidator_ResponseHeaders(t *testing.T) {
validator := newValidator()
rand.Seed(time.Now().UnixNano())
length := 0
for {
length = rand.Intn(100000)
if length > 0 {
break
}
}
validator.record.responseHeaders = map[string][]string{
"Content-Type": []string{"application/json"},
"X-API-Version": []string{"1.1.2"},
"Content-Length": []string{strconv.Itoa(length)},
}
contentLengthCalledAssertFunc := false
validator.ResponseHeaders(t, []TestCase{
NewTestCase("Content-Type", "application/json", ""),
NewTestCase("X-API-Version", "1.1.2", ""),
{"Content-Length", []string{"content length"}, "length is change every time", func(t *testing.T, expected, actual interface{}, desc string) {
contentLength, err := strconv.Atoi(actual.(string))
if err != nil {
t.Fatal("actual is not number")
}
if contentLength <= 0 {
t.Fatal("actual must be greater than 0")
}
contentLengthCalledAssertFunc = true
}},
})
if contentLengthCalledAssertFunc == false {
t.Fatal("content length AssertFunc should be called.")
}
var got int
validator.assertFunc = testAssertWithCount(&got)
validator.ResponseHeaders(t, []TestCase{
NewTestCase("Content-Type", []string{"application/protobuf"}, ""),
})
if want := 1; got != want {
t.Fatalf("expect valiate fails %d, got %d", want, got)
}
var buf bytes.Buffer
tFatalf = fprintFatalFunc(&buf)
validator.ResponseHeaders(t, []TestCase{
NewTestCase("Not-Found", []string{""}, ""),
})
if got, want := buf.String(), "not found"; !strings.Contains(got, want) {
t.Fatalf("expect %q to contain %q", got, want)
}
}
func TestValidator_RequestBody(t *testing.T) {
validator := newValidator()
validator.record.requestBody = []byte(`{
"id": 910,
"setting": {
"email": "taichi@mercari.com"
}
}
`)
validator.RequestBody(t, []TestCase{
NewTestCase("ID", 910, ""),
NewTestCase("Setting.Email", "taichi@mercari.com", ""),
}, &User{})
var got int
validator.assertFunc = testAssertWithCount(&got)
validator.RequestBody(t, []TestCase{
NewTestCase("ID", 123, ""),
NewTestCase("Active", true, ""),
NewTestCase("Setting.Email", "deeeet@gmail.com", ""),
}, &User{})
if want := 3; got != want {
t.Fatalf("expect valiate fails %d, got %d", want, got)
}
var buf bytes.Buffer
tFatalf = fprintFatalFunc(&buf)
validator.RequestBody(t, []TestCase{}, struct{}{})
if got, want := buf.String(), "Failed to unmarshal request"; !strings.Contains(got, want) {
t.Fatalf("expect %q to contain %q", got, want)
}
}
func TestValidator_ResponseBody(t *testing.T) {
validator := newValidator()
validator.record.responseBody = []byte(`{
"id": 789,
"active": false,
"setting": {
"email": "tcnksm@mercari.com"
},
"permission": ["write","read"],
"preference": {
"email": 0
}
}
`)
custommailCalledAssertFunc := false
validator.ResponseBody(t, []TestCase{
NewTestCase("ID", 789, ""),
NewTestCase("Active", false, ""),
NewTestCase("Setting.Email", "tcnksm@mercari.com", ""),
{"Setting.Email", "custommail", "", func(t *testing.T, expected, actual interface{}, desc string) {
if expected != "custommail" {
t.Fatal("Setting.Email is not custommail")
}
custommailCalledAssertFunc = true
}},
NewTestCase("Permission[1]", "read", ""),
{`Preference["email"]`, 0, "", nil},
}, &User{})
if custommailCalledAssertFunc == false {
t.Fatal("custom mail AssertFunc should be called.")
}
var got int
validator.assertFunc = testAssertWithCount(&got)
validator.ResponseBody(t, []TestCase{
NewTestCase("ID", 123, ""),
NewTestCase("Active", true, ""),
NewTestCase("Setting.Email", "deeeet@gmail.com", ""),
NewTestCase("Permission[1]", "write", ""),
{`Preference["email"]`, 1, "", nil},
}, &User{})
if want := 5; got != want {
t.Fatalf("expect valiate fails %d, got %d", want, got)
}
var buf bytes.Buffer
tFatalf = fprintFatalFunc(&buf)
validator.ResponseBody(t, []TestCase{}, struct{}{})
if got, want := buf.String(), "Failed to unmarshal response"; !strings.Contains(got, want) {
t.Fatalf("expect %q to contain %q", got, want)
}
}
func TestValidateFields(t *testing.T) {
testUser := &User{
ID: 12345,
Name: "tcnksm",
Active: true,
Setting: &Setting{
Email: "tcnksm@example.com",
SNS: SNS{
Twitter: "@deeeet",
},
},
Permission: []string{"write", "read"},
Preference: map[string]int{
"email": 0,
"push": 1,
},
}
activeCalledAssertFunc := false
validator := newValidator()
validator.validateFields(t, []TestCase{
NewTestCase("ID", 12345, ""),
NewTestCase("Name", "tcnksm", ""),
NewTestCase("Active", true, ""),
{"Active", "customactive", "", func(t *testing.T, expected, actual interface{}, desc string) {
if expected != "customactive" {
t.Fatal("Acitve is not customactive")
}
activeCalledAssertFunc = true
}},
NewTestCase("Setting.Email", "tcnksm@example.com", ""),
NewTestCase("Setting.SNS.Twitter", "@deeeet", ""),
NewTestCase("Permission[0]", "write", ""),
{`Preference["email"]`, 0, "", nil},
}, testUser, &[]Data{})
if activeCalledAssertFunc == false {
t.Fatal("active AssertFunc should be called.")
}
}
func TestValidator_RequestBody_Proto(t *testing.T) {
buf, err := proto.Marshal(&UserProtoRequest{
Id: 12345,
Name: "tcnksm",
})
if err != nil {
t.Fatal(err)
}
validator := newValidator()
validator.record.requestBody = buf
validator.unmarshalFunc = protoUnmarshalFunc
customIDCalledAssertFunc := false
validator.RequestBody(t, []TestCase{
NewTestCase("Id", int32(12345), ""),
NewTestCase("Name", "tcnksm", ""),
{"Id", "customid", "custom assert func test", func(t *testing.T, expected, actual interface{}, desc string) {
if expected != "customid" {
t.Fatal("expected is not customid")
}
customIDCalledAssertFunc = true
}},
}, &UserProtoRequest{})
if customIDCalledAssertFunc == false {
t.Fatal("custom id AssertFunc should be called.")
}
var got int
validator.assertFunc = testAssertWithCount(&got)
validator.RequestBody(t, []TestCase{
NewTestCase("Id", 123, ""),
}, &UserProtoRequest{})
if want := 1; got != want {
t.Fatalf("expect valiate fails %d, got %d", want, got)
}
}
func TestValidator_ResponseBody_Proto(t *testing.T) {
buf, err := proto.Marshal(&UserProtoResponse{
Id: 667854,
Setting: &UserProtoResponse_Setting{
Email: "httpdoc@example.com",
},
})
if err != nil {
t.Fatal(err)
}
validator := newValidator()
validator.unmarshalFunc = protoUnmarshalFunc
validator.record.responseBody = buf
validator.ResponseBody(t, []TestCase{
NewTestCase("Id", int32(667854), ""),
NewTestCase("Setting.Email", "httpdoc@example.com", ""),
}, &UserProtoResponse{})
var got int
validator.assertFunc = testAssertWithCount(&got)
validator.ResponseBody(t, []TestCase{
NewTestCase("Id", 123, ""),
NewTestCase("Setting.Email", "deeeet@gmail.com", ""),
}, &UserProtoResponse{})
if want := 2; got != want {
t.Fatalf("expect valiate fails %d, got %d", want, got)
}
}
func TestUnmarshallerFunc(t *testing.T) {
unmarshalFunc := protoUnmarshalFunc
if err := unmarshalFunc([]byte(""), &User{}); err == nil {
t.Fatal("expect to be failed")
}
}
func TestAssertFunc(t *testing.T) {
var buf bytes.Buffer
tFatalf = fprintFatalFunc(&buf)
defaultAssertFunc(t, 1, 2, "test-assert")
if got, want := buf.String(), "test-assert: got 2(int), want 1(int)"; !strings.Contains(got, want) {
t.Fatalf("expect %q to contain %q", got, want)
}
}