├── img ├── dump.png ├── platypus.jpg └── screenshot.png ├── .github ├── renovate.json └── workflows │ ├── goreleaser.yml │ └── ci.yml ├── .gitignore ├── cmd ├── logo.go ├── version.go └── root.go ├── Dockerfile ├── Dockerfile.pack ├── .goreleaser.yml ├── infra └── cors.go ├── LICENSE ├── mocker ├── mocker.go ├── dump.go ├── notfound.go ├── generator.go ├── response.go ├── validator.go ├── method.go └── endpoints.go ├── main.go ├── Makefile ├── go.mod ├── README.md └── go.sum /img/dump.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depado/platypus/HEAD/img/dump.png -------------------------------------------------------------------------------- /img/platypus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depado/platypus/HEAD/img/platypus.jpg -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depado/platypus/HEAD/img/screenshot.png -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:best-practices", ":automergeMinor", ":automergeDigest"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: GoReleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | actions: read 11 | 12 | jobs: 13 | goreleaser: 14 | uses: depado/github-actions/.github/workflows/goreleaser.yml@main 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | mocks/ 14 | mock.yml 15 | platypus 16 | dist/ 17 | coverage.txt 18 | .vscode -------------------------------------------------------------------------------- /cmd/logo.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Logo is the string that should be displayed when the program starts 4 | var Logo = ` _ _ 5 | _ __ | | __ _| |_ _ _ _ __ _ _ ___ 6 | | '_ \| |/ _` + "`" + ` | __| | | | '_ \| | | / __| 7 | | |_) | | (_| | |_| |_| | |_) | |_| \__ \ 8 | | .__/|_|\__,_|\__|\__, | .__/ \__,_|___/ 9 | |_| |___/|_| 10 | ` 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Step 2 | FROM golang:1.25.5-alpine@sha256:72567335df90b4ed71c01bf91fb5f8cc09fc4d5f6f21e183a085bafc7ae1bec8 as builder 3 | 4 | # Dependencies 5 | RUN apk update && apk add --no-cache make git 6 | 7 | # Source 8 | WORKDIR $GOPATH/src/github.com/depado/platypus 9 | COPY go.mod go.sum ./ 10 | RUN go mod download 11 | RUN go mod verify 12 | COPY . . 13 | 14 | # Build 15 | RUN make tmp 16 | 17 | 18 | # Final Step 19 | FROM gcr.io/distroless/static@sha256:4b2a093ef4649bccd586625090a3c668b254cfe180dee54f4c94f3e9bd7e381e 20 | COPY --from=builder /tmp/platypus /go/bin/platypus 21 | ENTRYPOINT ["/go/bin/platypus"] 22 | -------------------------------------------------------------------------------- /Dockerfile.pack: -------------------------------------------------------------------------------- 1 | # Build Step 2 | FROM golang:1.25.5-alpine@sha256:72567335df90b4ed71c01bf91fb5f8cc09fc4d5f6f21e183a085bafc7ae1bec8 as builder 3 | 4 | # Dependencies 5 | RUN apk update && apk add --no-cache upx make git 6 | 7 | # Source 8 | WORKDIR $GOPATH/src/github.com/depado/platypus 9 | COPY go.mod go.sum ./ 10 | RUN go mod download 11 | RUN go mod verify 12 | COPY . . 13 | 14 | # Build 15 | RUN make tmp 16 | RUN upx --best --lzma /tmp/platypus 17 | 18 | 19 | # Final Step 20 | FROM gcr.io/distroless/static@sha256:4b2a093ef4649bccd586625090a3c668b254cfe180dee54f4c94f3e9bd7e381e 21 | COPY --from=builder /tmp/platypus /go/bin/platypus 22 | ENTRYPOINT ["/go/bin/platypus"] 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: ["v*"] 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | golang: 12 | name: Golang 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | checks: write 17 | uses: depado/github-actions/.github/workflows/golang.yml@main 18 | 19 | docker: 20 | name: Docker 21 | needs: [golang] 22 | permissions: 23 | contents: read 24 | packages: write 25 | actions: read 26 | uses: depado/github-actions/.github/workflows/docker.yml@main 27 | secrets: 28 | cleanup-token: ${{ secrets.PAT_DELETE_PACKAGES }} 29 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | 7 | builds: 8 | - goarch: [amd64, arm, arm64, 386] 9 | goarm: [6, 7] 10 | goos: [linux, darwin, windows] 11 | ignore: 12 | - goos: darwin 13 | goarch: 386 14 | env: 15 | - CGO_ENABLED=0 16 | ldflags: 17 | - -s -w 18 | - -X 'main.Version={{ .Version }}' 19 | - -X 'main.Build={{ .Commit }}' 20 | 21 | archives: 22 | - format: tar.gz 23 | name_template: >- 24 | {{ .ProjectName }}_ 25 | {{- .Os }}_ 26 | {{- if eq .Arch "amd64" }}x86_64 27 | {{- else if eq .Arch "386" }}i386 28 | {{- else }}{{ .Arch }}{{ end }} 29 | {{- if .Arm }}v{{ .Arm }}{{ end }} 30 | format_overrides: 31 | - goos: windows 32 | format: zip 33 | 34 | changelog: 35 | sort: asc 36 | filters: 37 | exclude: 38 | - "^docs:" 39 | - "^test:" 40 | -------------------------------------------------------------------------------- /infra/cors.go: -------------------------------------------------------------------------------- 1 | package infra 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-contrib/cors" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // NewCorsConfig generates a new cors config with the given parameters 11 | func NewCorsConfig(enable, all bool, origins, methods, headers, expose []string) *cors.Config { 12 | if !enable { 13 | return nil 14 | } 15 | c := &cors.Config{ 16 | AllowCredentials: true, 17 | MaxAge: 50 * time.Second, 18 | AllowMethods: methods, 19 | AllowHeaders: headers, 20 | ExposeHeaders: expose, 21 | } 22 | 23 | switch { 24 | case len(origins) > 0: 25 | c.AllowOrigins = origins 26 | case all: 27 | c.AllowAllOrigins = true 28 | default: 29 | logrus.WithField("error", "allow all origin disabled but no allowed origin provided").Fatal("Couldn't configure CORS") 30 | } 31 | logrus.WithFields(logrus.Fields{ 32 | "AllowMethods": methods, 33 | "AllowHeaders": headers, 34 | "ExposeHeaders": expose, 35 | "AllowOrigins": origins, 36 | "AllowAllOrigins": all, 37 | }).Debug("CORS configuration") 38 | return c 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mocker/mocker.go: -------------------------------------------------------------------------------- 1 | package mocker 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/pkg/errors" 8 | "github.com/sirupsen/logrus" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | // MockConf contains all the endpoints and is used for parsing. 13 | type MockConf struct { 14 | Endpoints []Endpoint `yaml:"endpoints"` 15 | NoRoute *NoRouteConf `yaml:"noroute"` 16 | } 17 | 18 | // GenerateRoutes generates the routes and applies them to the r gin.Engine. 19 | func GenerateRoutes(path string, r *gin.Engine) error { 20 | var err error 21 | var out []byte 22 | 23 | mc := &MockConf{} 24 | if out, err = os.ReadFile(path); err != nil { 25 | return errors.Wrap(err, "open file") 26 | } 27 | logrus.Infof("Found mock file %s", path) 28 | 29 | if err = yaml.Unmarshal(out, mc); err != nil { 30 | return errors.Wrap(err, "unmarshal") 31 | } 32 | logrus.Info("Mock file is valid") 33 | 34 | for _, e := range mc.Endpoints { 35 | e.Compute() 36 | for _, g := range e.All { 37 | g.Generate(e.Path, r) 38 | } 39 | } 40 | 41 | if mc.NoRoute != nil { 42 | mc.NoRoute.Handle(r) 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // Build number and versions injected at compile time 10 | var ( 11 | Version = "unknown" 12 | Build = "unknown" 13 | Time = "unknown" 14 | Packer = "" 15 | ) 16 | 17 | // Descriptive help text for version command 18 | var versionHelp = ` 19 | This command will output the build number, version number and build date of platypus. 20 | The build number corresponds to the sha1 commit the binary was built against, 21 | while the version number corresponds to the latest tag the binary was built on. 22 | Finally the build date corresponds to the date the binary was built. 23 | 24 | If both values are "unknown" make sure to build platypus with "make". 25 | ` 26 | 27 | // VersionCmd is a command that will display the build number and version (if any) 28 | var VersionCmd = &cobra.Command{ 29 | Use: "version", 30 | Short: "Show build, version and build date", 31 | Long: versionHelp, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | packed := "" 34 | if Packer != "" { 35 | packed = fmt.Sprintf("Packer: %s\n", Packer) 36 | } 37 | 38 | fmt.Printf("Build: %s\nVersion: %s\nBuild Date: %s\n%s", Build, Version, Time, packed) 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /mocker/dump.go: -------------------------------------------------------------------------------- 1 | package mocker 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/logrusorgru/aurora" 9 | ) 10 | 11 | // Dump is a type alias 12 | type Dump []string 13 | 14 | // Contains will return whether or not a specific string is present in the 15 | // slice or not. 16 | func (d Dump) Contains(s string) bool { 17 | for _, v := range d { 18 | if v == s { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | // Handle will handle request dumping if any. 26 | func (d Dump) Handle(r *http.Request, body []byte) { 27 | fmt.Println("--------------------------------------------------") 28 | if d.Contains("host") || d.Contains("all") { 29 | fmt.Printf("%s %s\n", aurora.Blue("Host:"), r.Host) 30 | } 31 | 32 | if d.Contains("proto") || d.Contains("all") { 33 | fmt.Printf("%s %s\n", aurora.Blue("Proto:"), r.Proto) 34 | } 35 | 36 | if d.Contains("host") || d.Contains("proto") || d.Contains("all") { 37 | fmt.Println() 38 | } 39 | 40 | if d.Contains("headers") || d.Contains("all") { 41 | for k, v := range r.Header { 42 | fmt.Printf("%s %s\n", aurora.BrightBlue(k+":"), strings.Join(v, ",")) 43 | } 44 | fmt.Println() 45 | } 46 | 47 | if (d.Contains("body") || d.Contains("all")) && len(body) > 0 { 48 | fmt.Printf("%s\n\n", string(body)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /mocker/notfound.go: -------------------------------------------------------------------------------- 1 | package mocker 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/logrusorgru/aurora" 10 | ) 11 | 12 | // NoRouteConf defines the behavior when no route is found in the mock. Defaults 13 | // to a standard 404 response. 14 | type NoRouteConf struct { 15 | Dump Dump `yaml:"dump"` 16 | Echo bool `yaml:"echo"` 17 | Code *int `yaml:"code"` 18 | Body *string `yaml:"body"` 19 | } 20 | 21 | // Handle will add a special route to customize the 404 behavior. 22 | func (nc NoRouteConf) Handle(r *gin.Engine) { 23 | 24 | code := http.StatusNotFound 25 | if nc.Code != nil { 26 | code = *nc.Code 27 | } 28 | 29 | fmt.Printf("\n%s %s", aurora.Underline("No route"), codeToColor(code).String()) 30 | 31 | if nc.Echo { 32 | fmt.Print(" 🔊") 33 | } 34 | fmt.Print("\n") 35 | 36 | r.NoRoute(func(c *gin.Context) { 37 | body, err := io.ReadAll(c.Request.Body) 38 | if err != nil { 39 | c.Status(http.StatusInternalServerError) 40 | return 41 | } 42 | 43 | nc.Dump.Handle(c.Request, body) 44 | 45 | if nc.Echo { 46 | for header, values := range c.Request.Header { 47 | for _, v := range values { 48 | c.Header(header, v) 49 | } 50 | } 51 | status := http.StatusOK 52 | if nc.Code != nil { 53 | status = *nc.Code 54 | } 55 | if nc.Body != nil { 56 | c.String(status, string(body)) 57 | } else { 58 | c.Status(status) 59 | } 60 | return 61 | } 62 | 63 | if nc.Body == nil { 64 | c.Status(code) 65 | return 66 | } 67 | 68 | c.String(code, *nc.Body) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/depado/platypus/cmd" 7 | "github.com/depado/platypus/infra" 8 | "github.com/depado/platypus/mocker" 9 | "github.com/gin-contrib/cors" 10 | "github.com/gin-gonic/gin" 11 | "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | // Main command that will be run when no other command is provided on the 17 | // command-line 18 | var rootCmd = &cobra.Command{ 19 | Use: "platypus", 20 | Short: "Platypus is a very simple mock server", 21 | Run: func(cmd *cobra.Command, args []string) { run() }, // nolint: unparam 22 | } 23 | 24 | func run() { 25 | var err error 26 | 27 | fmt.Println(cmd.Logo) 28 | gin.SetMode(viper.GetString("server.mode")) 29 | r := gin.Default() 30 | corsc := infra.NewCorsConfig( 31 | viper.GetBool("server.cors.enable"), 32 | viper.GetBool("server.cors.all"), 33 | viper.GetStringSlice("server.cors.origins"), 34 | viper.GetStringSlice("server.cors.methods"), 35 | viper.GetStringSlice("server.cors.headers"), 36 | viper.GetStringSlice("server.cors.expose"), 37 | ) 38 | if corsc != nil { 39 | logrus.Info("CORS Enabled") 40 | r.Use(cors.New(*corsc)) 41 | } 42 | 43 | logrus.Info("Generating Routes") 44 | if err = mocker.GenerateRoutes(viper.GetString("mock"), r); err != nil { 45 | logrus.WithError(err).Fatal("Couldn't generate routes") 46 | } 47 | fmt.Println() 48 | logrus.Infof("Running Mock Server on %s:%d", viper.GetString("server.host"), viper.GetInt("server.port")) 49 | if err = r.Run(fmt.Sprintf("%s:%d", viper.GetString("server.host"), viper.GetInt("server.port"))); err != nil { 50 | logrus.WithError(err).Fatal("Couldn't start router") 51 | } 52 | } 53 | 54 | func main() { 55 | // Initialize Cobra and Viper 56 | cobra.OnInitialize(cmd.Initialize) 57 | cmd.AddAllFlags(rootCmd) 58 | rootCmd.AddCommand(cmd.VersionCmd) 59 | 60 | // Run the command 61 | if err := rootCmd.Execute(); err != nil { 62 | logrus.WithError(err).Fatal("Couldn't start") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := build 2 | 3 | export GO111MODULE=on 4 | export CGO_ENABLED=0 5 | export VERSION=$(shell git describe --abbrev=0 --tags 2> /dev/null || echo "0.1.0") 6 | export BUILD=$(shell git rev-parse HEAD 2> /dev/null || echo "undefined") 7 | export BUILDDATE=$(shell LANG=en_us_88591 date) 8 | BINARY=platypus 9 | LDFLAGS=-ldflags "-X 'github.com/depado/platypus/cmd.Version=$(VERSION)' \ 10 | -X 'github.com/depado/platypus/cmd.Build=$(BUILD)' \ 11 | -X 'github.com/depado/platypus/cmd.Time=$(BUILDDATE)' -s -w" 12 | PACKEDFLAGS=-ldflags "-X 'github.com/depado/platypus/cmd.Version=$(VERSION)' \ 13 | -X 'github.com/depado/platypus/cmd.Build=$(BUILD)' \ 14 | -X 'github.com/depado/platypus/cmd.Time=$(BUILDDATE)' \ 15 | -X 'github.com/depado/platypus/cmd.Packer=upx --best --lzma' -s -w" 16 | 17 | .PHONY: help 18 | help: ## Display help text for makefile 19 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 20 | 21 | .PHONY: build 22 | build: ## Build 23 | go build $(LDFLAGS) -o $(BINARY) 24 | 25 | .PHONY: tmp 26 | tmp: ## Build and output the binary in /tmp 27 | go build $(LDFLAGS) -o /tmp/$(BINARY) 28 | 29 | .PHONY: packed 30 | packed: ## Build a packed version of the binary 31 | go build $(PACKEDFLAGS) -o $(BINARY) 32 | upx --best --lzma $(BINARY) 33 | 34 | .PHONY: docker 35 | docker: ## Build the docker image 36 | docker build -t $(BINARY):latest -t $(BINARY):$(BUILD) -f Dockerfile . 37 | 38 | .PHONY: release 39 | release: ## Create a new release on Github 40 | goreleaser 41 | 42 | .PHONY: snapshot 43 | snapshot: ## Create a new snapshot release 44 | goreleaser --snapshot --clean 45 | 46 | .PHONY: lint 47 | lint: ## Runs the linter 48 | $(GOPATH)/bin/golangci-lint run --exclude-use-default=false 49 | 50 | .PHONY: test 51 | test: ## Run the test suite 52 | CGO_ENABLED=1 go test -race -coverprofile="coverage.txt" ./... 53 | 54 | .PHONY: clean 55 | clean: ## Remove the binary 56 | if [ -f $(BINARY) ] ; then rm $(BINARY) ; fi 57 | if [ -f coverage.txt ] ; then rm coverage.txt ; fi 58 | -------------------------------------------------------------------------------- /mocker/generator.go: -------------------------------------------------------------------------------- 1 | package mocker 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // EndpointGenerator is a simple interface. 6 | type EndpointGenerator interface { 7 | Generate(string, *gin.Engine) 8 | } 9 | 10 | // GetEndpoint implements the EndpointGenerator interface. 11 | type GetEndpoint struct { 12 | EndpointMethod `yaml:",inline"` 13 | } 14 | 15 | // Generate generates the endpoint 16 | func (e GetEndpoint) Generate(path string, r *gin.Engine) { r.GET(path, e.ToHandler()) } 17 | 18 | // PostEndpoint implements the EndpointGenerator interface. 19 | type PostEndpoint struct { 20 | EndpointMethod `yaml:",inline"` 21 | } 22 | 23 | // Generate generates the endpoint 24 | func (e PostEndpoint) Generate(path string, r *gin.Engine) { r.POST(path, e.ToHandler()) } 25 | 26 | // PutEndpoint implements the EndpointGenerator interface. 27 | type PutEndpoint struct { 28 | EndpointMethod `yaml:",inline"` 29 | } 30 | 31 | // Generate generates the endpoint 32 | func (e PutEndpoint) Generate(path string, r *gin.Engine) { r.PUT(path, e.ToHandler()) } 33 | 34 | // PatchEndpoint implements the EndpointGenerator interface. 35 | type PatchEndpoint struct { 36 | EndpointMethod `yaml:",inline"` 37 | } 38 | 39 | // Generate generates the endpoint 40 | func (e PatchEndpoint) Generate(path string, r *gin.Engine) { r.PATCH(path, e.ToHandler()) } 41 | 42 | // DeleteEndpoint implements the EndpointGenerator interface. 43 | type DeleteEndpoint struct { 44 | EndpointMethod `yaml:",inline"` 45 | } 46 | 47 | // Generate generates the endpoint 48 | func (e DeleteEndpoint) Generate(path string, r *gin.Engine) { r.DELETE(path, e.ToHandler()) } 49 | 50 | // HeadEndpoint implements the EndpointGenerator interface. 51 | type HeadEndpoint struct { 52 | EndpointMethod `yaml:",inline"` 53 | } 54 | 55 | // Generate generates the endpoint 56 | func (e HeadEndpoint) Generate(path string, r *gin.Engine) { r.HEAD(path, e.ToHandler()) } 57 | 58 | // OptionsEndpoint implements the EndpointGenerator interface. 59 | type OptionsEndpoint struct { 60 | EndpointMethod `yaml:",inline"` 61 | } 62 | 63 | // Generate generates the endpoint. 64 | func (e OptionsEndpoint) Generate(path string, r *gin.Engine) { r.OPTIONS(path, e.ToHandler()) } 65 | -------------------------------------------------------------------------------- /mocker/response.go: -------------------------------------------------------------------------------- 1 | package mocker 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/logrusorgru/aurora" 9 | ) 10 | 11 | // Responses represents multiple responses. 12 | type Responses []Response 13 | 14 | // Pick will pick the appropriate response. 15 | func (rr Responses) Pick(platy string) Response { 16 | // No desired code/name, return the first response 17 | if platy == "" { 18 | return rr[0] 19 | } 20 | 21 | // Find by name first 22 | for _, r := range rr { 23 | if r.Name == platy { 24 | return r 25 | } 26 | } 27 | 28 | // Find by status code next 29 | o, err := strconv.Atoi(platy) 30 | if err == nil { 31 | for _, r := range rr { 32 | if r.Code == o { 33 | return r 34 | } 35 | } 36 | } 37 | 38 | return rr[0] 39 | } 40 | 41 | // Response represents the response structure for a single endpoint method. 42 | type Response struct { 43 | Name string `yaml:"name"` 44 | Code int `yaml:"code"` 45 | Echo bool `yaml:"echo"` 46 | Headers map[string]string `yaml:"headers"` 47 | Preset string `yaml:"preset"` 48 | Body string `yaml:"body"` 49 | } 50 | 51 | // Info returns a string to print out the information of a response. 52 | func (r Response) Info(prefix string, last bool) string { 53 | var sb strings.Builder 54 | s := "├─" 55 | if last { 56 | s = "└─" 57 | } 58 | 59 | sb.WriteString(fmt.Sprintf("%s %s %s", prefix, s, codeToColor(r.Code).String())) 60 | if r.Name != "" { 61 | sb.WriteString(aurora.Cyan(" " + r.Name).String()) 62 | } 63 | if r.Preset != "" { 64 | switch r.Preset { 65 | case "json": 66 | sb.WriteString(" JSON") 67 | case "text": 68 | sb.WriteString(" Text") 69 | default: 70 | sb.WriteString(" " + r.Preset) 71 | } 72 | } 73 | if r.Echo { 74 | sb.WriteString(" 🔊") 75 | } 76 | 77 | return sb.String() 78 | } 79 | 80 | func codeToColor(code int) aurora.Value { 81 | switch { 82 | case code >= 100 && code < 200: 83 | return aurora.Cyan(code) 84 | case code >= 200 && code < 300: 85 | return aurora.Green(code) 86 | case code >= 300 && code < 400: 87 | return aurora.Yellow(code) 88 | case code >= 400 && code < 500: 89 | return aurora.Red(code) 90 | case code >= 500 && code < 600: 91 | return aurora.BrightRed(code) 92 | } 93 | return aurora.White(code) 94 | } 95 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/depado/platypus 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.25.5 6 | 7 | require ( 8 | github.com/gin-contrib/cors v1.7.6 9 | github.com/gin-gonic/gin v1.11.0 10 | github.com/logrusorgru/aurora v2.0.3+incompatible 11 | github.com/onrik/logrus v0.11.0 12 | github.com/pkg/errors v0.9.1 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/spf13/cobra v1.10.2 15 | github.com/spf13/viper v1.21.0 16 | gopkg.in/yaml.v3 v3.0.1 17 | ) 18 | 19 | require ( 20 | github.com/bytedance/sonic v1.14.0 // indirect 21 | github.com/bytedance/sonic/loader v0.3.0 // indirect 22 | github.com/cloudwego/base64x v0.1.6 // indirect 23 | github.com/fsnotify/fsnotify v1.9.0 // indirect 24 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 25 | github.com/gin-contrib/sse v1.1.0 // indirect 26 | github.com/go-playground/locales v0.14.1 // indirect 27 | github.com/go-playground/universal-translator v0.18.1 // indirect 28 | github.com/go-playground/validator/v10 v10.27.0 // indirect 29 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 30 | github.com/goccy/go-json v0.10.5 // indirect 31 | github.com/goccy/go-yaml v1.18.0 // indirect 32 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 33 | github.com/json-iterator/go v1.1.12 // indirect 34 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 35 | github.com/leodido/go-urn v1.4.0 // indirect 36 | github.com/mattn/go-isatty v0.0.20 // indirect 37 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 38 | github.com/modern-go/reflect2 v1.0.2 // indirect 39 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 40 | github.com/quic-go/qpack v0.6.0 // indirect 41 | github.com/quic-go/quic-go v0.57.0 // indirect 42 | github.com/sagikazarmark/locafero v0.11.0 // indirect 43 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect 44 | github.com/spf13/afero v1.15.0 // indirect 45 | github.com/spf13/cast v1.10.0 // indirect 46 | github.com/spf13/pflag v1.0.10 // indirect 47 | github.com/subosito/gotenv v1.6.0 // indirect 48 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 49 | github.com/ugorji/go/codec v1.3.0 // indirect 50 | go.yaml.in/yaml/v3 v3.0.4 // indirect 51 | golang.org/x/arch v0.20.0 // indirect 52 | golang.org/x/crypto v0.45.0 // indirect 53 | golang.org/x/net v0.47.0 // indirect 54 | golang.org/x/sys v0.38.0 // indirect 55 | golang.org/x/text v0.31.0 // indirect 56 | google.golang.org/protobuf v1.36.9 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /mocker/validator.go: -------------------------------------------------------------------------------- 1 | package mocker 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // RequestValidator wraps a header validator and a body validator. 10 | type RequestValidator struct { 11 | Headers HeadersValidator `yaml:"headers"` 12 | Body BodyValidator `yaml:"body"` 13 | } 14 | 15 | // Handle will handle the validation of an incoming request, given the request 16 | // and the body in byte form. 17 | func (rv RequestValidator) Handle(r *http.Request, body []byte) []error { 18 | errors := rv.Headers.Validate(r) 19 | if err := rv.Body.Validate(body); err != nil { 20 | errors = append(errors, err) 21 | } 22 | 23 | return errors 24 | } 25 | 26 | // BodyValidator holds the values to validate an incoming request's body. 27 | type BodyValidator struct { 28 | Contains string `yaml:"contains"` 29 | } 30 | 31 | // Validate will validate that the incoming body matches the expectations of 32 | // the body validator. 33 | func (bv BodyValidator) Validate(body []byte) error { 34 | if bv.Contains != "" { 35 | if !strings.Contains(string(body), bv.Contains) { 36 | return fmt.Errorf("body doesn't contain '%s'", bv.Contains) 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | // HeadersValidator holds the values to validate the headers of an incoming 43 | // request. 44 | type HeadersValidator struct { 45 | Present []string `yaml:"present"` 46 | Absent []string `yaml:"absent"` 47 | Match map[string]string `yaml:"match"` 48 | } 49 | 50 | // Validate will validate that the incoming HTTP request validates the various 51 | // rules. 52 | func (hv HeadersValidator) Validate(r *http.Request) []error { 53 | errors := []error{} 54 | 55 | for _, pres := range hv.Present { 56 | found := false 57 | for k := range r.Header { 58 | if k == pres { 59 | found = true 60 | } 61 | } 62 | if !found { 63 | errors = append(errors, fmt.Errorf("header not present: %s", pres)) 64 | } 65 | } 66 | 67 | for _, abs := range hv.Absent { 68 | for k := range r.Header { 69 | if k == abs { 70 | errors = append(errors, fmt.Errorf("header present: %s", abs)) 71 | } 72 | } 73 | } 74 | 75 | for reqkey, reqval := range hv.Match { 76 | found := false 77 | for hk, hv := range r.Header { 78 | if reqkey == hk { 79 | for _, v := range hv { 80 | if reqval == v { 81 | found = true 82 | break 83 | } 84 | } 85 | } 86 | } 87 | if !found { 88 | errors = append(errors, fmt.Errorf("incorrect header value or missing header: %s:%s", reqkey, reqval)) 89 | } 90 | } 91 | 92 | return errors 93 | } 94 | -------------------------------------------------------------------------------- /mocker/method.go: -------------------------------------------------------------------------------- 1 | package mocker 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // EndpointMethod represents a single method associated to a parent endpoint. 12 | type EndpointMethod struct { 13 | Echo bool `yaml:"echo"` 14 | Dump Dump `yaml:"dump"` 15 | Validate RequestValidator `yaml:"validate"` 16 | Responses Responses `yaml:"responses"` 17 | } 18 | 19 | // Info returns the string representing the information. 20 | func (e EndpointMethod) Info(last bool) string { 21 | var sb strings.Builder 22 | pref := "\n│ " 23 | if last { 24 | pref = "\n " 25 | } 26 | 27 | for i, r := range e.Responses { 28 | sb.WriteString(r.Info(pref, i == len(e.Responses)-1)) 29 | } 30 | 31 | return sb.String() 32 | } 33 | 34 | // ToHandler generates a handler to apply on the router. 35 | func (e EndpointMethod) ToHandler() func(c *gin.Context) { 36 | return func(c *gin.Context) { 37 | // Extract the body 38 | body, err := io.ReadAll(c.Request.Body) 39 | if err != nil { 40 | c.Status(http.StatusInternalServerError) 41 | return 42 | } 43 | 44 | // Dump if required 45 | e.Dump.Handle(c.Request, body) 46 | 47 | // Validate headers and body if any validation is required 48 | if err := e.Validate.Handle(c.Request, body); len(err) != 0 { 49 | var errs []string 50 | for _, ie := range err { 51 | errs = append(errs, ie.Error()) 52 | } 53 | c.JSON(http.StatusBadRequest, gin.H{"error": "request validation failed", "reasons": errs}) 54 | return 55 | } 56 | 57 | // If no response is defined, return a 200 without a body 58 | if e.Responses == nil { 59 | c.Status(http.StatusOK) 60 | return 61 | } 62 | 63 | // Pick the desired response 64 | desired := c.Query("platy") 65 | r := e.Responses.Pick(desired) 66 | 67 | // Echo mode 68 | if r.Echo { 69 | for header, values := range c.Request.Header { 70 | for _, v := range values { 71 | c.Header(header, v) 72 | } 73 | } 74 | status := http.StatusOK 75 | if r.Code != 0 { 76 | status = r.Code 77 | } 78 | if body != nil { 79 | c.String(status, string(body)) 80 | } else { 81 | c.Status(status) 82 | } 83 | return 84 | } 85 | 86 | switch r.Preset { 87 | case "json": 88 | c.Header("Content-Type", "application/json; charset=utf-8") 89 | } 90 | 91 | for k, v := range r.Headers { 92 | c.Header(k, v) 93 | } 94 | 95 | if r.Body != "" { 96 | c.String(r.Code, r.Body) 97 | } else { 98 | c.Status(r.Code) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /mocker/endpoints.go: -------------------------------------------------------------------------------- 1 | package mocker 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/logrusorgru/aurora" 7 | ) 8 | 9 | // Endpoint represents a single endpoint. 10 | type Endpoint struct { 11 | Path string `yaml:"path"` 12 | 13 | Get *GetEndpoint `yaml:"get"` 14 | Post *PostEndpoint `yaml:"post"` 15 | Put *PutEndpoint `yaml:"put"` 16 | Patch *PatchEndpoint `yaml:"patch"` 17 | Delete *DeleteEndpoint `yaml:"delete"` 18 | Head *HeadEndpoint `yaml:"head"` 19 | Options *OptionsEndpoint `yaml:"options"` 20 | 21 | All []EndpointGenerator `yaml:"-"` 22 | } 23 | 24 | // Compute checks every possible endpoint to generate a slice of 25 | // EndpointGenerator. 26 | func (e *Endpoint) Compute() { 27 | e.All = []EndpointGenerator{} 28 | var hasNext bool 29 | withNext := "├─ %s%s\n│\n" 30 | withoutNext := "└─ %s%s\n" 31 | 32 | fmt.Printf("\n%s\n", aurora.Underline(e.Path)) 33 | if e.Get != nil { 34 | e.All = append(e.All, e.Get) 35 | hasNext = e.Post != nil || e.Put != nil || e.Patch != nil || e.Delete != nil || e.Head != nil || e.Options != nil 36 | if hasNext { 37 | fmt.Printf(withNext, aurora.Blue("GET"), e.Get.Info(!hasNext)) 38 | } else { 39 | fmt.Printf(withoutNext, aurora.Blue("GET"), e.Get.Info(!hasNext)) 40 | } 41 | } 42 | if e.Post != nil { 43 | e.All = append(e.All, e.Post) 44 | hasNext = e.Put != nil || e.Patch != nil || e.Delete != nil || e.Head != nil || e.Options != nil 45 | if hasNext { 46 | fmt.Printf(withNext, aurora.Green("POST"), e.Post.Info(!hasNext)) 47 | } else { 48 | fmt.Printf(withoutNext, aurora.Green("POST"), e.Post.Info(!hasNext)) 49 | } 50 | } 51 | if e.Put != nil { 52 | e.All = append(e.All, e.Put) 53 | hasNext = e.Patch != nil || e.Delete != nil || e.Head != nil || e.Options != nil 54 | if hasNext { 55 | fmt.Printf(withNext, aurora.Yellow("PUT"), e.Put.Info(!hasNext)) 56 | } else { 57 | fmt.Printf(withoutNext, aurora.Yellow("PUT"), e.Put.Info(!hasNext)) 58 | } 59 | } 60 | if e.Patch != nil { 61 | e.All = append(e.All, e.Patch) 62 | hasNext = e.Delete != nil || e.Head != nil || e.Options != nil 63 | if hasNext { 64 | fmt.Printf(withNext, aurora.BrightYellow("PATCH"), e.Patch.Info(!hasNext)) 65 | } else { 66 | fmt.Printf(withoutNext, aurora.BrightYellow("PATCH"), e.Patch.Info(!hasNext)) 67 | } 68 | } 69 | if e.Delete != nil { 70 | e.All = append(e.All, e.Delete) 71 | hasNext = e.Head != nil || e.Options != nil 72 | if hasNext { 73 | fmt.Printf(withNext, aurora.Red("DELETE"), e.Delete.Info(!hasNext)) 74 | } else { 75 | fmt.Printf(withoutNext, aurora.Red("DELETE"), e.Delete.Info(!hasNext)) 76 | } 77 | } 78 | if e.Head != nil { 79 | e.All = append(e.All, e.Head) 80 | hasNext = e.Options != nil 81 | if hasNext { 82 | fmt.Printf(withNext, aurora.Cyan("HEAD"), e.Head.Info(!hasNext)) 83 | } else { 84 | fmt.Printf(withoutNext, aurora.Cyan("HEAD"), e.Head.Info(!hasNext)) 85 | } 86 | } 87 | if e.Options != nil { 88 | e.All = append(e.All, e.Options) 89 | fmt.Printf(withoutNext, aurora.BrightCyan("OPTIONS"), e.Options.Info(true)) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/onrik/logrus/filename" 7 | "github.com/sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | // AddLoggerFlags adds support to configure the level of the logger 13 | func AddLoggerFlags(c *cobra.Command) { 14 | c.PersistentFlags().String("log.level", "info", "one of debug, info, warn, error or fatal") 15 | c.PersistentFlags().String("log.format", "text", "one of text or json") 16 | c.PersistentFlags().Bool("log.line", false, "enable filename and line in logs") 17 | } 18 | 19 | // AddServerFlags adds support to configure the server 20 | func AddServerFlags(c *cobra.Command) { 21 | // Server related flags 22 | c.PersistentFlags().String("server.host", "127.0.0.1", "host on which the server should listen") 23 | c.PersistentFlags().Int("server.port", 8080, "port on which the server should listen") 24 | c.PersistentFlags().String("server.mode", "release", "server mode can be either 'debug', 'test' or 'release'") 25 | 26 | // CORS related flags 27 | c.PersistentFlags().Bool("server.cors.enable", false, "enable CORS") 28 | c.PersistentFlags().StringSlice("server.cors.methods", []string{"GET", "PUT", "POST", "DELETE", "OPTION", "PATCH"}, "array of allowed method when cors is enabled") 29 | c.PersistentFlags().StringSlice("server.cors.headers", []string{"Origin", "Authorization", "Content-Type"}, "array of allowed headers") 30 | c.PersistentFlags().StringSlice("server.cors.expose", []string{}, "array of exposed headers") 31 | c.PersistentFlags().StringSlice("server.cors.origins", []string{}, "array of allowed origins (overwritten if all is active)") 32 | c.PersistentFlags().Bool("server.cors.all", false, "defines that all origins are allowed") 33 | } 34 | 35 | // AddConfigurationFlag adds support to provide a configuration file on the 36 | // command line 37 | func AddConfigurationFlag(c *cobra.Command) { 38 | c.PersistentFlags().String("conf", "", "configuration file to use") 39 | c.PersistentFlags().String("mock", "mock.yml", "file to mock from") 40 | } 41 | 42 | // AddAllFlags will add all the flags provided in this package to the provided 43 | // command and will bind those flags with viper 44 | func AddAllFlags(c *cobra.Command) { 45 | AddConfigurationFlag(c) 46 | AddLoggerFlags(c) 47 | AddServerFlags(c) 48 | if err := viper.BindPFlags(c.PersistentFlags()); err != nil { 49 | logrus.WithError(err).WithField("step", "AddAllFlags").Fatal("Couldn't bind flags") 50 | } 51 | } 52 | 53 | // Initialize will be run when cobra finishes its initialization 54 | func Initialize() { 55 | // Environment variables 56 | viper.AutomaticEnv() 57 | viper.SetEnvPrefix("platypus") 58 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 59 | 60 | // Configuration file 61 | if viper.GetString("conf") != "" { 62 | viper.SetConfigFile(viper.GetString("conf")) 63 | } else { 64 | viper.SetConfigName("conf") 65 | viper.AddConfigPath(".") 66 | viper.AddConfigPath("/config/") 67 | } 68 | hasconf := viper.ReadInConfig() == nil 69 | 70 | // Set log level 71 | lvl := viper.GetString("log.level") 72 | if l, err := logrus.ParseLevel(lvl); err != nil { 73 | logrus.WithFields(logrus.Fields{"level": lvl, "fallback": "info"}).Warn("Invalid log level") 74 | } else { 75 | logrus.SetLevel(l) 76 | } 77 | 78 | // Set log format 79 | switch viper.GetString("log.format") { 80 | case "json": 81 | logrus.SetFormatter(&logrus.JSONFormatter{}) 82 | default: 83 | logrus.SetFormatter(&logrus.TextFormatter{ 84 | DisableTimestamp: true, 85 | ForceColors: true, 86 | }) 87 | } 88 | 89 | // Defines if logrus should display filenames and line where the log ocured 90 | if viper.GetBool("log.line") { 91 | logrus.AddHook(filename.NewHook()) 92 | } 93 | 94 | // Delays the log for once the logger has been setup 95 | if hasconf { 96 | logrus.WithField("file", viper.ConfigFileUsed()).Debug("Found configuration file") 97 | } else { 98 | logrus.Debug("No configuration file found") 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Platypus

2 |

3 | mascot 4 | 5 | [![forthebadge](https://forthebadge.com/images/badges/made-with-go.svg)](https://forthebadge.com)[![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com)[![forthebadge](https://forthebadge.com/images/badges/uses-badges.svg)](https://forthebadge.com) 6 | 7 | ![Go Version](https://img.shields.io/badge/Go%20Version-latest-brightgreen.svg) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/depado/platypus)](https://goreportcard.com/report/github.com/depado/platypus) 9 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/depado/platypus/blob/master/LICENSE) 10 | [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/Depado) 11 | 12 | Very simple mock server that doesn't do much 13 |

14 | 15 | > That external API you're relying on is supposed to send back a duck. 16 | > Unfortunately sometimes things don't go as expected and you're dealing with a 17 | > platypus instead. It's ok. 18 | 19 | ## Introduction 20 | 21 | Platypus is a very simple mock server to abstract external services. It supports 22 | CORS which is disabled by default but fully configurable. 23 | 24 | ## Install 25 | 26 | ### From source 27 | 28 | You can install platypus by using directly the go command line: 29 | 30 | ```bash 31 | $ go install github.com/depado/platypus 32 | ``` 33 | 34 | Or if you wish to have the version information and such: 35 | 36 | ``` 37 | $ git clone git@github.com:Depado/platypus.git 38 | $ make install 39 | ``` 40 | 41 | ### Binary release 42 | 43 | You can also download the latest release for your system on the 44 | [release page](https://github.com/depado/platypus/releases). 45 | 46 | ### Docker image 47 | 48 | You can run platypus directly using docker using the packages found 49 | [on this page](https://github.com/depado/platypus/pkgs/container/platypus). 50 | 51 | Example: 52 | 53 | ```bash 54 | docker run -it --rm -v (pwd):/data ghcr.io/depado/platypus:latest --mock mocks/gerrit.yml 55 | ``` 56 | 57 | ## Usage 58 | 59 |

60 | screenshot 61 |

62 | 63 | ``` 64 | Platypus is a very simple mock server 65 | 66 | Usage: 67 | platypus [flags] 68 | platypus [command] 69 | 70 | Available Commands: 71 | help Help about any command 72 | version Show build and version 73 | 74 | Flags: 75 | --conf string configuration file to use 76 | -h, --help help for platypus 77 | --log.format string one of text or json (default "text") 78 | --log.level string one of debug, info, warn, error or fatal (default "info") 79 | --log.line enable filename and line in logs 80 | --mock string file to mock from (default "mock.yml") 81 | --server.cors.all defines that all origins are allowed 82 | --server.cors.enable enable CORS 83 | --server.cors.expose strings array of exposed headers 84 | --server.cors.headers strings array of allowed headers (default [Origin,Authorization,Content-Type]) 85 | --server.cors.methods strings array of allowed method when cors is enabled (default [GET,PUT,POST,DELETE,OPTION,PATCH]) 86 | --server.cors.origins strings array of allowed origins (overwritten if all is active) 87 | --server.host string host on which the server should listen (default "127.0.0.1") 88 | --server.mode string server mode can be either 'debug', 'test' or 'release' (default "release") 89 | --server.port int port on which the server should listen (default 8080) 90 | 91 | Use "platypus [command] --help" for more information about a command. 92 | ``` 93 | 94 | When starting up, platypus will display the endpoints with the associated 95 | method(s) as well as the response information (type of response, code, name, 96 | etc) 97 | 98 | ## Configuration 99 | 100 | ### mock.yml 101 | 102 | To define how platypus should behave you should create a `mock.yml` file where 103 | you're going to start platypus. Alternatively you can place the yaml file 104 | anywhere and name it as you like if you provide the `--mock ` flag when 105 | starting platypus. 106 | 107 | This file contains the various definitions of your endpoints like so: 108 | 109 | ```yaml 110 | --- 111 | endpoints: 112 | - path: /hello/world 113 | get: 114 | responses: 115 | - code: 200 116 | body: | 117 | {"hello": "world"} 118 | 119 | - path: /with/parameter/:param 120 | get: 121 | responses: 122 | - code: 200 123 | body: | 124 | {"hello": "world"} 125 | ``` 126 | 127 | Each endpoint is defined by a string which can accept query params in the 128 | same form as [gin](https://github.com/gin-gonic/gin). Then you can define the 129 | HTTP methods it should handle (`get`, `post`, `put`, `patch`, `delete`, `head`, 130 | `options`). Each of these method can have a specific behavior. 131 | 132 | ## Methods 133 | 134 | Each method associated to an endpoint can be customized to add validation 135 | behavior or to output more information when it gets called. 136 | 137 | ```yaml 138 | endpoints: 139 | - path: /hello/world 140 | get: 141 | dump: [headers,body,host] 142 | validate: 143 | headers: 144 | present: [Authorization] 145 | absent: [Forbidden, X-Forbidden] 146 | match: 147 | X-Exact-Value: "exact value" 148 | body: 149 | contains: "interesting value" 150 | responses: 151 | - code: 200 152 | body: | 153 | {"hello": "world"} 154 | ``` 155 | 156 | ### Dump 157 | 158 | `dump` defines what should be shown in the terminal when a request calls this 159 | specific endpoint. Values can be `header`, `body`, `host`, `proto` or any 160 | combination of these. The special option `all` can also be used to group all 161 | these. 162 | 163 |

164 | screenshot 165 |

166 | 167 | ### Validate 168 | 169 | The `validate` key can be used to validate incoming requests to match special 170 | rules. This is especially useful to quickly see whether or not a request is 171 | supposed to fail with the non-mocked API. This is entirely optional as it may 172 | not be the desired behavior anyway. 173 | 174 | - `headers` 175 | - `present`: List of headers the incoming request must have 176 | - `absent`: List of headers the incoming request must not have 177 | - `match`: Header/Value pairs the incoming request must have 178 | - `body` 179 | - `contains`: 180 | 181 | 182 | 183 | ### Responses 184 | 185 | Every method in a path can have multiple responses. These responses are sent 186 | in a random manner when the endpoint is called. This allows to simulate 187 | unexpected behaviors on the mock side. If you wish you can add a `ratio` keyword 188 | which will tell platypus how often this response should be sent back. For 189 | example the following example will send back a `500` error 1% of the time: 190 | 191 | ```yaml 192 | endpoints: 193 | - path: /failure/maybe 194 | get: 195 | responses: 196 | - code: 200 197 | - code: 500 198 | ratio: 1 199 | ``` 200 | 201 | The ratio keyword is a percentage. If no ratio is provided, then the odds are 202 | distributed equally between all the possible responses. So if there's only one 203 | response provided, it will always be sent. 204 | 205 | Note that the above yaml is equivalent to this one: 206 | 207 | ```yaml 208 | endpoints: 209 | - path: /failure/maybe 210 | get: 211 | responses: 212 | - code: 200 213 | ratio: 99 214 | - code: 500 215 | ratio: 1 216 | ``` 217 | 218 | ### Headers 219 | 220 | An additional field `headers` can be added to a specific method. 221 | This header will then be considered as mandatory and, if absent from the request, 222 | the endpoint will return a `400 Bad Request` with a specific JSON output. 223 | 224 | Note that this check will happen before picking up the response, so it will 225 | always return this error before returning any other response. 226 | 227 | Headers in the responses can be defined directly in the response like so: 228 | 229 | ```yaml 230 | endpoints: 231 | - path: /failure/maybe 232 | get: 233 | responses: 234 | - code: 200 235 | headers: 236 | - name: Content-Type 237 | value: application/json; charset=utf-8 238 | - code: 500 239 | ratio: 1 240 | ``` 241 | 242 | To make things simpler regarding headers, a specific key `preset` can be used. 243 | The `json` preset will set the `Content-Type` header with the correct value. So 244 | the example above can be rewritten like so: 245 | 246 | ```yaml 247 | endpoints: 248 | - path: /failure/maybe 249 | get: 250 | responses: 251 | - code: 200 252 | preset: json 253 | - code: 500 254 | ratio: 1 255 | ``` 256 | 257 | ### Empty method endpoint 258 | 259 | If a method is defined within an endpoint but doesn't have responses or anything 260 | attached, then a `200` will be sent back. This can be achieved like so: 261 | 262 | ```yaml 263 | - path: /maybe 264 | post: {} 265 | patch: {} 266 | get: 267 | responses: 268 | - code: 200 269 | preset: json 270 | - code: 500 271 | ratio: 1 272 | ``` 273 | 274 | When starting up, platypus will be able to generate two simple endpoints for the 275 | POST and PATCH methods. Both will simply return a `200` and return. 276 | 277 | ## Attributions 278 | 279 | Platypus drawing by 280 | [rawpixel](https://www.rawpixel.com/image/562293/duck-billed-platypus-shade-drawing) 281 | under the [CC0 license](https://creativecommons.org/publicdomain/zero/1.0/) 282 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= 2 | github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= 3 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 4 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 5 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 6 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 12 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 13 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 14 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 15 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 16 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 17 | github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= 18 | github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= 19 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 20 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 21 | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= 22 | github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= 23 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 24 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 25 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 26 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 27 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 28 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 29 | github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 30 | github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 31 | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 32 | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 33 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 34 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 35 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 36 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 37 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 38 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 39 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 40 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 41 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 42 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 43 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 44 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 45 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 46 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 47 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 48 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 49 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 50 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 51 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 52 | github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= 53 | github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 54 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 55 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 56 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 59 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 60 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 61 | github.com/onrik/logrus v0.11.0 h1:pu+BCaWL36t0yQaj/2UHK2erf88dwssAKOT51mxPUVs= 62 | github.com/onrik/logrus v0.11.0/go.mod h1:fO2vlZwIdti6PidD3gV5YKt9Lq5ptpnP293RAe1ITwk= 63 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 64 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 65 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 66 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 67 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= 70 | github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 71 | github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= 72 | github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 73 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 74 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 75 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 76 | github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= 77 | github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= 78 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 79 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 80 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= 81 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= 82 | github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= 83 | github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= 84 | github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= 85 | github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= 86 | github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= 87 | github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 88 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 89 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 90 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 91 | github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= 92 | github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= 93 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 94 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 95 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 96 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 97 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 98 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 99 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 100 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 101 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 102 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 103 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 104 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 105 | github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= 106 | github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 107 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 108 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 109 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 110 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 111 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 112 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 113 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 114 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 115 | go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= 116 | go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= 117 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 118 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 119 | golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= 120 | golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 121 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 122 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 123 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 124 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 125 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 126 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 128 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 129 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 130 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 131 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 132 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 133 | google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 134 | google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 137 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 138 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 139 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 140 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 141 | --------------------------------------------------------------------------------