smartapi

package module
v0.0.0-...-4c3163d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 15, 2023 License: MIT Imports: 13 Imported by: 0

README

SmartAPI REST Library

GoDoc Build Status Go Report Card Coverage Status

SmartAPI allows to quickly implement solid REST APIs in Golang. The idea behind the project is to replace handler functions with ordinary looking functions. This allows service layer methods to be used as handlers. Designation of a dedicated API layer is still advisable in order to map errors to status codes, write cookies, headers, etc.

SmartAPI is based on github.com/go-chi/chi. This allows Chi middlewares to be used.

Examples

This example returns a greeting with a name based on a query param name.

package main

import (
    "fmt" 
    "log"
    "net/http"

    "github.com/mmbednarek/smartapi"
)

func MountAPI() http.Handler {
    r := smartapi.NewRouter()
    r.Get("/greeting", Greeting,
        smartapi.QueryParam("name"),
    )

    return r.MustHandler()
}

func Greeting(name string) string {
    return fmt.Sprintf("Hello %s!\n", name)
}

func main() {
    log.Fatal(http.ListenAndServe(":8080", MountAPI()))
}
$ curl '127.0.0.1:8080/greeting?name=Johnny'
Hello Johnny!

It's possible to use even standard Go functions

But it's a good practice to use your own handler functions for your api.

package main

import (
    "encoding/base32"
    "encoding/base64"
    "log"
    "net/http"

    "github.com/mmbednarek/smartapi"
)

func MountAPI() http.Handler {
    r := smartapi.NewRouter()

    r.Route("/encode", func(r smartapi.Router) {
        r.Post("/base64", base64.StdEncoding.EncodeToString)
        r.Post("/base32", base32.StdEncoding.EncodeToString)
    }, smartapi.ByteSliceBody())
    r.Route("/decode", func(r smartapi.Router) {
        r.Post("/base64", base64.StdEncoding.DecodeString)
        r.Post("/base32", base32.StdEncoding.DecodeString)
    }, smartapi.StringBody())

    return r.MustHandler()
}

func main() {
    log.Fatal(http.ListenAndServe(":8080", MountAPI()))
}
~ $ curl 127.0.0.1:8080/encode/base64 -d 'smartAPI'
c21hcnRBUEk=
~ $ curl 127.0.0.1:8080/encode/base32 -d 'smartAPI'
ONWWC4TUIFIES===
~ $ curl 127.0.0.1:8080/decode/base64 -d 'c21hcnRBUEk='
smartAPI
~ $ curl 127.0.0.1:8080/decode/base32 -d 'ONWWC4TUIFIES==='
smartAPI
Service example

You can use SmartAPI with service layer methods as shown here.

package main

import (
    "log"
    "net/http"


    "github.com/mmbednarek/smartapi"
)

type Date struct {
    Day   int `json:"day"`
    Month int `json:"month"`
    Year  int `json:"year"`
}

type User struct {
    Login       string `json:"login"`
    Password    string `json:"password,omitempty"`
    Email       string `json:"email"`
    DateOfBirth Date   `json:"date_of_birth"`
}

type Service interface {
    RegisterUser(user *User) error
    Auth(login, password string) (string, error)
    GetUserData(session string) (*User, error)
    UpdateUser(session string, user *User) error
}

func newHandler(service Service) http.Handler {
    r := smartapi.NewRouter()

    r.Post("/user", service.RegisterUser,
        smartapi.JSONBody(User{}),
    )
    r.Post("/user/auth", service.Auth,
        smartapi.PostQueryParam("login"),
        smartapi.PostQueryParam("password"),
    )
    r.Get("/user", service.GetUserData,
        smartapi.Header("X-Session-ID"),
    )
    r.Patch("/user", service.UpdateUser,
        smartapi.Header("X-Session-ID"),
        smartapi.JSONBody(User{}),
    )

    return r.MustHandler()
}

func main() {
    svr := service.NewService() // Your service implementation
    log.Fatal(http.ListenAndServe(":8080", newHandler(svr)))
}

Middlewares

Middlewares can be used just as in Chi. Use(...) appends middlewares to be used. With(...) creates a copy of a router with chosen middlewares.

Routing

Routing works similarity to Chi routing. Parameters can be prepended to be used in all endpoints in that route.

r.Route("/v1/foo", func(r smartapi.Router) {
    r.Route("/bar", func(r smartapi.Router) {
        r.Get("/test", func(ctx context.Context, foo string, test string) {
            ...
        },
            smartapi.QueryParam("test"),
        )
    },
        smartapi.Header("X-Foo"),
    )
},
    smartapi.Context(),
)

Support for legacy handlers

Legacy handlers are supported with no overhead. They are directly passed as ordinary handler functions. No additional variadic arguments are required for legacy handler to be used.

Handler response

Empty body response

A handler function with error only return argument will return empty response body with 204 NO CONTENT status as default.

r.Post("/test", func() error {
    return nil
})
String response

Returned string will we written directly into a function body.

r.Get("/test", func() (string, error) {
    return "Hello World", nil
})
Byte slice response

Just as with the string, the slice will we written directly into a function body.

r.Get("/test", func() ([]byte, error) {
    return []byte("Hello World"), nil
})
Struct, pointer or interface response

A struct, a pointer, an interface or a slice different than a byte slice with be encoded into a json format.

r.Get("/test", func() (interface{}, error) {
    return struct{
        Name string `json:"name"`
        Age  int    `json:"age"`
    }{"John", 34}, nil
})

Errors

To return an error with a status code you can use one of the error functions: smartapi.Error(status int, msg, reason string), smartapi.Errorf(status int, msg string, fmt ...interface{}), smartapi.WrapError(status int, err error, reason string). The API error contains an error message and an error reason. The message will be printed with a logger. The reason will be returned in the response body. You can also return ordinary errors. They are treated as if their status code was 500.

r.Get("/order/{id}", func(orderID string) (*Order, error) {
    order, err := db.GetOrder(orderID)
    if err != nil {
        if errors.Is(err, ErrNoSuchOrder) {
            return nil, smartapi.WrapError(http.StatusNotFound, err, "no such order")
        }
        return nil, err
    }
    return order, nil
},
    smartapi.URLParam("id"),
)
$ curl -i 127.0.0.1:8080/order/someorder
HTTP/1.1 404 Not Found
Date: Sun, 22 Mar 2020 14:17:34 GMT
Content-Length: 40
Content-Type: text/plain; charset=utf-8

{"status":404,"reason":"no such order"}

Endpoint arguments

List of available endpoint attributes

Request Struct

Request can be passed into a structure's field by tags.

type headers struct {
    Foo string `smartapi:"header=X-Foo"`
    Bar string `smartapi:"header=X-Bar"`
}
r.Post("/user", func(h *headers) (string, error) {
    return fmt.Sprintf("Foo: %s, Bar: %s\n", h.Foo, h.Bar), nil
},
    smartapi.RequestStruct(headers{}),
)

Every argument has a tag value equivalent

Tag Value Function Equivalent Expected Type
header=name Header("name") string
r_header=name RequiredHeader("name") string
json_body JSONBody() ...
string_body StringBody() string
byte_slice_body ByteSliceBody() []byte
body_reader BodyReader() io.Reader
url_param=name URLParam("name") string
context Context() context.Context
query_param=name QueryParam("name") string
r_query_param=name RequiredQueryParam("name") string
post_query_param=name PostQueryParam("name") string
r_post_query_param=name RequiredPostQueryParam("name") string
cookie=name Cookie("name") string
r_cookie=name RequiredCookie("name") string
response_headers ResponseHeaders() smartapi.Headers
response_cookies ResponseCookies() smartapi.Cookies
response_writer ResponseWriter() http.ResponseWriter
request Request() *http.Request
request_struct RequestStruct() struct{...}
as_int=header=name AsInt(Header("name") int
as_byte_slice=header=name AsByteSlice(Header("name") []byte
JSON Body

JSON Body unmarshals the request's body into a given structure type. Expects a pointer to that structure as a function argument. If you want to use the object directly (not as a pointer) you can use JSONBodyDirect.

r.Post("/user", func(u *User) error {
    return db.AddUser(u)
},
    smartapi.JSONBody(User{}),
)
String Body

String body passes the request's body as a string.

r.Post("/user", func(body string) error {
    fmt.Printf("Request body: %s\n", body)
    return nil
},
    smartapi.StringBody(),
)
Byte Slice Body

Byte slice body passes the request's body as a byte slice.

r.Post("/user", func(body []byte) error {
    fmt.Printf("Request body: %s\n", string(body))
    return nil
},
    smartapi.ByteSliceBody(),
)
Body Reader

Byte reader body passes the io.Reader interface to read request's body.

r.Post("/user", func(body io.Reader) error {
    buff, err := ioutil.ReadAll()
    if err != nil {
        return err
    }
    return nil
},
    smartapi.BodyReader(),
)
Response Writer

Classic http.ResponseWriter can be used as well.

r.Post("/user", func(w http.ResponseWriter) error {
    _, err := w.Write([]byte("RESPONSE"))
    if err != nil {
        return err
    }
    return nil
},
    smartapi.ResponseWriter(),
)
Request

Classic *http.Request can be passed as an argument.

r.Post("/user", func(r *http.Request) error {
    buff, err := ioutil.ReadAll(r.Body)
    if err != nil {
        return err
    }
    fmt.Printf("Request body is: %s\n", string(buff))
    return nil
},
    smartapi.Request(),
)
Query param

Query param reads the value of the selected param and passes it as a string to function.

r.Get("/user", func(name string) (*User, error) {
    return db.GetUser(name)
},
    smartapi.QueryParam("name"),
)
Required Query param

Like QueryParam() but returns 400 BAD REQUEST when empty.

r.Get("/user", func(name string) (*User, error) {
    return db.GetUser(name)
},
    smartapi.RequiredQueryParam("name"),
)
Post Query param

Reads a query param from requests body.

r.Get("/user", func(name string) (*User, error) {
    return db.GetUser(name)
},
    smartapi.PostQueryParam("name"),
)
Required Post Query param

Like PostQueryParam() but returns 400 BAD REQUEST when empty.

r.Get("/user", func(name string) (*User, error) {
    return db.GetUser(name)
},
    smartapi.RequiredPostQueryParam("name"),
)
URL param

URL uses read chi's URL param and passes it into a function as a string.

r.Get("/user/{name}", func(name string) (*User, error) {
    return db.GetUser(name)
},
    smartapi.URLParam("name"),
)
Header

Header reads the value of the selected request header and passes it as a string to function.

r.Get("/example", func(test string) (string, error) {
    return fmt.Sprintf("The X-Test headers is %s", test), nil
},
    smartapi.Header("X-Test"),
)
Required Header

Like Header(), but responds with 400 BAD REQUEST, if the header is not present.

r.Get("/example", func(test string) (string, error) {
    return fmt.Sprintf("The X-Test headers is %s", test), nil
},
    smartapi.RequiredHeader("X-Test"),
)

Reads a cookie from the request and passes the value into a function as a string.

r.Get("/example", func(c string) (string, error) {
    return fmt.Sprintf("cookie: %s", c)
},
    smartapi.Cookie("cookie"),
)

Like Cookie(), but returns 400 BAD REQUEST when empty.

r.Get("/example", func(c string) (string, error) {
    return fmt.Sprintf("cookie: %s", c)
},
    smartapi.RequiredCookie("cookie"),
)
Context

Context passes r.Context() into a function.

r.Get("/example", func(ctx context.Context) (string, error) {
    return fmt.Sprintf("ctx: %s", ctx)
},
    smartapi.Context(),
)
ResponseHeaders

Response headers allows an endpoint to add response headers.

r.Get("/example", func(headers smartapi.Headers) error {
    headers.Set("Api-Version", "1.2.3")
    return nil
},
    smartapi.ResponseHeaders(),
)
ResponseCookies

Response cookies allows an endpoint to easily add Set-Cookie header.

r.Get("/example", func(cookies smartapi.Cookies) error {
    cookies.Add(&http.Cookie{Name: "Foo", Value: "Bar"})
    return nil
},
    smartapi.ResponseCookies(),
)

Casts

Request attributes can be automatically casted to desired type.

AsInt
r.Get("/example", func(value int) error {
    ...
    return nil
},
    smartapi.AsInt(smartapi.Header("Value")),
)
AsByteSlice
r.Get("/example", func(value []byte) error {
    ...
    return nil
},
    smartapi.AsByteSlice(smartapi.Header("Value")),
)

Conversion to int

Endpoint attributes

Response status

ResponseStatus allows the response status to be set for endpoints with empty response body. Default status is 204 NO CONTENT.

r.Get("/example", func() error {
    return nil
},
    smartapi.ResponseStatus(http.StatusCreated),
)

Documentation

Overview

Package smartapi allows to quickly implement solid REST APIs in Golang. The idea behind the project is to replace handler functions with ordinary looking functions. This allows service layer methods to be used as handlers. Designation of a dedicated API layer is still advisable in order to map errors to status codes, write cookies, headers, etc.

SmartAPI is based on github.com/go-chi/chi. This allows Chi middlewares to be used.

Examples

This example returns a greeting with a name based on a query param `name`.

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/mmbednarek/smartapi"
)

func MountAPI() http.Handler {
	r := smartapi.NewRouter()
	r.Get("/greeting", Greeting,
		smartapi.QueryParam("name"),
	)

	return r.MustHandler()
}

func Greeting(name string) string {
	return fmt.Sprintf("Hello %s!\n", name)
}

func main() {
	log.Fatal(http.ListenAndServe(":8080", MountAPI()))
}

It's possible to use even standard Go functions

But it's a good practice to use your own handler functions for your api.

package main

import (
	"encoding/base32"
	"encoding/base64"
	"log"
	"net/http"

	"github.com/mmbednarek/smartapi"
)

func MountAPI() http.Handler {
	r := smartapi.NewRouter()

	r.Route("/encode", func(r smartapi.Router) {
		r.Post("/base64", base64.StdEncoding.EncodeToString)
		r.Post("/base32", base32.StdEncoding.EncodeToString)
	}, smartapi.ByteSliceBody())
	r.Route("/decode", func(r smartapi.Router) {
		r.Post("/base64", base64.StdEncoding.DecodeString)
		r.Post("/base32", base32.StdEncoding.DecodeString)
	}, smartapi.StringBody())

	return r.MustHandler()
}

func main() {
	log.Fatal(http.ListenAndServe(":8080", MountAPI()))
}

You can use SmartAPI with service layer methods as shown here.

package main

import (
	"log"
	"net/http"

	"github.com/mmbednarek/smartapi"
)

type Date struct {
	Day   int `json:"day"`
	Month int `json:"month"`
	Year  int `json:"year"`
}

type User struct {
	Login       string `json:"login"`
	Password    string `json:"password,omitempty"`
	Email       string `json:"email"`
	DateOfBirth Date   `json:"date_of_birth"`
}

type Service interface {
	RegisterUser(user *User) error
	Auth(login, password string) (string, error)
	GetUserData(session string) (*User, error)
	UpdateUser(session string, user *User) error
}

func newHandler(service Service) http.Handler {
	r := smartapi.NewRouter()

	r.Post("/user", service.RegisterUser,
		smartapi.JSONBody(User{}),
	)
	r.Post("/user/auth", service.Auth,
		smartapi.PostQueryParam("login"),
		smartapi.PostQueryParam("password"),
	)
	r.Get("/user", service.GetUserData,
		smartapi.Header("X-Session-ID"),
	)
	r.Patch("/user", service.UpdateUser,
		smartapi.Header("X-Session-ID"),
		smartapi.JSONBody(User{}),
	)

	return r.MustHandler()
}

func main() {
	svr := service.NewService() // Your service implementation
	log.Fatal(http.ListenAndServe(":8080", newHandler(svr)))
}

Middlewares can be used just as in Chi. `Use(...)` appends middlewares to be used. `With(...)` creates a copy of a router with chosen middlewares.

Routing works similarity to Chi routing. Parameters can be prepended to be used in all endpoints in that route.

r.Route("/v1/foo", func(r smartapi.Router) {
	r.Route("/bar", func(r smartapi.Router) {
		r.Get("/test", func(ctx context.Context, foo string, test string) {
			...
		},
			smartapi.QueryParam("test"),
		)
	},
		smartapi.Header("X-Foo"),
	)
},
	smartapi.Context(),
)

Support for legacy handlers

Legacy handlers are supported with no overhead. They are directly passed as ordinary handler functions. No additional variadic arguments are required for legacy handler to be used.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewRouter

func NewRouter() *router

func NewRouterLogger

func NewRouterLogger(logger Logger) *router

func StartAPI

func StartAPI(a API, address string) error

StartAPI starts a user defined API

Types

type API

type API interface {
	Start(string) error
	Init()
}

API interface represents an API

type ApiError

type ApiError interface {
	Error() string
	Status() int
	Reason() string
}

ApiError represents an API error

func Error

func Error(status int, logMsg string, reason string) ApiError

Error creates an error

func Errorf

func Errorf(status int, format string, args ...interface{}) ApiError

Errorf creates an error with formatting

func WrapError

func WrapError(status int, err error, reason string) ApiError

WrapError wraps already existing error

type Argument

type Argument interface {
	EndpointParam
	// contains filtered or unexported methods
}

Argument represents an argument passed to a function

type Cookies

type Cookies interface {
	Add(c *http.Cookie)
}

Cookies interface allows to add response cookies

type EndpointParam

type EndpointParam interface {
	// contains filtered or unexported methods
}

EndpointParam is used with endpointData definition

func AsByteSlice

func AsByteSlice(param EndpointParam) EndpointParam

AsByteSlice converts an argument and passes it as a byte slice

func AsInt

func AsInt(param EndpointParam) EndpointParam

AsInt converts an argument and passes it as an int into the function

func BodyReader

func BodyReader() EndpointParam

BodyReader passes an io.Reader interface to read request's body.

func ByteSliceBody

func ByteSliceBody() EndpointParam

ByteSliceBody reads request's body end passes it as a byte slice.

func Context

func Context() EndpointParam

Context passes request's context into the function

func Cookie(name string) EndpointParam

Cookie reads a cookie from the request and passes it as a string

func Header(name string) EndpointParam

Header reads a header from the request and passes it as string to a function

func JSONBody

func JSONBody(v interface{}) EndpointParam

JSONBody reads request's body and unmarshals it into a pointer to a json structure

func JSONBodyDirect

func JSONBodyDirect(v interface{}) EndpointParam

JSONBodyDirect reads request's body and unmarshals it into a json structure

func PostQueryParam

func PostQueryParam(name string) EndpointParam

PostQueryParam parses query end passes post query param into a string as an argument

func QueryParam

func QueryParam(name string) EndpointParam

QueryParam reads a query param and passes it as a string

func Request

func Request() EndpointParam

Request passes an *http.Request into a function

func RequestStruct

func RequestStruct(s interface{}) EndpointParam

RequestStruct passes request's arguments into struct's fields by tags

func RequestStructDirect

func RequestStructDirect(s interface{}) EndpointParam

RequestStructDirect passes request's arguments into struct's fields by tags

func RequiredCookie

func RequiredCookie(name string) EndpointParam

RequiredCookie reads a cookie from the request and passes it as a string

func RequiredHeader

func RequiredHeader(name string) EndpointParam

RequiredHeader reads a header from the request and passes it as string to a function

func RequiredPostQueryParam

func RequiredPostQueryParam(name string) EndpointParam

RequiredPostQueryParam reads a post query param and passes it as a string. Returns 400 BAD REQUEST if empty.

func RequiredQueryParam

func RequiredQueryParam(name string) EndpointParam

RequiredQueryParam reads a query param and passes it as a string. Returns 400 BAD REQUEST when empty

func ResponseCookies

func ResponseCookies() EndpointParam

ResponseCookies passes an interface to set cookie values

func ResponseHeaders

func ResponseHeaders() EndpointParam

ResponseHeaders passes an interface to set response header values

func ResponseStatus

func ResponseStatus(status int) EndpointParam

ResponseStatus allows to set successful response status

func ResponseWriter

func ResponseWriter() EndpointParam

ResponseWriter passes an http.ResponseWriter into a function

func StringBody

func StringBody() EndpointParam

StringBody reads request's body end passes it as a string

func URLParam

func URLParam(name string) EndpointParam

URLParam reads a url param and passes it as a string

func XMLBody

func XMLBody(v interface{}) EndpointParam

XMLBody reads request's body and unmarshals it into a pointer to an xml structure

type Headers

type Headers interface {
	Add(key, value string)
	Set(key, value string)
	Get(key string) string
}

Headers interface allows to add response header values

type Logger

type Logger interface {
	LogApiError(ctx context.Context, err ApiError)
	LogError(ctx context.Context, err error)
}

Logger logs the outcome of unsuccessful http requests

var DefaultLogger Logger = defaultLogger{}

DefaultLogger is simple implementation of the Logger interface

type Method

type Method int

Method represents an http method

const (
	MethodPost Method = iota
	MethodGet
	MethodPatch
	MethodDelete
	MethodPut
	MethodOptions
	MethodConnect
	MethodHead
	MethodTrace
)

func (Method) String

func (m Method) String() string

String converts a method into a string with name of the method in capital letters

type RouteHandler

type RouteHandler func(r Router)

type Router

type Router interface {
	Use(middlewares ...func(http.Handler) http.Handler)
	With(middlewares ...func(http.Handler) http.Handler) Router
	AddEndpoint(method Method, pattern string, handler interface{}, args []EndpointParam)
	Post(pattern string, handler interface{}, args ...EndpointParam)
	Get(pattern string, handler interface{}, args ...EndpointParam)
	Put(pattern string, handler interface{}, args ...EndpointParam)
	Patch(pattern string, handler interface{}, args ...EndpointParam)
	Delete(pattern string, handler interface{}, args ...EndpointParam)
	Head(pattern string, handler interface{}, args ...EndpointParam)
	Options(pattern string, handler interface{}, args ...EndpointParam)
	Connect(pattern string, handler interface{}, args ...EndpointParam)
	Trace(pattern string, handler interface{}, args ...EndpointParam)
	Route(pattern string, handler RouteHandler, args ...EndpointParam)
	Handle(pattern string, handler http.Handler)
	Handler() (http.Handler, error)
	MustHandler() http.Handler
}

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server handles http endpoints

func NewServer

func NewServer(logger Logger) *Server

NewServer constructs a server

func (*Server) AddEndpoint

func (r *Server) AddEndpoint(method Method, name string, handler interface{}, params []EndpointParam)

func (*Server) Connect

func (r *Server) Connect(pattern string, handler interface{}, args ...EndpointParam)

Connect adds an endpoint with a CONNECT Method

func (*Server) Delete

func (r *Server) Delete(pattern string, handler interface{}, args ...EndpointParam)

Delete adds an endpoint with a DELETE Method

func (*Server) Get

func (r *Server) Get(pattern string, handler interface{}, args ...EndpointParam)

Get adds an endpoint with a GET Method

func (*Server) Handle

func (r *Server) Handle(pattern string, handler http.Handler)

Handle handles request with specified http handler

func (*Server) Handler

func (r *Server) Handler() (http.Handler, error)

Handler returns an http.Handler of the API

func (*Server) Head

func (r *Server) Head(pattern string, handler interface{}, args ...EndpointParam)

Head adds an endpoint with a HEAD Method

func (*Server) MustHandler

func (r *Server) MustHandler() http.Handler

MustHandler returns a handler but panics if handler cannot be obtained

func (*Server) Options

func (r *Server) Options(pattern string, handler interface{}, args ...EndpointParam)

Options adds an endpoint with a OPTIONS Method

func (*Server) Patch

func (r *Server) Patch(pattern string, handler interface{}, args ...EndpointParam)

Patch adds an endpoint with a PATCH Method

func (*Server) Post

func (r *Server) Post(pattern string, handler interface{}, args ...EndpointParam)

Post adds an endpoint with a POST Method

func (*Server) Put

func (r *Server) Put(pattern string, handler interface{}, args ...EndpointParam)

Put adds an endpoint with a PUT Method

func (*Server) Route

func (r *Server) Route(pattern string, handler RouteHandler, params ...EndpointParam)

Route routs endpoints to a specific path

func (*Server) Start

func (s *Server) Start(address string) error

Start starts the api

func (*Server) Trace

func (r *Server) Trace(pattern string, handler interface{}, args ...EndpointParam)

Trace adds an endpoint with a TRACE Method

func (*Server) Use

func (r *Server) Use(middlewares ...func(http.Handler) http.Handler)

Use adds chi middlewares

func (*Server) With

func (r *Server) With(middlewares ...func(http.Handler) http.Handler) Router

With returns a version of a handler with a middleware

Directories

Path Synopsis
Package mocks is a generated GoMock package.
Package mocks is a generated GoMock package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL