diff --git a/CHANGELOG.md b/CHANGELOG.md index fabb4a1..993a9ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,10 @@ This project tries to follow [SemVer 2.0.0](https://semver.org/). much quicker as it can skip applying migrations that are already applied. (#144) +- Added automatic JSON indentation in HTTP responses based on the user agent, if + they are a desktop, mobile, or tablet device, or specifically cURL. Can be + disabled by the new query parameter `?pretty=false`. (#158) + - Changed query parameter `?status` and `?statusId` in `GET /api/build` to support multiple values, where it will respond with builds matching any of the supplied statuses. (#150) @@ -59,19 +63,20 @@ This project tries to follow [SemVer 2.0.0](https://semver.org/). the Sqlite database driver. The HTTP response model still uses the field name `"token"`. (#144) -- Added numerous dependencies: +- Added dependencies: - `github.com/alta/protopatch` v0.5.0 (#147) - - `github.com/go-gormigrate/gormigrate/v2` v2.0.0. (#144) + - `github.com/go-gormigrate/gormigrate/v2` v2.0.0 (#144) + - `github.com/mileusna/useragent` v1.0.2 (#158) - `github.com/soheilhy/cmux` v0.1.5 (#147) - `google.golang.org/grpc` v1.44.0 (#147) - `google.golang.org/protobuf` v1.27.1 (#147) - Changed version of numerous dependencies: - - `github.com/gin-gonic/gin` from v1.7.4 to v1.7.7. (#151) - - `github.com/swaggo/gin-swagger` from v1.3.1 to v1.4.1. (#151) - - `github.com/swaggo/swag` from v1.7.1 to v1.8.0. (#151) + - `github.com/gin-gonic/gin` from v1.7.4 to v1.7.7 (#151) + - `github.com/swaggo/gin-swagger` from v1.3.1 to v1.4.1 (#151) + - `github.com/swaggo/swag` from v1.7.1 to v1.8.0 (#151) - `gorm.io/driver/postgres` from v1.1.1 to v1.2.3 (#144) - `gorm.io/driver/sqlite` from v1.1.5 to v1.2.6 (#144) - `gorm.io/gorm` from v1.21.15 to v1.22.5 (#144) diff --git a/artifact.go b/artifact.go index 6ceb44e..d6f0499 100644 --- a/artifact.go +++ b/artifact.go @@ -47,6 +47,7 @@ var defaultGetArtifactsOrderBy = orderby.Column{Name: database.ArtifactColumns.A // @description while the matching filters are meant for searches by humans where it tries to find soft matches and is therefore inaccurate by nature. // @description Added in v5.0.0. // @tags artifact +// @produce json // @param buildId path uint true "Build ID" minimum(0) // @param limit query int false "Number of results to return. No limiting is applied if empty (`?limit=`) or non-positive (`?limit=0`). Required if `offset` is used." default(100) // @param offset query int false "Skipped results, where 0 means from the start." minimum(0) default(0) @@ -56,6 +57,7 @@ var defaultGetArtifactsOrderBy = orderby.Column{Name: database.ArtifactColumns.A // @param nameMatch query string false "Filter by matching artifact name. Cannot be used with `name`." // @param fileNameMatch query string false "Filter by matching artifact file name. Cannot be used with `fileName`." // @param match query string false "Filter by matching on any supported fields." +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.PaginatedArtifacts // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -120,7 +122,7 @@ func (m artifactModule) getBuildArtifactListHandler(c *gin.Context) { return } - c.JSON(http.StatusOK, response.PaginatedArtifacts{ + renderJSON(c, http.StatusOK, response.PaginatedArtifacts{ List: modelconv.DBArtifactsToResponses(dbArtifacts), TotalCount: totalCount, }) @@ -131,6 +133,7 @@ func (m artifactModule) getBuildArtifactListHandler(c *gin.Context) { // @summary Get build artifact // @description Added in v0.7.1. // @tags artifact +// @produce multipart/form-data // @param buildId path uint true "Build ID" minimum(0) // @param artifactId path uint true "Artifact ID" minimum(0) // @success 200 {file} string "OK" @@ -193,6 +196,7 @@ func (m artifactModule) getBuildArtifactHandler(c *gin.Context) { // @failure 502 {object} problem.Response "Database is unreachable" // @router /build/{buildId}/artifact [post] func (m artifactModule) createBuildArtifactHandler(c *gin.Context) { + log.Debug().Message("Start of createBuildArtifactHandler") buildID, ok := ginutil.ParseParamUint(c, "buildId") if !ok { return @@ -221,7 +225,9 @@ func (m artifactModule) createBuildArtifactHandler(c *gin.Context) { // @description Deprecated, /build/{buildid}/test-result/list-summary should be used instead. // @description Added in v0.7.0. // @tags artifact +// @produce json // @param buildId path uint true "Build ID" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.TestsResults // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -264,7 +270,7 @@ func (m artifactModule) getBuildTestResultListHandler(c *gin.Context) { resResults.Status = response.TestStatusFailed } - c.JSON(http.StatusOK, resResults) + renderJSON(c, http.StatusOK, resResults) } func createArtifacts(c *gin.Context, db *gorm.DB, files []ctxparser.File, buildID uint) ([]database.Artifact, bool) { diff --git a/branch.go b/branch.go index 1a1b60e..b8d907f 100644 --- a/branch.go +++ b/branch.go @@ -32,7 +32,9 @@ func (m branchModule) Register(g *gin.RouterGroup) { // @summary Get list of branches. // @description Added in v5.0.0. // @tags branch +// @produce json // @param projectId path uint true "project ID" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.PaginatedBranches "Branches" // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -58,7 +60,7 @@ func (m branchModule) getProjectBranchListHandler(c *gin.Context) { return } dbDefaultBranch := findDefaultDBBranch(dbBranches) - c.JSON(http.StatusOK, modelconv.DBBranchListToPaginatedResponse(dbBranches, int64(len(dbBranches)), dbDefaultBranch)) + renderJSON(c, http.StatusOK, modelconv.DBBranchListToPaginatedResponse(dbBranches, int64(len(dbBranches)), dbDefaultBranch)) } // createProjectBranchHandler godoc @@ -72,6 +74,7 @@ func (m branchModule) getProjectBranchListHandler(c *gin.Context) { // @produce json // @param projectId path uint true "project ID" minimum(0) // @param branch body request.Branch true "Branch object" +// @param pretty query bool false "Pretty indented JSON output" // @success 201 {object} response.Branch "Created branch" // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -121,7 +124,7 @@ func (m branchModule) createProjectBranchHandler(c *gin.Context) { projectID)) return } - c.JSON(http.StatusCreated, modelconv.DBBranchToResponse(dbBranch)) + renderJSON(c, http.StatusCreated, modelconv.DBBranchToResponse(dbBranch)) } // updateProjectBranchListHandler godoc @@ -137,6 +140,7 @@ func (m branchModule) createProjectBranchHandler(c *gin.Context) { // @produce json // @param projectId path uint true "project ID" minimum(0) // @param branches body request.BranchListUpdate true "Branch update" +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.BranchList "Updated branches" // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -164,7 +168,7 @@ func (m branchModule) updateProjectBranchListHandler(c *gin.Context) { return } resBranchList := modelconv.DBBranchListToResponse(dbBranchList.branches, dbBranchList.defaultBranch) - c.JSON(http.StatusOK, resBranchList) + renderJSON(c, http.StatusOK, resBranchList) } type databaseBranchList struct { diff --git a/build.go b/build.go index 5bb446a..bdf2f12 100644 --- a/build.go +++ b/build.go @@ -90,7 +90,9 @@ func build(buildID uint) broadcast.Broadcaster { // @summary Finds build by build ID // @description Added in v0.3.5. // @tags build +// @produce json // @param buildId path uint true "build id" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.Build // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -117,7 +119,7 @@ func (m buildModule) getBuildHandler(c *gin.Context) { } resBuild := modelconv.DBBuildToResponse(dbBuild, m.engineLookup) - c.JSON(http.StatusOK, resBuild) + renderJSON(c, http.StatusOK, resBuild) } var buildJSONToColumns = map[string]database.SafeSQLName{ @@ -141,6 +143,7 @@ var defaultGetBuildsOrderBy = orderby.Column{Name: database.BuildColumns.BuildID // @description while the matching filters are meant for searches by humans where it tries to find soft matches and is therefore inaccurate by nature. // @description Added in v5.0.0. // @tags build +// @produce json // @param limit query int false "Number of results to return. No limiting is applied if empty (`?limit=`) or non-positive (`?limit=0`). Required if `offset` is used." default(100) // @param offset query int false "Skipped results, where 0 means from the start." minimum(0) default(0) // @param orderby query []string false "Sorting orders. Takes the property name followed by either 'asc' or 'desc'. Can be specified multiple times for more granular sorting. Defaults to `?orderby=buildId desc`" @@ -160,6 +163,7 @@ var defaultGetBuildsOrderBy = orderby.Column{Name: database.BuildColumns.BuildID // @param gitBranchMatch query string false "Filter by matching build Git branch. Cannot be used with `gitBranch`." // @param stageMatch query string false "Filter by matching build stage. Cannot be used with `stage`." // @param match query string false "Filter by matching on any supported fields." +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.PaginatedBuilds // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -272,7 +276,7 @@ func (m buildModule) getBuildListHandler(c *gin.Context) { return } - c.JSON(http.StatusOK, response.PaginatedBuilds{ + renderJSON(c, http.StatusOK, response.PaginatedBuilds{ List: modelconv.DBBuildsToResponses(dbBuilds, m.engineLookup), TotalCount: totalCount, }) @@ -294,7 +298,9 @@ func parseBuildStatusOrWriteError(c *gin.Context, str, paramName string) (databa // @summary Finds logs for build with selected build ID // @description Added in v0.3.8. // @tags build +// @produce json // @param buildId path uint true "build id" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} []response.Log "logs from selected build" // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -324,7 +330,7 @@ func (m buildModule) getBuildLogListHandler(c *gin.Context) { } } - c.JSON(http.StatusOK, resLogs) + renderJSON(c, http.StatusOK, resLogs) } // streamBuildLogHandler godoc @@ -332,6 +338,7 @@ func (m buildModule) getBuildLogListHandler(c *gin.Context) { // @summary Opens stream listener // @description Added in v0.3.8. // @tags build +// @produce json-stream // @param buildId path uint true "build id" minimum(0) // @success 200 "Open stream" // @failure 400 {object} problem.Response "Bad request" @@ -363,6 +370,7 @@ func (m buildModule) streamBuildLogHandler(c *gin.Context) { // @summary Post a log to selected build // @description Added in v0.1.0. // @tags build +// @accept json // @param buildId path uint true "build id" minimum(0) // @param data body request.LogOrStatusUpdate true "data" // @success 201 "Created" @@ -502,6 +510,7 @@ func createLogBatchSqliteQuery(db *gorm.DB, dbLogs []database.Log) *gorm.DB { // @summary Update a build's status. // @description Added in v5.0.0. // @tags build +// @accept json // @param buildId path uint true "Build ID" minimum(0) // @param data body request.BuildStatusUpdate true "Status update" // @success 204 "Updated" @@ -626,11 +635,13 @@ func (m buildModule) getLogs(buildID uint) ([]database.Log, error) { // @description Added in v0.2.4. // @tags project // @accept json +// @produce json // @param projectId path uint true "project ID" minimum(0) // @param stage path string true "name of stage to run, or specify ALL to run everything" // @param branch query string false "branch name, uses default branch if omitted" // @param environment query string false "environment name" // @param inputs body string _ "user inputs" example(foo:bar) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.BuildReferenceWrapper "Build scheduled" // @failure 400 {object} problem.Response "Bad request, such as invalid body JSON" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -655,12 +666,14 @@ func (m buildModule) oldStartProjectBuildHandler(c *gin.Context) { // @description Added in v5.0.0. // @tags build // @accept json +// @produce json // @param projectId path uint true "Project ID" minimum(0) // @param stage query string false "Name of stage to run, or specify `ALL` to run all stages." default(ALL) // @param branch query string false "Branch name. Uses project's default branch if omitted" // @param environment query string false "Environment name filter. If left empty it will run all stages without any environment filters." // @param engine query string false "Execution engine ID" // @param inputs body request.BuildInputs _ "Input variable values. Map of variable names (as defined in the project's `.wharf-ci.yml` file) as keys paired with their string, boolean, or numeric value." +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.BuildReferenceWrapper "Build scheduled" // @failure 400 {object} problem.Response "Bad request, such as invalid body JSON" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -822,6 +835,7 @@ func (m buildModule) startBuildHandler(c *gin.Context, projectID uint, stageName } } + renderJSON(c, http.StatusOK, modelconv.DBBuildToResponseBuildReferenceWrapper(dbBuild)) c.JSON(http.StatusOK, modelconv.DBBuildToResponseBuildReferenceWrapper(dbBuild)) } diff --git a/engine.go b/engine.go index deff352..c5b21d5 100644 --- a/engine.go +++ b/engine.go @@ -20,6 +20,8 @@ func (m engineModule) Register(r *gin.RouterGroup) { // @summary Get list of engines. // @description Added in v5.1.0. // @tags engine +// @produce json +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.EngineList "Engines" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" // @router /engine [get] @@ -36,7 +38,7 @@ func (m engineModule) getEngineList(c *gin.Context) { } engines := getEnginesFromConfig(conf) res.List = convCIEnginesToResponses(engines) - c.JSON(200, res) + renderJSON(c, 200, res) } func getEnginesFromConfig(ciConf CIConfig) []CIEngineConfig { diff --git a/go.mod b/go.mod index 4b4b09b..d117ad6 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/go-gormigrate/gormigrate/v2 v2.0.0 github.com/golang-jwt/jwt/v4 v4.1.0 github.com/iver-wharf/wharf-core v1.3.0 + github.com/mileusna/useragent v1.0.2 github.com/stretchr/testify v1.7.0 github.com/swaggo/gin-swagger v1.4.1 github.com/swaggo/swag v1.8.0 diff --git a/go.sum b/go.sum index 078cf47..ef163fe 100644 --- a/go.sum +++ b/go.sum @@ -413,6 +413,8 @@ github.com/mattn/go-sqlite3 v1.14.11 h1:gt+cp9c0XGqe9S/wAHTL3n/7MqY+siPWgWJgqdsF github.com/mattn/go-sqlite3 v1.14.11/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mileusna/useragent v1.0.2 h1:DgVKtiPnjxlb73z9bCwgdUvU2nQNQ97uhgfO8l9uz/w= +github.com/mileusna/useragent v1.0.2/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= diff --git a/ping.go b/ping.go index 75bcc6e..0fbc2d9 100644 --- a/ping.go +++ b/ping.go @@ -28,10 +28,11 @@ func (m healthModule) DeprecatedRegister(e *gin.Engine) { // @description Added in v4.2.0. // @tags health // @produce json +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.Ping // @router /ping [get] func (m healthModule) pingHandler(c *gin.Context) { - c.JSON(200, response.Ping{Message: "pong"}) + renderJSON(c, 200, response.Ping{Message: "pong"}) } // healthHandler godoc @@ -41,8 +42,9 @@ func (m healthModule) pingHandler(c *gin.Context) { // @description Added in v0.7.1. // @tags health // @produce json +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.HealthStatus // @router /health [get] func (m healthModule) healthHandler(c *gin.Context) { - c.JSON(200, response.HealthStatus{Message: "API is healthy.", IsHealthy: true}) + renderJSON(c, 200, response.HealthStatus{Message: "API is healthy.", IsHealthy: true}) } diff --git a/project.go b/project.go index 90171d0..7f0afe9 100644 --- a/project.go +++ b/project.go @@ -63,6 +63,7 @@ var defaultGetProjectsOrderBy = orderby.Column{Name: database.ProjectColumns.Pro // @description while the matching filters are meant for searches by humans where it tries to find soft matches and is therefore inaccurate by nature. // @description Added in v5.0.0. // @tags project +// @produce json // @param orderby query []string false "Sorting orders. Takes the property name followed by either 'asc' or 'desc'. Can be specified multiple times for more granular sorting. Defaults to `?orderby=projectId desc`" // @param limit query int false "Number of results to return. No limiting is applied if empty (`?limit=`) or non-positive (`?limit=0`). Required if `offset` is used." default(100) // @param offset query int false "Skipped results, where 0 means from the start." minimum(0) default(0) @@ -77,6 +78,7 @@ var defaultGetProjectsOrderBy = orderby.Column{Name: database.ProjectColumns.Pro // @param descriptionMatch query string false "Filter by matching description. Cannot be used with `description`." // @param gitUrlMatch query string false "Filter by matching Git URL. Cannot be used with `gitUrl`." // @param match query string false "Filter by matching on any supported fields." +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.PaginatedProjects // @failure 502 {object} problem.Response "Database is unreachable" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -143,7 +145,7 @@ func (m projectModule) getProjectListHandler(c *gin.Context) { return } - c.JSON(http.StatusOK, response.PaginatedProjects{ + renderJSON(c, http.StatusOK, response.PaginatedProjects{ List: modelconv.DBProjectsToResponses(dbProjects), TotalCount: totalCount, }) @@ -154,7 +156,9 @@ func (m projectModule) getProjectListHandler(c *gin.Context) { // @summary Returns project with selected project ID // @description Added in v0.1.8. // @tags project +// @produce json // @param projectId path uint true "project ID" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.Project // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -171,7 +175,7 @@ func (m projectModule) getProjectHandler(c *gin.Context) { return } resProject := modelconv.DBProjectToResponse(dbProject) - c.JSON(http.StatusOK, resProject) + renderJSON(c, http.StatusOK, resProject) } // createProjectHandler godoc @@ -183,6 +187,7 @@ func (m projectModule) getProjectHandler(c *gin.Context) { // @accept json // @produce json // @param project body request.Project true "Project to create" +// @param pretty query bool false "Pretty indented JSON output" // @success 201 {object} response.Project // @failure 400 {object} problem.Response "Bad request" // @failure 404 {object} problem.Response "Project to update is not found" @@ -206,7 +211,7 @@ func (m projectModule) createProjectHandler(c *gin.Context) { } resProject := modelconv.DBProjectToResponse(dbProject) - c.JSON(http.StatusCreated, resProject) + renderJSON(c, http.StatusCreated, resProject) } // deleteProjectHandler godoc @@ -248,6 +253,7 @@ func (m projectModule) deleteProjectHandler(c *gin.Context) { // @produce json // @param projectId path uint true "project ID" minimum(0) // @param project body request.ProjectUpdate _ "New project values" +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.Project // @failure 400 {object} problem.Response "Bad request, such as invalid body JSON" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -287,7 +293,7 @@ func (m projectModule) updateProjectHandler(c *gin.Context) { } resProject := modelconv.DBProjectToResponse(dbProject) - c.JSON(http.StatusOK, resProject) + renderJSON(c, http.StatusOK, resProject) } // getProjectOverridesHandler godoc @@ -301,6 +307,7 @@ func (m projectModule) updateProjectHandler(c *gin.Context) { // @tags project // @produce json // @param projectId path uint true "project ID" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.ProjectOverrides // @failure 400 {object} problem.Response "Bad request, such as invalid body JSON" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -330,7 +337,7 @@ func (m projectModule) getProjectOverridesHandler(c *gin.Context) { } resProject := modelconv.DBProjectOverridesToResponse(dbProjectOverrides) - c.JSON(http.StatusOK, resProject) + renderJSON(c, http.StatusOK, resProject) } // updateProjectOverridesHandler godoc @@ -346,6 +353,7 @@ func (m projectModule) getProjectOverridesHandler(c *gin.Context) { // @produce json // @param projectId path uint true "project ID" minimum(0) // @param overrides body request.ProjectOverridesUpdate _ "New project overrides" +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.ProjectOverrides // @failure 400 {object} problem.Response "Bad request, such as invalid body JSON" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -389,7 +397,7 @@ func (m projectModule) updateProjectOverridesHandler(c *gin.Context) { } resProject := modelconv.DBProjectOverridesToResponse(dbProjectOverrides) - c.JSON(http.StatusOK, resProject) + renderJSON(c, http.StatusOK, resProject) } // deleteProjectOverridesHandler godoc diff --git a/provider.go b/provider.go index 357a034..a65dcbd 100644 --- a/provider.go +++ b/provider.go @@ -51,6 +51,7 @@ var defaultGetProvidersOrderBy = orderby.Column{Name: database.ProviderColumns.P // @description while the matching filters are meant for searches by humans where it tries to find soft matches and is therefore inaccurate by nature. // @description Added in v5.0.0. // @tags provider +// @produce json // @param limit query int false "Number of results to return. No limiting is applied if empty (`?limit=`) or non-positive (`?limit=0`). Required if `offset` is used." default(100) // @param offset query int false "Skipped results, where 0 means from the start." minimum(0) default(0) // @param orderby query []string false "Sorting orders. Takes the property name followed by either 'asc' or 'desc'. Can be specified multiple times for more granular sorting. Defaults to `?orderby=providerId desc`" @@ -59,6 +60,7 @@ var defaultGetProvidersOrderBy = orderby.Column{Name: database.ProviderColumns.P // @param nameMatch query string false "Filter by matching provider name. Cannot be used with `name`." // @param urlMatch query string false "Filter by matching provider URL. Cannot be used with `url`." // @param match query string false "Filter by matching on any supported fields." +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.PaginatedProviders // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt provider" @@ -113,7 +115,7 @@ func (m providerModule) getProviderListHandler(c *gin.Context) { return } - c.JSON(http.StatusOK, response.PaginatedProviders{ + renderJSON(c, http.StatusOK, response.PaginatedProviders{ List: modelconv.DBProvidersToResponses(dbProviders), TotalCount: totalCount, }) @@ -124,7 +126,9 @@ func (m providerModule) getProviderListHandler(c *gin.Context) { // @summary Returns provider with selected provider ID // @description Added in v0.3.9. // @tags provider +// @produce json // @param providerId path uint true "Provider ID" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.Provider // @failure 400 {object} problem.Response "Bad request" // @failure 404 {object} problem.Response "Provider not found" @@ -143,7 +147,7 @@ func (m providerModule) getProviderHandler(c *gin.Context) { } resProvider := modelconv.DBProviderToResponse(dbProvider) - c.JSON(http.StatusOK, resProvider) + renderJSON(c, http.StatusOK, resProvider) } // createProviderHandler godoc @@ -156,6 +160,7 @@ func (m providerModule) getProviderHandler(c *gin.Context) { // @accept json // @produce json // @param provider body request.Provider _ "Provider to create" +// @param pretty query bool false "Pretty indented JSON output" // @success 201 {object} response.Provider // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -189,7 +194,7 @@ func (m providerModule) createProviderHandler(c *gin.Context) { } resProvider := modelconv.DBProviderToResponse(dbProvider) - c.JSON(http.StatusCreated, resProvider) + renderJSON(c, http.StatusCreated, resProvider) } // updateProviderHandler godoc @@ -202,6 +207,7 @@ func (m providerModule) createProviderHandler(c *gin.Context) { // @produce json // @param providerId path uint _ "ID of provider to update" minimum(0) // @param provider body request.ProviderUpdate _ "New provider values" +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.Provider // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -247,7 +253,7 @@ func (m providerModule) updateProviderHandler(c *gin.Context) { } resProvider := modelconv.DBProviderToResponse(dbProvider) - c.JSON(http.StatusOK, resProvider) + renderJSON(c, http.StatusOK, resProvider) } func fetchProviderByID(c *gin.Context, db *gorm.DB, providerID uint, whenMsg string) (database.Provider, bool) { diff --git a/test_result.go b/test_result.go index 9926f65..16b1930 100644 --- a/test_result.go +++ b/test_result.go @@ -41,8 +41,10 @@ func (m buildTestResultModule) Register(r gin.IRouter) { // @description Added in v5.0.0. // @tags test-result // @accept multipart/form-data +// @produce json // @param buildId path uint true "Build ID" minimum(0) // @param files formData file true "Test result file" +// @param pretty query bool false "Pretty indented JSON output" // @success 201 {object} []response.ArtifactMetadata "Added new test result data and created summaries" // @failure 400 {object} problem.Response "Bad request" // @failure 502 {object} problem.Response "Database unreachable or bad gateway" @@ -120,7 +122,7 @@ func (m buildTestResultModule) createBuildTestResultHandler(c *gin.Context) { buildID)) } - c.JSON(http.StatusOK, resArtifactMetadataList) + renderJSON(c, http.StatusOK, resArtifactMetadataList) } // getBuildAllTestResultDetailListHandler godoc @@ -128,7 +130,9 @@ func (m buildTestResultModule) createBuildTestResultHandler(c *gin.Context) { // @summary Get all test result details for specified build // @description Added in v5.0.0. // @tags test-result +// @produce json // @param buildId path uint true "Build ID" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.PaginatedTestResultDetails // @failure 400 {object} problem.Response "Bad request" // @failure 502 {object} problem.Response "Database is unreachable" @@ -153,7 +157,7 @@ func (m buildTestResultModule) getBuildAllTestResultDetailListHandler(c *gin.Con } resDetails := modelconv.DBTestResultDetailsToResponses(dbDetails) - c.JSON(http.StatusOK, response.PaginatedTestResultDetails{ + renderJSON(c, http.StatusOK, response.PaginatedTestResultDetails{ List: resDetails, TotalCount: int64(len(resDetails)), }) @@ -164,7 +168,9 @@ func (m buildTestResultModule) getBuildAllTestResultDetailListHandler(c *gin.Con // @summary Get all test result summaries for specified build // @description Added in v5.0.0. // @tags test-result +// @produce json // @param buildId path uint true "Build ID" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.PaginatedTestResultSummaries // @failure 400 {object} problem.Response "Bad Request" // @failure 502 {object} problem.Response "Database is unreachable" @@ -193,7 +199,7 @@ func (m buildTestResultModule) getBuildAllTestResultSummaryListHandler(c *gin.Co resSummaries[i] = modelconv.DBTestResultSummaryToResponse(dbSummary) } - c.JSON(http.StatusOK, response.PaginatedTestResultSummaries{ + renderJSON(c, http.StatusOK, response.PaginatedTestResultSummaries{ List: resSummaries, TotalCount: int64(len(resSummaries)), }) @@ -204,8 +210,10 @@ func (m buildTestResultModule) getBuildAllTestResultSummaryListHandler(c *gin.Co // @summary Get test result summary for specified test // @description Added in v5.0.0. // @tags test-result +// @produce json // @param buildId path uint true "Build ID" minimum(0) // @param artifactId path uint true "Artifact ID" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.TestResultSummary // @failure 400 {object} problem.Response "Bad Request" // @failure 502 {object} problem.Response "Database is unreachable" @@ -235,7 +243,7 @@ func (m buildTestResultModule) getBuildTestResultSummaryHandler(c *gin.Context) } resSummary := modelconv.DBTestResultSummaryToResponse(dbSummary) - c.JSON(http.StatusOK, resSummary) + renderJSON(c, http.StatusOK, resSummary) } // getBuildTestResultDetailListHandler godoc @@ -243,8 +251,10 @@ func (m buildTestResultModule) getBuildTestResultSummaryHandler(c *gin.Context) // @summary Get all test result details for specified test // @description Added in v5.0.0. // @tags test-result +// @produce json // @param buildId path uint true "Build ID" minimum(0) // @param artifactId path uint true "Artifact ID" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.PaginatedTestResultDetails // @failure 400 {object} problem.Response "Bad Request" // @failure 502 {object} problem.Response "Database is unreachable" @@ -274,7 +284,7 @@ func (m buildTestResultModule) getBuildTestResultDetailListHandler(c *gin.Contex } resDetails := modelconv.DBTestResultDetailsToResponses(dbDetails) - c.JSON(http.StatusOK, response.PaginatedTestResultDetails{ + renderJSON(c, http.StatusOK, response.PaginatedTestResultDetails{ List: resDetails, TotalCount: int64(len(dbDetails)), }) @@ -285,7 +295,9 @@ func (m buildTestResultModule) getBuildTestResultDetailListHandler(c *gin.Contex // @summary Get test result list summary of all tests for specified build // @description Added in v5.0.0. // @tags test-result +// @produce json // @param buildId path uint true "Build ID" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.TestResultListSummary // @failure 400 {object} problem.Response "Bad Request" // @failure 502 {object} problem.Response "Database is unreachable" @@ -324,7 +336,7 @@ func (m buildTestResultModule) getBuildAllTestResultListSummaryHandler(c *gin.Co Failed: dbListSummary.Failed, } - c.JSON(http.StatusOK, resListSummary) + renderJSON(c, http.StatusOK, resListSummary) } type xmlInnerString struct { diff --git a/token.go b/token.go index 5c181c6..c7a1833 100644 --- a/token.go +++ b/token.go @@ -51,11 +51,13 @@ var defaultGetTokensOrderBy = orderby.Column{Name: database.TokenColumns.TokenID // @description while the matching filters are meant for searches by humans where it tries to find soft matches and is therefore inaccurate by nature. // @description Added in v5.0.0. // @tags token +// @produce json // @param limit query int false "Number of results to return. No limiting is applied if empty (`?limit=`) or non-positive (`?limit=0`). Required if `offset` is used." default(100) // @param offset query int false "Skipped results, where 0 means from the start." minimum(0) default(0) // @param orderby query []string false "Sorting orders. Takes the property name followed by either 'asc' or 'desc'. Can be specified multiple times for more granular sorting. Defaults to `?orderby=tokenId desc`" // @param userName query string false "Filter by verbatim token user name." // @param userNameMatch query string false "Filter by matching token user name. Cannot be used with `userName`." +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.PaginatedTokens // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -98,7 +100,7 @@ func (m tokenModule) getTokenListHandler(c *gin.Context) { return } - c.JSON(http.StatusOK, response.PaginatedTokens{ + renderJSON(c, http.StatusOK, response.PaginatedTokens{ List: modelconv.DBTokensToResponses(dbTokens), TotalCount: totalCount, }) @@ -109,7 +111,9 @@ func (m tokenModule) getTokenListHandler(c *gin.Context) { // @summary Returns token with selected token ID // @description Added in v0.2.2. // @tags token +// @produce json // @param tokenId path uint true "Token ID" minimum(0) +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.Token // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -127,7 +131,7 @@ func (m tokenModule) getTokenHandler(c *gin.Context) { } resToken := modelconv.DBTokenToResponse(dbToken) - c.JSON(http.StatusOK, resToken) + renderJSON(c, http.StatusOK, resToken) } // createTokenHandler godoc @@ -139,6 +143,7 @@ func (m tokenModule) getTokenHandler(c *gin.Context) { // @accept json // @produce json // @param token body request.Token _ "Token to create" +// @param pretty query bool false "Pretty indented JSON output" // @success 201 {object} response.Token // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -178,7 +183,7 @@ func (m tokenModule) createTokenHandler(c *gin.Context) { } resToken := modelconv.DBTokenToResponse(dbToken) - c.JSON(http.StatusCreated, resToken) + renderJSON(c, http.StatusCreated, resToken) } // updateTokenHandler godoc @@ -191,6 +196,7 @@ func (m tokenModule) createTokenHandler(c *gin.Context) { // @produce json // @param tokenId path uint true "ID of token to update" minimum(0) // @param token body request.TokenUpdate _ "New token values" +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} response.Token // @failure 400 {object} problem.Response "Bad request" // @failure 401 {object} problem.Response "Unauthorized or missing jwt token" @@ -222,7 +228,7 @@ func (m tokenModule) updateTokenHandler(c *gin.Context) { } resToken := modelconv.DBTokenToResponse(dbToken) - c.JSON(http.StatusOK, resToken) + renderJSON(c, http.StatusOK, resToken) } func fetchTokenByID(c *gin.Context, db *gorm.DB, tokenID uint, whenMsg string) (database.Token, bool) { diff --git a/utils_gin.go b/utils_gin.go index 3c4035c..edd3c25 100644 --- a/utils_gin.go +++ b/utils_gin.go @@ -8,6 +8,7 @@ import ( "github.com/iver-wharf/wharf-api/v5/pkg/model/database" "github.com/iver-wharf/wharf-api/v5/pkg/orderby" "github.com/iver-wharf/wharf-core/pkg/ginutil" + ua "github.com/mileusna/useragent" ) type commonGetQueryParams struct { @@ -42,3 +43,23 @@ func parseCommonOrderBySlice(c *gin.Context, orders []string, fieldToColumnNames } return orderBySlice, true } + +func renderJSON(c *gin.Context, code int, response interface{}) { + indent := false + prettyQuery, ok := c.GetQuery("pretty") + if ok { + if prettyQuery == "" || strings.EqualFold(prettyQuery, "true") { + indent = true + } + } else { + agent := ua.Parse(c.Request.UserAgent()) + if agent.Name == "curl" || agent.Desktop || agent.Mobile || agent.Tablet { + indent = true + } + } + if indent { + c.IndentedJSON(code, response) + } else { + c.JSON(code, response) + } +} diff --git a/version.go b/version.go index fb180d2..f06af4b 100644 --- a/version.go +++ b/version.go @@ -26,8 +26,10 @@ func loadEmbeddedVersionFile() error { // @summary Returns the version of this API // @description Added in v4.0.0. // @tags meta +// @produce json +// @param pretty query bool false "Pretty indented JSON output" // @success 200 {object} app.Version // @router /version [get] func getVersionHandler(c *gin.Context) { - c.JSON(http.StatusOK, AppVersion) + renderJSON(c, http.StatusOK, AppVersion) }