提交 496db42b 编写于 作者: H hongming

refine api docs

Signed-off-by: Nhongming <talonwan@yunify.com>
上级 44799ae6
此差异已折叠。
......@@ -21,8 +21,8 @@ require (
github.com/elastic/go-elasticsearch/v6 v6.8.2
github.com/elastic/go-elasticsearch/v7 v7.3.0
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 // indirect
github.com/emicklei/go-restful v2.11.1+incompatible
github.com/emicklei/go-restful-openapi v1.0.0
github.com/emicklei/go-restful v2.14.3+incompatible
github.com/emicklei/go-restful-openapi v1.4.1
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fatih/structs v1.1.0
github.com/go-ldap/ldap v3.0.3+incompatible
......@@ -94,6 +94,7 @@ require (
k8s.io/klog v1.0.0
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a
k8s.io/kubectl v0.17.3
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f
openpitrix.io/openpitrix v0.4.9-0.20200611125425-ae07f141e797
sigs.k8s.io/application v1.0.0
sigs.k8s.io/controller-runtime v0.5.0
......@@ -187,8 +188,8 @@ replace (
github.com/elastic/go-elasticsearch/v7 => github.com/elastic/go-elasticsearch/v7 v7.3.0
github.com/elazarl/goproxy => github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2
github.com/elazarl/goproxy/ext => github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2
github.com/emicklei/go-restful => github.com/emicklei/go-restful v2.9.5+incompatible
github.com/emicklei/go-restful-openapi => github.com/emicklei/go-restful-openapi v1.0.0
github.com/emicklei/go-restful => github.com/emicklei/go-restful v2.14.3+incompatible
github.com/emicklei/go-restful-openapi => github.com/emicklei/go-restful-openapi v1.4.1
github.com/emirpasic/gods => github.com/emirpasic/gods v1.12.0
github.com/erikstmartin/go-testdb => github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5
github.com/evanphx/json-patch => github.com/evanphx/json-patch v4.5.0+incompatible
......
......@@ -124,10 +124,10 @@ github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 h1:aZtFdDNWY/yH86J
github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful-openapi v1.0.0 h1:ZFk3RuCl8ZmG1yUAF/mSbXRi5cuyA/k5+EpHayuuTXM=
github.com/emicklei/go-restful-openapi v1.0.0/go.mod h1:Q+bHVYfUWv1fvC4FNTsz2AVvFSsXAC7RCiWjF1Sva1A=
github.com/emicklei/go-restful v2.14.3+incompatible h1:i59XyRHAxKCVBw3vHzQlpP/+pi89wH1v1HL+RKyVgxk=
github.com/emicklei/go-restful v2.14.3+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful-openapi v1.4.1 h1:SocVTIQWnXyit4dotTrwmncBAjtRaBmfcHjo3XGcCm4=
github.com/emicklei/go-restful-openapi v1.4.1/go.mod h1:kWQ8rQMVQ6G6lePwjDveJ00KjAUr/jq6z1X8DrDP3Gc=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
......
......@@ -32,63 +32,80 @@ const (
AdminUserName = "admin"
IngressControllerPrefix = "kubesphere-router-"
WorkspaceLabelKey = "kubesphere.io/workspace"
NamespaceLabelKey = "kubesphere.io/namespace"
RuntimeLabelKey = "openpitrix.io/namespace"
DisplayNameAnnotationKey = "kubesphere.io/alias-name"
DescriptionAnnotationKey = "kubesphere.io/description"
CreatorAnnotationKey = "kubesphere.io/creator"
UsernameLabelKey = "kubesphere.io/username"
System = "system"
OpenPitrixRuntimeAnnotationKey = "openpitrix_runtime"
WorkspaceAdmin = "workspace-admin"
ClusterAdmin = "cluster-admin"
WorkspaceRegular = "workspace-regular"
WorkspaceViewer = "workspace-viewer"
WorkspacesManager = "workspaces-manager"
DevopsOwner = "owner"
DevopsReporter = "reporter"
DevOpsProjectLabelKey = "kubesphere.io/devopsproject"
KubefedManagedLabel = "kubefed.io/managed"
WorkspaceLabelKey = "kubesphere.io/workspace"
NamespaceLabelKey = "kubesphere.io/namespace"
DisplayNameAnnotationKey = "kubesphere.io/alias-name"
DescriptionAnnotationKey = "kubesphere.io/description"
CreatorAnnotationKey = "kubesphere.io/creator"
UsernameLabelKey = "kubesphere.io/username"
DevOpsProjectLabelKey = "kubesphere.io/devopsproject"
KubefedManagedLabel = "kubefed.io/managed"
UserNameHeader = "X-Token-Username"
TenantResourcesTag = "Tenant Resources"
IdentityManagementTag = "Identity Management"
AccessManagementTag = "Access Management"
NamespaceResourcesTag = "Namespace Resources"
ClusterResourcesTag = "Cluster Resources"
ComponentStatusTag = "Component Status"
OpenpitrixTag = "Openpitrix Resources"
VerificationTag = "Verification"
RegistryTag = "Docker Registry"
NetworkTopologyTag = "Network Topology"
UserResourcesTag = "User Resources"
DevOpsProjectTag = "DevOps Project"
DevOpsProjectCredentialTag = "DevOps Project Credential"
DevOpsProjectMemberTag = "DevOps Project Member"
DevOpsPipelineTag = "DevOps Pipeline"
DevOpsWebhookTag = "DevOps Webhook"
DevOpsJenkinsfileTag = "DevOps Jenkinsfile"
DevOpsScmTag = "DevOps Scm"
KubeSphereMetricsTag = "KubeSphere Metrics"
ClusterMetricsTag = "Cluster Metrics"
NodeMetricsTag = "Node Metrics"
NamespaceMetricsTag = "Namespace Metrics"
PodMetricsTag = "Pod Metrics"
PVCMetricsTag = "PVC Metrics"
ContainerMetricsTag = "Container Metrics"
WorkloadMetricsTag = "Workload Metrics"
WorkspaceMetricsTag = "Workspace Metrics"
ComponentMetricsTag = "Component Metrics"
CustomMetricsTag = "Custom Metrics"
LogQueryTag = "Log Query"
TerminalTag = "Terminal"
EventsQueryTag = "Events Query"
AuditingQueryTag = "Auditing Query"
AuthenticationTag = "Authentication"
UserTag = "User"
GroupTag = "Group"
WorkspaceMemberTag = "Workspace Member"
DevOpsProjectMemberTag = "DevOps Project Member"
NamespaceMemberTag = "Namespace Member"
ClusterMemberTag = "Cluster Member"
GlobalRoleTag = "Global Role"
ClusterRoleTag = "Cluster Role"
WorkspaceRoleTag = "Workspace Role"
DevOpsProjectRoleTag = "DevOps Project Role"
NamespaceRoleTag = "Namespace Role"
OpenpitrixAppInstanceTag = "App Instance"
OpenpitrixAppTemplateTag = "App Template"
OpenpitrixCategoryTag = "Category"
OpenpitrixAttachmentTag = "Attachment"
OpenpitrixRepositoryTag = "Repository"
OpenpitrixManagementTag = "App Management"
DevOpsCredentialTag = "DevOps Credential"
DevOpsPipelineTag = "DevOps Pipeline"
DevOpsWebhookTag = "DevOps Webhook"
DevOpsJenkinsfileTag = "DevOps Jenkinsfile"
DevOpsScmTag = "DevOps Scm"
DevOpsJenkinsTag = "Jenkins"
ToolboxTag = "Toolbox"
RegistryTag = "Docker Registry"
GitTag = "Git"
TerminalTag = "Terminal"
MultiClusterTag = "Multi-cluster"
WorkspaceTag = "Workspace"
NamespaceTag = "Namespace"
DevOpsProjectTag = "DevOps Project"
UserResourceTag = "User's Resources"
NamespaceResourcesTag = "Namespace Resources"
ClusterResourcesTag = "Cluster Resources"
ComponentStatusTag = "Component Status"
NetworkTopologyTag = "Network Topology"
KubeSphereMetricsTag = "KubeSphere Metrics"
ClusterMetricsTag = "Cluster Metrics"
NodeMetricsTag = "Node Metrics"
NamespaceMetricsTag = "Namespace Metrics"
PodMetricsTag = "Pod Metrics"
PVCMetricsTag = "PVC Metrics"
ContainerMetricsTag = "Container Metrics"
WorkloadMetricsTag = "Workload Metrics"
WorkspaceMetricsTag = "Workspace Metrics"
ComponentMetricsTag = "Component Metrics"
CustomMetricsTag = "Custom Metrics"
LogQueryTag = "Log Query"
EventsQueryTag = "Events Query"
AuditingQueryTag = "Auditing Query"
)
var (
WorkSpaceRoles = []string{WorkspaceAdmin, WorkspaceRegular, WorkspaceViewer}
SystemNamespaces = []string{KubeSphereNamespace, KubeSphereLoggingNamespace, KubeSphereMonitoringNamespace, OpenPitrixNamespace, KubeSystemNamespace, IstioNamespace, KubesphereDevOpsNamespace, PorterNamespace}
)
......@@ -18,11 +18,13 @@ package v1alpha1
import (
"github.com/emicklei/go-restful"
restfulspec "github.com/emicklei/go-restful-openapi"
"k8s.io/apimachinery/pkg/runtime/schema"
k8sinformers "k8s.io/client-go/informers"
"kubesphere.io/kubesphere/pkg/api"
"kubesphere.io/kubesphere/pkg/apiserver/runtime"
"kubesphere.io/kubesphere/pkg/client/informers/externalversions"
"kubesphere.io/kubesphere/pkg/constants"
"net/http"
)
......@@ -47,13 +49,15 @@ func AddToContainer(container *restful.Container,
Doc("Return deployment yaml for cluster agent.").
Param(webservice.PathParameter("cluster", "Name of the cluster.").Required(true)).
To(h.generateAgentDeployment).
Returns(http.StatusOK, api.StatusOK, nil))
Returns(http.StatusOK, api.StatusOK, nil).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.MultiClusterTag}))
webservice.Route(webservice.POST("/clusters/validation").
Doc("").
Param(webservice.BodyParameter("cluster", "cluster specification")).
To(h.validateCluster).
Returns(http.StatusOK, api.StatusOK, nil))
Returns(http.StatusOK, api.StatusOK, nil).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.MultiClusterTag}))
container.Add(webservice)
......
......@@ -84,7 +84,7 @@ func AddPipelineToWebService(webservice *restful.WebService, devopsClient devops
webservice.Route(webservice.GET("/devops/{devops}/credentials/{credential}/usage").
To(projectPipelineHandler.GetProjectCredentialUsage).
Doc("Get the specified credential usage of the DevOps project").
Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectCredentialTag}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsCredentialTag}).
Param(webservice.PathParameter("devops", "DevOps project's ID, e.g. project-RRRRAzLBlLEm")).
Param(webservice.PathParameter("credential", "credential's ID, e.g. dockerhub-id")).
Returns(http.StatusOK, RespOK, devops.Credential{}))
......@@ -697,7 +697,9 @@ func AddJenkinsToContainer(webservice *restful.WebService, devopsClient devops.I
u.Path = strings.Replace(request.Request.URL.Path, fmt.Sprintf("/kapis/%s/%s/jenkins", GroupVersion.Group, GroupVersion.Version), "", 1)
httpProxy := proxy.NewUpgradeAwareHandler(u, http.DefaultTransport, false, false, &errorResponder{})
httpProxy.ServeHTTP(response, request.Request)
}).Returns(http.StatusOK, RespOK, nil))
}).
Returns(http.StatusOK, RespOK, nil).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsJenkinsTag}))
return nil
}
......
此差异已折叠。
......@@ -50,7 +50,7 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, tok
Reads(auth.TokenReview{}).
To(handler.TokenReview).
Returns(http.StatusOK, api.StatusOK, auth.TokenReview{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.IdentityManagementTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
// Only support implicit grant flow
// https://tools.ietf.org/html/rfc6749#section-4.2
......@@ -64,15 +64,22 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, tok
Param(ws.QueryParameter("redirect_uri", "After completing its interaction with the resource owner, "+
"the authorization server directs the resource owner's user-agent back to the client.The redirection endpoint "+
"URI MUST be an absolute URI as defined by [RFC3986] Section 4.3.").Required(false)).
To(handler.Authorize))
To(handler.Authorize).
Returns(http.StatusFound, http.StatusText(http.StatusFound), "").
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
// Resource Owner Password Credentials Grant
// https://tools.ietf.org/html/rfc6749#section-4.3
ws.Route(ws.POST("/token").
Consumes("application/x-www-form-urlencoded").
Doc("The resource owner password credentials grant type is suitable in\n" +
"cases where the resource owner has a trust relationship with the\n" +
Doc("The resource owner password credentials grant type is suitable in\n"+
"cases where the resource owner has a trust relationship with the\n"+
"client, such as the device operating system or a highly privileged application.").
To(handler.Token))
Param(ws.FormParameter("grant_type", "Value MUST be set to \"password\".").Required(true)).
Param(ws.FormParameter("username", "The resource owner username.").Required(true)).
Param(ws.FormParameter("password", "The resource owner password.").Required(true)).
To(handler.Token).
Returns(http.StatusOK, http.StatusText(http.StatusOK), &oauth.Token{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
// Authorization callback URL, where the end of the URL contains the identity provider name.
// The provider name is also used to build the callback URL.
......@@ -92,7 +99,8 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, tok
Param(ws.QueryParameter("state", "if the \"state\" parameter was present in the client authorization request."+
"The exact value received from the client.").Required(true)).
To(handler.oAuthCallBack).
Returns(http.StatusOK, api.StatusOK, oauth.Token{}))
Returns(http.StatusOK, api.StatusOK, oauth.Token{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
c.Add(ws)
......@@ -107,7 +115,7 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, tok
Doc("KubeSphere APIs support token-based authentication via the Authtoken request header. The POST Login API is used to retrieve the authentication token. After the authentication token is obtained, it must be inserted into the Authtoken header for all requests.").
Reads(auth.LoginRequest{}).
Returns(http.StatusOK, api.StatusOK, oauth.Token{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.IdentityManagementTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
c.Add(legacy)
......
此差异已折叠。
......@@ -86,7 +86,7 @@ func AddToContainer(c *restful.Container, k8sClient kubernetes.Interface, factor
To(handler.GetKubectlPod).
Doc("get user's kubectl pod").
Param(webservice.PathParameter("user", "username")).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.UserResourcesTag}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.ToolboxTag}).
Returns(http.StatusOK, api.StatusOK, models.PodInfo{}))
webservice.Route(webservice.GET("/users/{user}/kubeconfig").
......@@ -95,7 +95,7 @@ func AddToContainer(c *restful.Container, k8sClient kubernetes.Interface, factor
Doc("get users' kubeconfig").
Param(webservice.PathParameter("user", "username")).
Returns(http.StatusOK, api.StatusOK, "").
Metadata(restfulspec.KeyOpenAPITags, []string{constants.UserResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.ToolboxTag}))
webservice.Route(webservice.GET("/components").
To(handler.handleGetComponents).
......@@ -130,7 +130,7 @@ func AddToContainer(c *restful.Container, k8sClient kubernetes.Interface, factor
webservice.Route(webservice.POST("registry/verify").
To(handler.handleVerifyRegistryCredential).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.VerificationTag}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.RegistryTag}).
Doc("verify if a user has access to the docker registry").
Reads(api.RegistryCredential{}).
Returns(http.StatusOK, api.StatusOK, errors.Error{}))
......@@ -152,7 +152,7 @@ func AddToContainer(c *restful.Container, k8sClient kubernetes.Interface, factor
)
webservice.Route(webservice.POST("git/verify").
To(handler.handleVerifyGitCredential).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.VerificationTag}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.GitTag}).
Doc("Verify if the kubernetes secret has read access to the git repository").
Reads(gitmodel.AuthInfo{}).
Returns(http.StatusOK, api.StatusOK, errors.Error{}),
......
......@@ -59,21 +59,21 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s
To(handler.ListClusters).
Doc("List clusters available to users").
Returns(http.StatusOK, api.StatusOK, api.ListResult{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.UserResourceTag}))
ws.Route(ws.POST("/workspaces").
To(handler.CreateWorkspace).
Reads(tenantv1alpha2.WorkspaceTemplate{}).
Returns(http.StatusOK, api.StatusOK, tenantv1alpha2.WorkspaceTemplate{}).
Doc("Create workspace.").
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.WorkspaceTag}))
ws.Route(ws.DELETE("/workspaces/{workspace}").
To(handler.DeleteWorkspace).
Param(ws.PathParameter("workspace", "workspace name")).
Returns(http.StatusOK, api.StatusOK, errors.None).
Doc("Delete workspace.").
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.WorkspaceTag}))
ws.Route(ws.PUT("/workspaces/{workspace}").
To(handler.UpdateWorkspace).
......@@ -81,7 +81,7 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s
Reads(tenantv1alpha2.WorkspaceTemplate{}).
Returns(http.StatusOK, api.StatusOK, tenantv1alpha2.WorkspaceTemplate{}).
Doc("Update workspace.").
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.WorkspaceTag}))
ws.Route(ws.PATCH("/workspaces/{workspace}").
To(handler.PatchWorkspace).
......@@ -90,60 +90,60 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s
Reads(tenantv1alpha2.WorkspaceTemplate{}).
Returns(http.StatusOK, api.StatusOK, tenantv1alpha2.WorkspaceTemplate{}).
Doc("Update workspace.").
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.WorkspaceTag}))
ws.Route(ws.GET("/workspaces").
To(handler.ListWorkspaces).
Returns(http.StatusOK, api.StatusOK, models.PageableResponse{}).
Doc("List all workspaces that belongs to the current user").
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.WorkspaceTag}))
ws.Route(ws.GET("/workspaces/{workspace}").
To(handler.DescribeWorkspace).
Param(ws.PathParameter("workspace", "workspace name")).
Returns(http.StatusOK, api.StatusOK, tenantv1alpha2.WorkspaceTemplate{}).
Doc("Describe workspace.").
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.WorkspaceTag}))
ws.Route(ws.GET("/workspaces/{workspace}/clusters").
To(handler.ListWorkspaceClusters).
Param(ws.PathParameter("workspace", "workspace name")).
Returns(http.StatusOK, api.StatusOK, api.ListResult{}).
Doc("List clusters authorized to the specified workspace.").
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.WorkspaceTag}))
ws.Route(ws.GET("/namespaces").
To(handler.ListNamespaces).
Doc("List the namespaces for the current user").
Returns(http.StatusOK, api.StatusOK, api.ListResult{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.NamespaceTag}))
ws.Route(ws.GET("/federatednamespaces").
To(handler.ListFederatedNamespaces).
Doc("List the federated namespaces for the current user").
Returns(http.StatusOK, api.StatusOK, api.ListResult{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.NamespaceTag}))
ws.Route(ws.GET("/workspaces/{workspace}/federatednamespaces").
To(handler.ListFederatedNamespaces).
Param(ws.PathParameter("workspace", "workspace name")).
Doc("List the federated namespaces of the specified workspace for the current user").
Returns(http.StatusOK, api.StatusOK, api.ListResult{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.NamespaceTag}))
ws.Route(ws.GET("/workspaces/{workspace}/namespaces").
To(handler.ListNamespaces).
Param(ws.PathParameter("workspace", "workspace name")).
Doc("List the namespaces of the specified workspace for the current user").
Returns(http.StatusOK, api.StatusOK, api.ListResult{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.NamespaceTag}))
ws.Route(ws.GET("/workspaces/{workspace}/devops").
To(handler.ListDevOpsProjects).
Param(ws.PathParameter("workspace", "workspace name")).
Doc("List the devops projects of the specified workspace for the current user").
Returns(http.StatusOK, api.StatusOK, api.ListResult{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectTag}))
ws.Route(ws.GET("/workspaces/{workspace}/workspacemembers/{workspacemember}/devops").
To(handler.ListDevOpsProjects).
......@@ -152,7 +152,7 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s
Doc("List the devops projects of specified workspace for the workspace member").
Reads(corev1.Namespace{}).
Returns(http.StatusOK, api.StatusOK, corev1.Namespace{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectTag}))
ws.Route(ws.GET("/workspaces/{workspace}/namespaces/{namespace}").
To(handler.DescribeNamespace).
......@@ -160,7 +160,7 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s
Param(ws.PathParameter("namespace", "project name")).
Doc("Retrieve namespace details.").
Returns(http.StatusOK, api.StatusOK, corev1.Namespace{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.NamespaceTag}))
ws.Route(ws.DELETE("/workspaces/{workspace}/namespaces/{namespace}").
To(handler.DeleteNamespace).
......@@ -168,7 +168,7 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s
Param(ws.PathParameter("namespace", "project name")).
Doc("Delete namespace.").
Returns(http.StatusOK, api.StatusOK, errors.None).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.NamespaceTag}))
ws.Route(ws.POST("/workspaces/{workspace}/namespaces").
To(handler.CreateNamespace).
......@@ -176,7 +176,7 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s
Doc("List the namespaces of the specified workspace for the current user").
Reads(corev1.Namespace{}).
Returns(http.StatusOK, api.StatusOK, corev1.Namespace{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.NamespaceTag}))
ws.Route(ws.GET("/workspaces/{workspace}/workspacemembers/{workspacemember}/namespaces").
To(handler.ListNamespaces).
......@@ -185,7 +185,7 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s
Doc("List the namespaces of the specified workspace for the workspace member").
Reads(corev1.Namespace{}).
Returns(http.StatusOK, api.StatusOK, corev1.Namespace{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.NamespaceTag}))
ws.Route(ws.PUT("/workspaces/{workspace}/namespaces/{namespace}").
To(handler.UpdateNamespace).
......@@ -193,7 +193,7 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s
Param(ws.PathParameter("namespace", "project name")).
Reads(corev1.Namespace{}).
Returns(http.StatusOK, api.StatusOK, corev1.Namespace{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.NamespaceTag}))
ws.Route(ws.PATCH("/workspaces/{workspace}/namespaces/{namespace}").
To(handler.PatchNamespace).
......@@ -202,7 +202,7 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s
Param(ws.PathParameter("namespace", "project name")).
Reads(corev1.Namespace{}).
Returns(http.StatusOK, api.StatusOK, corev1.Namespace{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag}))
Metadata(restfulspec.KeyOpenAPITags, []string{constants.NamespaceTag}))
ws.Route(ws.GET("/events").
To(handler.Events).
......
......@@ -35,11 +35,13 @@ import (
"kubesphere.io/kubesphere/pkg/apiserver/runtime"
"kubesphere.io/kubesphere/pkg/constants"
"kubesphere.io/kubesphere/pkg/informers"
clusterkapisv1alpha1 "kubesphere.io/kubesphere/pkg/kapis/cluster/v1alpha1"
devopsv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/devops/v1alpha2"
devopsv1alpha3 "kubesphere.io/kubesphere/pkg/kapis/devops/v1alpha3"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/iam/v1alpha2"
monitoringv1alpha3 "kubesphere.io/kubesphere/pkg/kapis/monitoring/v1alpha3"
networkv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/network/v1alpha2"
"kubesphere.io/kubesphere/pkg/kapis/oauth"
openpitrixv1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v1"
operationsv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/operations/v1alpha2"
resourcesv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/resources/v1alpha2"
......@@ -49,8 +51,9 @@ import (
terminalv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/terminal/v1alpha2"
"kubesphere.io/kubesphere/pkg/models/iam/am"
"kubesphere.io/kubesphere/pkg/models/iam/im"
"kubesphere.io/kubesphere/pkg/simple/client/devops/fake"
fakedevops "kubesphere.io/kubesphere/pkg/simple/client/devops/fake"
"kubesphere.io/kubesphere/pkg/simple/client/k8s"
"kubesphere.io/kubesphere/pkg/simple/client/openpitrix"
fakes3 "kubesphere.io/kubesphere/pkg/simple/client/s3/fake"
"kubesphere.io/kubesphere/pkg/version"
"log"
......@@ -111,11 +114,14 @@ func generateSwaggerJson() []byte {
informerFactory := informers.NewNullInformerFactory()
urlruntime.Must(devopsv1alpha2.AddToContainer(container, informerFactory.KubeSphereSharedInformerFactory(), &fake.Devops{}, nil, clientsets.KubeSphere(), fakes3.NewFakeS3(), ""))
urlruntime.Must(devopsv1alpha3.AddToContainer(container, &fake.Devops{}, clientsets.Kubernetes(), clientsets.KubeSphere(), informerFactory.KubeSphereSharedInformerFactory(), informerFactory.KubernetesSharedInformerFactory()))
urlruntime.Must(oauth.AddToContainer(container, nil, nil, nil, nil, nil))
urlruntime.Must(clusterkapisv1alpha1.AddToContainer(container, informerFactory.KubernetesSharedInformerFactory(),
informerFactory.KubeSphereSharedInformerFactory(), "", "", ""))
urlruntime.Must(devopsv1alpha2.AddToContainer(container, informerFactory.KubeSphereSharedInformerFactory(), &fakedevops.Devops{}, nil, clientsets.KubeSphere(), fakes3.NewFakeS3(), ""))
urlruntime.Must(devopsv1alpha3.AddToContainer(container, &fakedevops.Devops{}, clientsets.Kubernetes(), clientsets.KubeSphere(), informerFactory.KubeSphereSharedInformerFactory(), informerFactory.KubernetesSharedInformerFactory()))
urlruntime.Must(iamv1alpha2.AddToContainer(container, im.NewOperator(clientsets.KubeSphere(), informerFactory, nil), am.NewReadOnlyOperator(informerFactory), authoptions.NewAuthenticateOptions()))
urlruntime.Must(monitoringv1alpha3.AddToContainer(container, clientsets.Kubernetes(), nil, informerFactory, nil))
urlruntime.Must(openpitrixv1.AddToContainer(container, informerFactory, nil))
urlruntime.Must(openpitrixv1.AddToContainer(container, informerFactory, openpitrix.NewMockClient(nil)))
urlruntime.Must(operationsv1alpha2.AddToContainer(container, clientsets.Kubernetes()))
urlruntime.Must(resourcesv1alpha2.AddToContainer(container, clientsets.Kubernetes(), informerFactory, ""))
urlruntime.Must(resourcesv1alpha3.AddToContainer(container, informerFactory))
......@@ -129,51 +135,119 @@ func generateSwaggerJson() []byte {
PostBuildSwaggerObjectHandler: enrichSwaggerObject}
swagger := restfulspec.BuildSwagger(config)
swagger.Info.Extensions = make(spec.Extensions)
swagger.Info.Extensions.Add("x-tagGroups", []struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}{
{
Name: "IAM",
Tags: []string{constants.IdentityManagementTag, constants.AccessManagementTag},
Name: "Authentication",
Tags: []string{constants.AuthenticationTag},
},
{
Name: "Resources",
Tags: []string{constants.ClusterResourcesTag, constants.NamespaceResourcesTag, constants.UserResourcesTag},
Name: "Identity Management",
Tags: []string{
constants.UserTag,
},
},
{
Name: "AppStore",
Tags: []string{constants.OpenpitrixTag},
Name: "Access Management",
Tags: []string{
constants.ClusterMemberTag,
constants.WorkspaceMemberTag,
constants.DevOpsProjectMemberTag,
constants.NamespaceMemberTag,
constants.GlobalRoleTag,
constants.ClusterRoleTag,
constants.WorkspaceRoleTag,
constants.DevOpsProjectRoleTag,
constants.NamespaceRoleTag,
},
},
{
Name: "Monitoring",
Tags: []string{constants.ComponentStatusTag},
Name: "Multi-tenancy",
Tags: []string{
constants.WorkspaceTag,
constants.NamespaceTag,
constants.UserResourceTag,
},
},
{
Name: "Tenant",
Tags: []string{constants.TenantResourcesTag},
Name: "Multi-cluster",
Tags: []string{
constants.MultiClusterTag,
},
},
{
Name: "Resources",
Tags: []string{
constants.ClusterResourcesTag,
constants.NamespaceResourcesTag,
},
},
{
Name: "App Store",
Tags: []string{
constants.OpenpitrixAppInstanceTag,
constants.OpenpitrixAppTemplateTag,
constants.OpenpitrixCategoryTag,
constants.OpenpitrixAttachmentTag,
constants.OpenpitrixRepositoryTag,
constants.OpenpitrixManagementTag,
},
},
{
Name: "Other",
Tags: []string{constants.VerificationTag, constants.RegistryTag},
Tags: []string{
constants.RegistryTag,
constants.GitTag,
constants.ToolboxTag,
constants.TerminalTag,
},
},
{
Name: "DevOps",
Tags: []string{constants.DevOpsProjectTag, constants.DevOpsProjectCredentialTag,
constants.DevOpsPipelineTag, constants.DevOpsProjectMemberTag,
constants.DevOpsWebhookTag, constants.DevOpsJenkinsfileTag, constants.DevOpsScmTag},
Tags: []string{
constants.DevOpsProjectTag,
constants.DevOpsCredentialTag,
constants.DevOpsPipelineTag,
constants.DevOpsProjectMemberTag,
constants.DevOpsWebhookTag,
constants.DevOpsJenkinsfileTag,
constants.DevOpsScmTag,
constants.DevOpsJenkinsTag,
},
},
{
Name: "Monitoring",
Tags: []string{constants.ClusterMetricsTag, constants.NodeMetricsTag, constants.NamespaceMetricsTag, constants.WorkloadMetricsTag,
constants.PodMetricsTag, constants.ContainerMetricsTag, constants.WorkspaceMetricsTag, constants.ComponentMetricsTag},
Tags: []string{
constants.ClusterMetricsTag,
constants.NodeMetricsTag,
constants.NamespaceMetricsTag,
constants.WorkloadMetricsTag,
constants.PodMetricsTag,
constants.ContainerMetricsTag,
constants.WorkspaceMetricsTag,
constants.ComponentMetricsTag,
constants.ComponentStatusTag,
},
},
{
Name: "Logging",
Tags: []string{constants.LogQueryTag},
},
{
Name: "Events",
Tags: []string{constants.EventsQueryTag},
},
{
Name: "Auditing",
Tags: []string{constants.AuditingQueryTag},
},
{
Name: "Network",
Tags: []string{constants.NetworkTopologyTag},
},
})
data, _ := json.MarshalIndent(swagger, "", " ")
......
# changes to the go-restful-openapi package
## v1.0.0
# v2+ versions are using the Go module of go-restful v3+
- Fix for #19 MapModelTypeNameFunc has incomplete behavior
- prevent array param.Type be overwritten in the else case below (#47)
- Merge paths with existing paths from other webServices (#48)
## v1.4.0 + v2.2.0
- Allow maps as top level types and support maps to slices (#63)
## v1.3.0 + v2.1.0
- add json.Number handling (PR #61)
- add type alias support for primitives (PR #61)
## v1.2.0
- handle map[string][]byte (#59)
## v1.1.0 (v0.14.1)
- Add Host field to Config which is copied into Swagger object
- Enable CORS by default as per the documentation (#58)
- add go module
- update dependencies
## v0.13.0
- Do not use 200 as default response, instead use the one explicitly defined.
- support time.Duration
- Fix Parameter 'AllowableValues' to populate swagger definition
## v0.12.0
- add support for time.Duration
- Populate the swagger definition with the parameter's 'AllowableValues' as an enum (#53)
- Fix for #19 MapModelTypeNameFunc has incomplete behavior
- Merge paths with existing paths from other webServices (#48)
- prevent array param.Type be overwritten in the else case below (#47)
## v0.11.0
......
......@@ -23,4 +23,16 @@ See TestThatExtraTagsAreReadIntoModel for examples.
- [go-restful](https://github.com/emicklei/go-restful)
- [go-openapi](https://github.com/go-openapi/spec)
© 2018, ernestmicklei.com. MIT License. Contributions welcome.
## Go modules
Versions `v1` of this package require Go module version `v2` of the go-restful package.
To use version `v3` of the go-restful package, you need to import `v2 of this package, such as:
import (
restfulspec "github.com/emicklei/go-restful-openapi/v2"
restful "github.com/emicklei/go-restful/v3"
)
© 2017-2020, ernestmicklei.com. MIT License. Contributions welcome.
......@@ -100,9 +100,10 @@ func buildOperation(ws *restful.WebService, r restful.Route, patterns map[string
for k, v := range r.ResponseErrors {
r := buildResponse(v, cfg)
props.StatusCodeResponses[k] = r
if 200 == k { // any 2xx code?
o.Responses.Default = &r
}
}
if r.DefaultResponse != nil {
r := buildResponse(*r.DefaultResponse, cfg)
o.Responses.Default = &r
}
if len(o.Responses.StatusCodeResponses) == 0 {
o.Responses.StatusCodeResponses[200] = spec.Response{ResponseProps: spec.ResponseProps{Description: http.StatusText(http.StatusOK)}}
......@@ -133,6 +134,13 @@ func buildParameter(r restful.Route, restfulParam *restful.Parameter, pattern st
p.Name = param.Name
p.Required = param.Required
if len(param.AllowableValues) > 0 {
p.Enum = make([]interface{}, 0, len(param.AllowableValues))
for key := range param.AllowableValues {
p.Enum = append(p.Enum, key)
}
}
if param.Kind == restful.PathParameterKind {
p.Pattern = pattern
}
......@@ -223,7 +231,7 @@ func isPrimitiveType(modelName string) bool {
if len(modelName) == 0 {
return false
}
return strings.Contains("uint uint8 uint16 uint32 uint64 int int8 int16 int32 int64 float32 float64 bool string byte rune time.Time", modelName)
return strings.Contains("uint uint8 uint16 uint32 uint64 int int8 int16 int32 int64 float32 float64 bool string byte rune time.Time time.Duration", modelName)
}
func jsonSchemaType(modelName string) string {
......@@ -245,6 +253,7 @@ func jsonSchemaType(modelName string) string {
"float32": "number",
"bool": "boolean",
"time.Time": "string",
"time.Duration": "integer",
}
mapped, ok := schemaMap[modelName]
if !ok {
......
......@@ -22,6 +22,8 @@ type PostBuildSwaggerObjectFunc func(s *spec.Swagger)
// Config holds service api metadata.
type Config struct {
// [optional] If set then set this field with the generated Swagger Object
Host string
// WebServicesURL is a DEPRECATED field; it never had any effect in this package.
WebServicesURL string
// APIPath is the path where the JSON api is avaiable , e.g. /apidocs.json
......
......@@ -39,21 +39,23 @@ func (b definitionBuilder) addModel(st reflect.Type, nameOverride string) *spec.
if st.Kind() == reflect.Ptr {
st = st.Elem()
}
if b.isSliceOrArrayType(st.Kind()) {
st = st.Elem()
}
modelName := keyFrom(st, b.Config)
if nameOverride != "" {
modelName = nameOverride
}
// no models needed for primitive types
if b.isPrimitiveType(modelName) {
if b.isPrimitiveType(modelName, st.Kind()) {
return nil
}
// golang encoding/json packages says array and slice values encode as
// JSON arrays, except that []byte encodes as a base64-encoded string.
// If we see a []byte here, treat it at as a primitive type (string)
// and deal with it in buildArrayTypeProperty.
if (st.Kind() == reflect.Slice || st.Kind() == reflect.Array) &&
st.Elem().Kind() == reflect.Uint8 {
if b.isByteArrayType(st) {
return nil
}
// see if we already have visited this model
......@@ -70,9 +72,10 @@ func (b definitionBuilder) addModel(st reflect.Type, nameOverride string) *spec.
// reference the model before further initializing (enables recursive structs)
b.Definitions[modelName] = sm
// check for slice or array
if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
st = st.Elem()
if st.Kind() == reflect.Map {
_, sm = b.buildMapType(st, "value", modelName)
b.Definitions[modelName] = sm
return &sm
}
// check for structure or primitive type
if st.Kind() != reflect.Struct {
......@@ -165,7 +168,7 @@ func (b definitionBuilder) buildProperty(field reflect.StructField, model *spec.
prop.Type = []string{pType}
}
if prop.Format == "" {
prop.Format = b.jsonSchemaFormat(keyFrom(fieldType, b.Config))
prop.Format = b.jsonSchemaFormat(keyFrom(fieldType, b.Config), fieldType.Kind())
}
return jsonName, modelDescription, prop
}
......@@ -185,26 +188,22 @@ func (b definitionBuilder) buildProperty(field reflect.StructField, model *spec.
case fieldKind == reflect.Struct:
jsonName, prop := b.buildStructTypeProperty(field, jsonName, model)
return jsonName, modelDescription, prop
case fieldKind == reflect.Slice || fieldKind == reflect.Array:
case b.isSliceOrArrayType(fieldKind):
jsonName, prop := b.buildArrayTypeProperty(field, jsonName, modelName)
return jsonName, modelDescription, prop
case fieldKind == reflect.Ptr:
jsonName, prop := b.buildPointerTypeProperty(field, jsonName, modelName)
return jsonName, modelDescription, prop
case fieldKind == reflect.String:
stringt := "string"
prop.Type = []string{stringt}
return jsonName, modelDescription, prop
case fieldKind == reflect.Map:
jsonName, prop := b.buildMapTypeProperty(field, jsonName, modelName)
return jsonName, modelDescription, prop
}
fieldTypeName := keyFrom(fieldType, b.Config)
if b.isPrimitiveType(fieldTypeName) {
mapped := b.jsonSchemaType(fieldTypeName)
if b.isPrimitiveType(fieldTypeName, fieldKind) {
mapped := b.jsonSchemaType(fieldTypeName, fieldKind)
prop.Type = []string{mapped}
prop.Format = b.jsonSchemaFormat(fieldTypeName)
prop.Format = b.jsonSchemaFormat(fieldTypeName, fieldKind)
return jsonName, modelDescription, prop
}
modelType := keyFrom(fieldType, b.Config)
......@@ -294,13 +293,13 @@ func (b definitionBuilder) buildArrayTypeProperty(field reflect.StructField, jso
}
var pType = "array"
prop.Type = []string{pType}
isPrimitive := b.isPrimitiveType(fieldType.Elem().Name())
isPrimitive := b.isPrimitiveType(fieldType.Elem().Name(), fieldType.Elem().Kind())
elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem())
prop.Items = &spec.SchemaOrArray{
Schema: &spec.Schema{},
}
if isPrimitive {
mapped := b.jsonSchemaType(elemTypeName)
mapped := b.jsonSchemaType(elemTypeName, fieldType.Elem().Kind())
prop.Items.Schema.Type = []string{mapped}
} else {
prop.Items.Schema.Ref = spec.MustCreateRef("#/definitions/" + elemTypeName)
......@@ -316,37 +315,62 @@ func (b definitionBuilder) buildArrayTypeProperty(field reflect.StructField, jso
}
func (b definitionBuilder) buildMapTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop spec.Schema) {
nameJson, prop = b.buildMapType(field.Type, jsonName, modelName)
setPropertyMetadata(&prop, field)
fieldType := field.Type
return nameJson, prop
}
func (b definitionBuilder) buildMapType(mapType reflect.Type, jsonName, modelName string) (nameJson string, prop spec.Schema) {
var pType = "object"
prop.Type = []string{pType}
// As long as the element isn't an interface, we should be able to figure out what the
// intended type is and represent it in `AdditionalProperties`.
// See: https://swagger.io/docs/specification/data-models/dictionaries/
if fieldType.Elem().Kind().String() != "interface" {
isPrimitive := b.isPrimitiveType(fieldType.Elem().Name())
elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem())
if mapType.Elem().Kind().String() != "interface" {
isSlice := b.isSliceOrArrayType(mapType.Elem().Kind())
if isSlice && !b.isByteArrayType(mapType.Elem()) {
mapType = mapType.Elem()
}
isPrimitive := b.isPrimitiveType(mapType.Elem().Name(), mapType.Elem().Kind())
elemTypeName := b.getElementTypeName(modelName, jsonName, mapType.Elem())
prop.AdditionalProperties = &spec.SchemaOrBool{
Schema: &spec.Schema{},
}
if isPrimitive {
mapped := b.jsonSchemaType(elemTypeName)
prop.AdditionalProperties.Schema.Type = []string{mapped}
// golang encoding/json packages says array and slice values encode as
// JSON arrays, except that []byte encodes as a base64-encoded string.
// If we see a []byte here, treat it at as a string
if b.isByteArrayType(mapType.Elem()) {
prop.AdditionalProperties.Schema.Type = []string{"string"}
} else {
prop.AdditionalProperties.Schema.Ref = spec.MustCreateRef("#/definitions/" + elemTypeName)
}
// add|overwrite model for element type
if fieldType.Elem().Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
if !isPrimitive {
b.addModel(fieldType.Elem(), elemTypeName)
if isSlice {
var item *spec.Schema
if isPrimitive {
mapped := b.jsonSchemaType(elemTypeName, mapType.Kind())
item = &spec.Schema{}
item.Type = []string{mapped}
item.Format = b.jsonSchemaFormat(elemTypeName, mapType.Kind())
} else {
item = spec.RefProperty("#/definitions/" + elemTypeName)
}
prop.AdditionalProperties.Schema = spec.ArrayProperty(item)
} else if isPrimitive {
mapped := b.jsonSchemaType(elemTypeName, mapType.Elem().Kind())
prop.AdditionalProperties.Schema.Type = []string{mapped}
} else {
prop.AdditionalProperties.Schema.Ref = spec.MustCreateRef("#/definitions/" + elemTypeName)
}
// add|overwrite model for element type
if mapType.Elem().Kind() == reflect.Ptr {
mapType = mapType.Elem()
}
if !isPrimitive {
b.addModel(mapType.Elem(), elemTypeName)
}
}
}
return jsonName, prop
}
func (b definitionBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop spec.Schema) {
setPropertyMetadata(&prop, field)
fieldType := field.Type
......@@ -355,13 +379,13 @@ func (b definitionBuilder) buildPointerTypeProperty(field reflect.StructField, j
if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array {
var pType = "array"
prop.Type = []string{pType}
isPrimitive := b.isPrimitiveType(fieldType.Elem().Elem().Name())
isPrimitive := b.isPrimitiveType(fieldType.Elem().Elem().Name(), fieldType.Elem().Elem().Kind())
elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem().Elem())
prop.Items = &spec.SchemaOrArray{
Schema: &spec.Schema{},
}
if isPrimitive {
primName := b.jsonSchemaType(elemName)
primName := b.jsonSchemaType(elemName, fieldType.Elem().Elem().Kind())
prop.Items.Schema.Type = []string{primName}
} else {
prop.Items.Schema.Ref = spec.MustCreateRef("#/definitions/" + elemName)
......@@ -373,10 +397,11 @@ func (b definitionBuilder) buildPointerTypeProperty(field reflect.StructField, j
} else {
// non-array, pointer type
fieldTypeName := keyFrom(fieldType.Elem(), b.Config)
var pType = b.jsonSchemaType(fieldTypeName) // no star, include pkg path
if b.isPrimitiveType(fieldTypeName) {
isPrimitive := b.isPrimitiveType(fieldTypeName, fieldType.Elem().Kind())
var pType = b.jsonSchemaType(fieldTypeName, fieldType.Elem().Kind()) // no star, include pkg path
if isPrimitive {
prop.Type = []string{pType}
prop.Format = b.jsonSchemaFormat(fieldTypeName)
prop.Format = b.jsonSchemaFormat(fieldTypeName, fieldType.Elem().Kind())
return jsonName, prop
}
prop.Ref = spec.MustCreateRef("#/definitions/" + pType)
......@@ -385,7 +410,9 @@ func (b definitionBuilder) buildPointerTypeProperty(field reflect.StructField, j
elemName = modelName + "." + jsonName
prop.Ref = spec.MustCreateRef("#/definitions/" + elemName)
}
b.addModel(fieldType.Elem(), elemName)
if !isPrimitive {
b.addModel(fieldType.Elem(), elemName)
}
}
return jsonName, prop
}
......@@ -416,12 +443,34 @@ func keyFrom(st reflect.Type, cfg Config) string {
return key
}
func (b definitionBuilder) isSliceOrArrayType(t reflect.Kind) bool {
return t == reflect.Slice || t == reflect.Array
}
// Does the type represent a []byte?
func (b definitionBuilder) isByteArrayType(t reflect.Type) bool {
return (t.Kind() == reflect.Slice || t.Kind() == reflect.Array) &&
t.Elem().Kind() == reflect.Uint8
}
// see also https://golang.org/ref/spec#Numeric_types
func (b definitionBuilder) isPrimitiveType(modelName string) bool {
func (b definitionBuilder) isPrimitiveType(modelName string, modelKind reflect.Kind) bool {
switch modelKind {
case reflect.Bool:
return true
case reflect.Float32, reflect.Float64,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return true
case reflect.String:
return true
}
if len(modelName) == 0 {
return false
}
return strings.Contains("uint uint8 uint16 uint32 uint64 int int8 int16 int32 int64 float32 float64 bool string byte rune time.Time", modelName)
return strings.Contains("time.Time time.Duration json.Number", modelName)
}
// jsonNameOfField returns the name of the field as it should appear in JSON format
......@@ -440,54 +489,80 @@ func (b definitionBuilder) jsonNameOfField(field reflect.StructField) string {
}
// see also http://json-schema.org/latest/json-schema-core.html#anchor8
func (b definitionBuilder) jsonSchemaType(modelName string) string {
func (b definitionBuilder) jsonSchemaType(modelName string, modelKind reflect.Kind) string {
schemaMap := map[string]string{
"uint": "integer",
"uint8": "integer",
"uint16": "integer",
"uint32": "integer",
"uint64": "integer",
"int": "integer",
"int8": "integer",
"int16": "integer",
"int32": "integer",
"int64": "integer",
"byte": "integer",
"float64": "number",
"float32": "number",
"bool": "boolean",
"time.Time": "string",
}
mapped, ok := schemaMap[modelName]
if !ok {
return modelName // use as is (custom or struct)
}
return mapped
"time.Time": "string",
"time.Duration": "integer",
"json.Number": "number",
}
if mapped, ok := schemaMap[modelName]; ok {
return mapped
}
// check if original type is primitive
switch modelKind {
case reflect.Bool:
return "boolean"
case reflect.Float32, reflect.Float64:
return "number"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "integer"
case reflect.String:
return "string"
}
return modelName // use as is (custom or struct)
}
func (b definitionBuilder) jsonSchemaFormat(modelName string) string {
func (b definitionBuilder) jsonSchemaFormat(modelName string, modelKind reflect.Kind) string {
if b.Config.SchemaFormatHandler != nil {
if mapped := b.Config.SchemaFormatHandler(modelName); mapped != "" {
return mapped
}
}
schemaMap := map[string]string{
"int": "int32",
"int32": "int32",
"int64": "int64",
"byte": "byte",
"uint": "integer",
"uint8": "byte",
"float64": "double",
"float32": "float",
"time.Time": "date-time",
"*time.Time": "date-time",
}
mapped, ok := schemaMap[modelName]
if !ok {
return "" // no format
}
return mapped
"time.Time": "date-time",
"*time.Time": "date-time",
"time.Duration": "integer",
"*time.Duration": "integer",
"json.Number": "double",
"*json.Number": "double",
}
if mapped, ok := schemaMap[modelName]; ok {
return mapped
}
// check if original type is primitive
switch modelKind {
case reflect.Float32:
return "float"
case reflect.Float64:
return "double"
case reflect.Int:
return "int32"
case reflect.Int8:
return "byte"
case reflect.Int16:
return "integer"
case reflect.Int32:
return "int32"
case reflect.Int64:
return "int64"
case reflect.Uint:
return "integer"
case reflect.Uint8:
return "byte"
case reflect.Uint16:
return "integer"
case reflect.Uint32:
return "integer"
case reflect.Uint64:
return "integer"
}
return "" // no format
}
module github.com/emicklei/go-restful-openapi
require (
github.com/PuerkitoBio/purell v1.1.0 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/emicklei/go-restful v2.9.6+incompatible
github.com/go-openapi/jsonpointer v0.0.0-20180322222829-3a0015ad55fa // indirect
github.com/go-openapi/jsonreference v0.0.0-20180322222742-3fb327e6747d // indirect
github.com/go-openapi/spec v0.0.0-20180415031709-bcff419492ee
github.com/go-openapi/swag v0.0.0-20180405201759-811b1089cde9 // indirect
github.com/mailru/easyjson v0.0.0-20180323154445-8b799c424f57 // indirect
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/net v0.0.0-20180530234432-1e491301e022 // indirect
golang.org/x/text v0.3.0 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
)
go 1.13
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful v1.1.3 h1:KOKLkEASmIa2roa2xEV6WkADqyWrok5dt3TOMMHF1fE=
github.com/emicklei/go-restful v1.1.3/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.3+incompatible h1:2OwhVdhtzYUp5P5wuGsVDPagKSRd9JK72sJCHVCXh5g=
github.com/emicklei/go-restful v2.9.3+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.6+incompatible h1:tfrHha8zJ01ywiOEC1miGY8st1/igzWB8OmvPgoYX7w=
github.com/emicklei/go-restful v2.9.6+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/go-openapi/jsonpointer v0.0.0-20180322222829-3a0015ad55fa h1:hr8WVDjg4JKtQptZpzyb196TmruCs7PIsdJz8KAOZp8=
github.com/go-openapi/jsonpointer v0.0.0-20180322222829-3a0015ad55fa/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonreference v0.0.0-20180322222742-3fb327e6747d h1:k3UQ7Z8yFYq0BNkYykKIheY0HlZBl1Hku+pO9HE9FNU=
github.com/go-openapi/jsonreference v0.0.0-20180322222742-3fb327e6747d/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/spec v0.0.0-20180415031709-bcff419492ee h1:eo0HQoNFtbiEc7+1gRF9pgW6azx8a1cO2fXcqq1MuD0=
github.com/go-openapi/spec v0.0.0-20180415031709-bcff419492ee/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k4=
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/swag v0.0.0-20180405201759-811b1089cde9 h1:+vsw187FKvA2QUGAcE+vQSfyxqLbUXixPYRRMAzwu04=
github.com/go-openapi/swag v0.0.0-20180405201759-811b1089cde9/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/mailru/easyjson v0.0.0-20180323154445-8b799c424f57 h1:qhv1ir3dIyOFmFU+5KqG4dF3zSQTA4nn1DFhu2NQC44=
github.com/mailru/easyjson v0.0.0-20180323154445-8b799c424f57/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/net v0.0.0-20180530234432-1e491301e022 h1:MVYFTUmVD3/+ERcvRRI+P/C2+WOUimXh+Pd8LVsklZ4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
......@@ -12,7 +12,7 @@ func NewOpenAPIService(config Config) *restful.WebService {
ws := new(restful.WebService)
ws.Path(config.APIPath)
ws.Produces(restful.MIME_JSON)
if config.DisableCORS {
if !config.DisableCORS {
ws.Filter(enableCORS)
}
......@@ -45,6 +45,7 @@ func BuildSwagger(config Config) *spec.Swagger {
}
swagger := &spec.Swagger{
SwaggerProps: spec.SwaggerProps{
Host: config.Host,
Swagger: "2.0",
Paths: paths,
Definitions: definitions,
......
......@@ -68,3 +68,4 @@ examples/restful-html-template
s.html
restful-path-tail
.idea
......@@ -3,4 +3,11 @@ language: go
go:
- 1.x
script: go test -v
\ No newline at end of file
before_install:
- go test -v
script:
- go test -race -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)
\ No newline at end of file
## Change history of go-restful
# Change history of go-restful
v2.9.5
## [v2.14.3] - 2020-08-31
- Fixed duplicate compression in dispatch. #449
## [v2.14.2] - 2020-08-31
- Added check on writer to prevent compression of response twice. #447
## [v2.14.0] - 2020-08-19
- Enable content encoding on Handle and ServeHTTP (#446)
- List available representations in 406 body (#437)
- Convert to string using rune() (#443)
## [v2.13.0] - 2020-06-21
- 405 Method Not Allowed must have Allow header (#436)
- add field allowedMethodsWithoutContentType (#424)
## v2.12.0
- support describing response headers (#426)
- fix openapi examples (#425)
- merge v3 fix (#422)
## v2.11.1
- fix WriteError return value (#415)
## v2.11.0
- allow prefix and suffix in path variable expression (#414)
## v2.9.6
- support google custome verb (#413)
## v2.9.5
- fix panic in Response.WriteError if err == nil
v2.9.4
## v2.9.4
- fix issue #400 , parsing mime type quality
- Route Builder added option for contentEncodingEnabled (#398)
v2.9.3
## v2.9.3
- Avoid return of 415 Unsupported Media Type when request body is empty (#396)
v2.9.2
## v2.9.2
- Reduce allocations in per-request methods to improve performance (#395)
v2.9.1
## v2.9.1
- Fix issue with default responses and invalid status code 0. (#393)
v2.9.0
## v2.9.0
- add per Route content encoding setting (overrides container setting)
v2.8.0
## v2.8.0
- add Request.QueryParameters()
- add json-iterator (via build tag)
- disable vgo module (until log is moved)
v2.7.1
## v2.7.1
- add vgo module
v2.6.1
## v2.6.1
- add JSONNewDecoderFunc to allow custom JSON Decoder usage (go 1.10+)
v2.6.0
## v2.6.0
- Make JSR 311 routing and path param processing consistent
- Adding description to RouteBuilder.Reads()
- Update example for Swagger12 and OpenAPI
2017-09-13
## 2017-09-13
- added route condition functions using `.If(func)` in route building.
2017-02-16
## 2017-02-16
- solved issue #304, make operation names unique
2017-01-30
## 2017-01-30
[IMPORTANT] For swagger users, change your import statement to:
swagger "github.com/emicklei/go-restful-swagger12"
......@@ -61,60 +99,60 @@ v2.6.0
- moved swagger 1.2 code to go-restful-swagger12
- created TAG 2.0.0
2017-01-27
## 2017-01-27
- remove defer request body close
- expose Dispatch for testing filters and Routefunctions
- swagger response model cannot be array
- created TAG 1.0.0
2016-12-22
## 2016-12-22
- (API change) Remove code related to caching request content. Removes SetCacheReadEntity(doCache bool)
2016-11-26
## 2016-11-26
- Default change! now use CurlyRouter (was RouterJSR311)
- Default change! no more caching of request content
- Default change! do not recover from panics
2016-09-22
## 2016-09-22
- fix the DefaultRequestContentType feature
2016-02-14
## 2016-02-14
- take the qualify factor of the Accept header mediatype into account when deciding the contentype of the response
- add constructors for custom entity accessors for xml and json
2015-09-27
## 2015-09-27
- rename new WriteStatusAnd... to WriteHeaderAnd... for consistency
2015-09-25
## 2015-09-25
- fixed problem with changing Header after WriteHeader (issue 235)
2015-09-14
## 2015-09-14
- changed behavior of WriteHeader (immediate write) and WriteEntity (no status write)
- added support for custom EntityReaderWriters.
2015-08-06
## 2015-08-06
- add support for reading entities from compressed request content
- use sync.Pool for compressors of http response and request body
- add Description to Parameter for documentation in Swagger UI
2015-03-20
## 2015-03-20
- add configurable logging
2015-03-18
## 2015-03-18
- if not specified, the Operation is derived from the Route function
2015-03-17
## 2015-03-17
- expose Parameter creation functions
- make trace logger an interface
......@@ -123,26 +161,26 @@ v2.6.0
- JSR311 router now handles wildcards
- add Notes to Route
2014-11-27
## 2014-11-27
- (api add) PrettyPrint per response. (as proposed in #167)
2014-11-12
## 2014-11-12
- (api add) ApiVersion(.) for documentation in Swagger UI
2014-11-10
## 2014-11-10
- (api change) struct fields tagged with "description" show up in Swagger UI
2014-10-31
## 2014-10-31
- (api change) ReturnsError -> Returns
- (api add) RouteBuilder.Do(aBuilder) for DRY use of RouteBuilder
- fix swagger nested structs
- sort Swagger response messages by code
2014-10-23
## 2014-10-23
- (api add) ReturnsError allows you to document Http codes in swagger
- fixed problem with greedy CurlyRouter
......@@ -156,73 +194,73 @@ v2.6.0
- (api add) added AllowedDomains in CORS
- (api add) ParameterNamed for detailed documentation
2014-04-16
## 2014-04-16
- (api add) expose constructor of Request for testing.
2014-06-27
## 2014-06-27
- (api add) ParameterNamed gives access to a Parameter definition and its data (for further specification).
- (api add) SetCacheReadEntity allow scontrol over whether or not the request body is being cached (default true for compatibility reasons).
2014-07-03
## 2014-07-03
- (api add) CORS can be configured with a list of allowed domains
2014-03-12
## 2014-03-12
- (api add) Route path parameters can use wildcard or regular expressions. (requires CurlyRouter)
2014-02-26
## 2014-02-26
- (api add) Request now provides information about the matched Route, see method SelectedRoutePath
2014-02-17
## 2014-02-17
- (api change) renamed parameter constants (go-lint checks)
2014-01-10
## 2014-01-10
- (api add) support for CloseNotify, see http://golang.org/pkg/net/http/#CloseNotifier
2014-01-07
## 2014-01-07
- (api change) Write* methods in Response now return the error or nil.
- added example of serving HTML from a Go template.
- fixed comparing Allowed headers in CORS (is now case-insensitive)
2013-11-13
## 2013-11-13
- (api add) Response knows how many bytes are written to the response body.
2013-10-29
## 2013-10-29
- (api add) RecoverHandler(handler RecoverHandleFunction) to change how panic recovery is handled. Default behavior is to log and return a stacktrace. This may be a security issue as it exposes sourcecode information.
2013-10-04
## 2013-10-04
- (api add) Response knows what HTTP status has been written
- (api add) Request can have attributes (map of string->interface, also called request-scoped variables
2013-09-12
## 2013-09-12
- (api change) Router interface simplified
- Implemented CurlyRouter, a Router that does not use|allow regular expressions in paths
2013-08-05
## 2013-08-05
- add OPTIONS support
- add CORS support
2013-08-27
## 2013-08-27
- fixed some reported issues (see github)
- (api change) deprecated use of WriteError; use WriteErrorString instead
2014-04-15
## 2014-04-15
- (fix) v1.0.1 tag: fix Issue 111: WriteErrorString
2013-08-08
## 2013-08-08
- (api add) Added implementation Container: a WebServices collection with its own http.ServeMux allowing multiple endpoints per program. Existing uses of go-restful will register their services to the DefaultContainer.
- (api add) the swagger package has be extended to have a UI per container.
......@@ -235,38 +273,38 @@ Important API changes:
- (api remove) package variable EnableContentEncoding no longer works ; use restful.DefaultContainer.EnableContentEncoding(true) instead.
2013-07-06
## 2013-07-06
- (api add) Added support for response encoding (gzip and deflate(zlib)). This feature is disabled on default (for backwards compatibility). Use restful.EnableContentEncoding = true in your initialization to enable this feature.
2013-06-19
## 2013-06-19
- (improve) DoNotRecover option, moved request body closer, improved ReadEntity
2013-06-03
## 2013-06-03
- (api change) removed Dispatcher interface, hide PathExpression
- changed receiver names of type functions to be more idiomatic Go
2013-06-02
## 2013-06-02
- (optimize) Cache the RegExp compilation of Paths.
2013-05-22
## 2013-05-22
- (api add) Added support for request/response filter functions
2013-05-18
## 2013-05-18
- (api add) Added feature to change the default Http Request Dispatch function (travis cline)
- (api change) Moved Swagger Webservice to swagger package (see example restful-user)
[2012-11-14 .. 2013-05-18>
## [2012-11-14 .. 2013-05-18>
- See https://github.com/emicklei/go-restful/commits
2012-11-14
## 2012-11-14
- Initial commit
......
all: test
test:
go test -v .
go vet .
go test -cover -v .
ex:
cd examples && ls *.go | xargs go build -o /tmp/ignore
\ No newline at end of file
......@@ -4,7 +4,8 @@ package for building REST-style Web Services using Google Go
[![Build Status](https://travis-ci.org/emicklei/go-restful.png)](https://travis-ci.org/emicklei/go-restful)
[![Go Report Card](https://goreportcard.com/badge/github.com/emicklei/go-restful)](https://goreportcard.com/report/github.com/emicklei/go-restful)
[![GoDoc](https://godoc.org/github.com/emicklei/go-restful?status.svg)](https://godoc.org/github.com/emicklei/go-restful)
[![GoDoc](https://godoc.org/github.com/emicklei/go-restful?status.svg)](https://pkg.go.dev/github.com/emicklei/go-restful)
[![codecov](https://codecov.io/gh/emicklei/go-restful/branch/master/graph/badge.svg)](https://codecov.io/gh/emicklei/go-restful)
- [Code examples](https://github.com/emicklei/go-restful/tree/master/examples)
......@@ -18,6 +19,28 @@ REST asks developers to use HTTP methods explicitly and in a way that's consiste
- PATCH = Update partial content of a resource
- OPTIONS = Get information about the communication options for the request URI
### Usage
#### Without Go Modules
All versions up to `v2.*.*` (on the master) are not supporting Go modules.
```
import (
restful "github.com/emicklei/go-restful"
)
```
#### Using Go Modules
As of version `v3.0.0` (on the v3 branch), this package supports Go modules.
```
import (
restful "github.com/emicklei/go-restful/v3"
)
```
### Example
```Go
......@@ -43,9 +66,9 @@ func (u UserResource) findUser(request *restful.Request, response *restful.Respo
### Features
- Routes for request &#8594; function mapping with path parameter (e.g. {id}) support
- Routes for request &#8594; function mapping with path parameter (e.g. {id} but also prefix_{var} and {var}_suffix) support
- Configurable router:
- (default) Fast routing algorithm that allows static elements, regular expressions and dynamic parameters in the URL path (e.g. /meetings/{id} or /static/{subpath:*}
- (default) Fast routing algorithm that allows static elements, [google custom method](https://cloud.google.com/apis/design/custom_methods), regular expressions and dynamic parameters in the URL path (e.g. /resource/name:customVerb, /meetings/{id} or /static/{subpath:*})
- Routing algorithm after [JSR311](http://jsr311.java.net/nonav/releases/1.1/spec/spec.html) that is implemented using (but does **not** accept) regular expressions
- Request API for reading structs from JSON/XML and accesing parameters (path,query,header)
- Response API for writing structs to JSON/XML and setting headers
......@@ -85,4 +108,4 @@ TODO: write examples of these.
Type ```git shortlog -s``` for a full list of contributors.
© 2012 - 2018, http://ernestmicklei.com. MIT License. Contributions are welcome.
© 2012 - 2020, http://ernestmicklei.com. MIT License. Contributions are welcome.
......@@ -185,6 +185,11 @@ func logStackOnRecover(panicReason interface{}, httpWriter http.ResponseWriter)
// when a ServiceError is returned during route selection. Default implementation
// calls resp.WriteErrorString(err.Code, err.Message)
func writeServiceError(err ServiceError, req *Request, resp *Response) {
for header, values := range err.Header {
for _, value := range values {
resp.Header().Add(header, value)
}
}
resp.WriteErrorString(err.Code, err.Message)
}
......@@ -201,6 +206,7 @@ func (c *Container) Dispatch(httpWriter http.ResponseWriter, httpRequest *http.R
// Dispatch the incoming Http Request to a matching WebService.
func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.Request) {
// so we can assign a compressing one later
writer := httpWriter
// CompressingResponseWriter should be closed after all operations are done
......@@ -231,28 +237,8 @@ func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.R
c.webServices,
httpRequest)
}()
// Detect if compression is needed
// assume without compression, test for override
contentEncodingEnabled := c.contentEncodingEnabled
if route != nil && route.contentEncodingEnabled != nil {
contentEncodingEnabled = *route.contentEncodingEnabled
}
if contentEncodingEnabled {
doCompress, encoding := wantsCompressedResponse(httpRequest)
if doCompress {
var err error
writer, err = NewCompressingResponseWriter(httpWriter, encoding)
if err != nil {
log.Print("unable to install compressor: ", err)
httpWriter.WriteHeader(http.StatusInternalServerError)
return
}
}
}
if err != nil {
// a non-200 response has already been written
// a non-200 response (may be compressed) has already been written
// run container filters anyway ; they should not touch the response...
chain := FilterChain{Filters: c.containerFilters, Target: func(req *Request, resp *Response) {
switch err.(type) {
......@@ -265,6 +251,29 @@ func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.R
chain.ProcessFilter(NewRequest(httpRequest), NewResponse(writer))
return
}
// Unless httpWriter is already an CompressingResponseWriter see if we need to install one
if _, isCompressing := httpWriter.(*CompressingResponseWriter); !isCompressing {
// Detect if compression is needed
// assume without compression, test for override
contentEncodingEnabled := c.contentEncodingEnabled
if route != nil && route.contentEncodingEnabled != nil {
contentEncodingEnabled = *route.contentEncodingEnabled
}
if contentEncodingEnabled {
doCompress, encoding := wantsCompressedResponse(httpRequest)
if doCompress {
var err error
writer, err = NewCompressingResponseWriter(httpWriter, encoding)
if err != nil {
log.Print("unable to install compressor: ", err)
httpWriter.WriteHeader(http.StatusInternalServerError)
return
}
}
}
}
pathProcessor, routerProcessesPath := c.router.(PathProcessor)
if !routerProcessesPath {
pathProcessor = defaultPathProcessor{}
......@@ -272,16 +281,13 @@ func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.R
pathParams := pathProcessor.ExtractParameters(route, webService, httpRequest.URL.Path)
wrappedRequest, wrappedResponse := route.wrapRequestResponse(writer, httpRequest, pathParams)
// pass through filters (if any)
if len(c.containerFilters)+len(webService.filters)+len(route.Filters) > 0 {
if size := len(c.containerFilters) + len(webService.filters) + len(route.Filters); size > 0 {
// compose filter chain
allFilters := []FilterFunction{}
allFilters := make([]FilterFunction, 0, size)
allFilters = append(allFilters, c.containerFilters...)
allFilters = append(allFilters, webService.filters...)
allFilters = append(allFilters, route.Filters...)
chain := FilterChain{Filters: allFilters, Target: func(req *Request, resp *Response) {
// handle request by route after passing all filters
route.Function(wrappedRequest, wrappedResponse)
}}
chain := FilterChain{Filters: allFilters, Target: route.Function}
chain.ProcessFilter(wrappedRequest, wrappedResponse)
} else {
// no filters, handle request by route
......@@ -299,13 +305,75 @@ func fixedPrefixPath(pathspec string) string {
}
// ServeHTTP implements net/http.Handler therefore a Container can be a Handler in a http.Server
func (c *Container) ServeHTTP(httpwriter http.ResponseWriter, httpRequest *http.Request) {
c.ServeMux.ServeHTTP(httpwriter, httpRequest)
func (c *Container) ServeHTTP(httpWriter http.ResponseWriter, httpRequest *http.Request) {
// Skip, if content encoding is disabled
if !c.contentEncodingEnabled {
c.ServeMux.ServeHTTP(httpWriter, httpRequest)
return
}
// content encoding is enabled
// Skip, if httpWriter is already an CompressingResponseWriter
if _, ok := httpWriter.(*CompressingResponseWriter); ok {
c.ServeMux.ServeHTTP(httpWriter, httpRequest)
return
}
writer := httpWriter
// CompressingResponseWriter should be closed after all operations are done
defer func() {
if compressWriter, ok := writer.(*CompressingResponseWriter); ok {
compressWriter.Close()
}
}()
doCompress, encoding := wantsCompressedResponse(httpRequest)
if doCompress {
var err error
writer, err = NewCompressingResponseWriter(httpWriter, encoding)
if err != nil {
log.Print("unable to install compressor: ", err)
httpWriter.WriteHeader(http.StatusInternalServerError)
return
}
}
c.ServeMux.ServeHTTP(writer, httpRequest)
}
// Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics.
func (c *Container) Handle(pattern string, handler http.Handler) {
c.ServeMux.Handle(pattern, handler)
c.ServeMux.Handle(pattern, http.HandlerFunc(func(httpWriter http.ResponseWriter, httpRequest *http.Request) {
// Skip, if httpWriter is already an CompressingResponseWriter
if _, ok := httpWriter.(*CompressingResponseWriter); ok {
handler.ServeHTTP(httpWriter, httpRequest)
return
}
writer := httpWriter
// CompressingResponseWriter should be closed after all operations are done
defer func() {
if compressWriter, ok := writer.(*CompressingResponseWriter); ok {
compressWriter.Close()
}
}()
if c.contentEncodingEnabled {
doCompress, encoding := wantsCompressedResponse(httpRequest)
if doCompress {
var err error
writer, err = NewCompressingResponseWriter(httpWriter, encoding)
if err != nil {
log.Print("unable to install compressor: ", err)
httpWriter.WriteHeader(http.StatusInternalServerError)
return
}
}
}
handler.ServeHTTP(writer, httpRequest)
}))
}
// HandleWithFilter registers the handler for the given pattern.
......@@ -319,7 +387,7 @@ func (c *Container) HandleWithFilter(pattern string, handler http.Handler) {
}
chain := FilterChain{Filters: c.containerFilters, Target: func(req *Request, resp *Response) {
handler.ServeHTTP(httpResponse, httpRequest)
handler.ServeHTTP(resp, req.Request)
}}
chain.ProcessFilter(NewRequest(httpRequest), NewResponse(httpResponse))
}
......
......@@ -47,7 +47,7 @@ func (c CurlyRouter) SelectRoute(
func (c CurlyRouter) selectRoutes(ws *WebService, requestTokens []string) sortableCurlyRoutes {
candidates := make(sortableCurlyRoutes, 0, 8)
for _, each := range ws.routes {
matches, paramCount, staticCount := c.matchesRouteByPathTokens(each.pathParts, requestTokens)
matches, paramCount, staticCount := c.matchesRouteByPathTokens(each.pathParts, requestTokens, each.hasCustomVerb)
if matches {
candidates.add(curlyRoute{each, paramCount, staticCount}) // TODO make sure Routes() return pointers?
}
......@@ -57,7 +57,7 @@ func (c CurlyRouter) selectRoutes(ws *WebService, requestTokens []string) sortab
}
// matchesRouteByPathTokens computes whether it matches, howmany parameters do match and what the number of static path elements are.
func (c CurlyRouter) matchesRouteByPathTokens(routeTokens, requestTokens []string) (matches bool, paramCount int, staticCount int) {
func (c CurlyRouter) matchesRouteByPathTokens(routeTokens, requestTokens []string, routeHasCustomVerb bool) (matches bool, paramCount int, staticCount int) {
if len(routeTokens) < len(requestTokens) {
// proceed in matching only if last routeToken is wildcard
count := len(routeTokens)
......@@ -72,6 +72,15 @@ func (c CurlyRouter) matchesRouteByPathTokens(routeTokens, requestTokens []strin
return false, 0, 0
}
requestToken := requestTokens[i]
if routeHasCustomVerb && hasCustomVerb(routeToken){
if !isMatchCustomVerb(routeToken, requestToken) {
return false, 0, 0
}
staticCount++
requestToken = removeCustomVerb(requestToken)
routeToken = removeCustomVerb(routeToken)
}
if strings.HasPrefix(routeToken, "{") {
paramCount++
if colon := strings.Index(routeToken, ":"); colon != -1 {
......
package restful
import (
"fmt"
"regexp"
)
var (
customVerbReg = regexp.MustCompile(":([A-Za-z]+)$")
)
func hasCustomVerb(routeToken string) bool {
return customVerbReg.MatchString(routeToken)
}
func isMatchCustomVerb(routeToken string, pathToken string) bool {
rs := customVerbReg.FindStringSubmatch(routeToken)
if len(rs) < 2 {
return false
}
customVerb := rs[1]
specificVerbReg := regexp.MustCompile(fmt.Sprintf(":%s$", customVerb))
return specificVerbReg.MatchString(pathToken)
}
func removeCustomVerb(str string) string {
return customVerbReg.ReplaceAllString(str, "")
}
......@@ -9,6 +9,7 @@ import (
"fmt"
"net/http"
"sort"
"strings"
)
// RouterJSR311 implements the flow for matching Requests to Routes (and consequently Resource Functions)
......@@ -98,7 +99,18 @@ func (r RouterJSR311) detectRoute(routes []Route, httpRequest *http.Request) (*R
if trace {
traceLogger.Printf("no Route found (in %d routes) that matches HTTP method %s\n", len(previous), httpRequest.Method)
}
return nil, NewError(http.StatusMethodNotAllowed, "405: Method Not Allowed")
allowed := []string{}
allowedLoop:
for _, candidate := range previous {
for _, method := range allowed {
if method == candidate.Method {
continue allowedLoop
}
}
allowed = append(allowed, candidate.Method)
}
header := http.Header{"Allow": []string{strings.Join(allowed, ", ")}}
return nil, NewErrorWithHeader(http.StatusMethodNotAllowed, "405: Method Not Allowed", header)
}
// content-type
......@@ -135,7 +147,14 @@ func (r RouterJSR311) detectRoute(routes []Route, httpRequest *http.Request) (*R
if trace {
traceLogger.Printf("no Route found (from %d) that matches HTTP Accept: %s\n", len(previous), accept)
}
return nil, NewError(http.StatusNotAcceptable, "406: Not Acceptable")
available := []string{}
for _, candidate := range previous {
available = append(available, candidate.Produces...)
}
return nil, NewError(
http.StatusNotAcceptable,
fmt.Sprintf("406: Not Acceptable\n\nAvailable representations: %s", strings.Join(available, ", ")),
)
}
// return r.bestMatchByMedia(outputMediaOk, contentType, accept), nil
return candidates[0], nil
......
......@@ -29,7 +29,12 @@ func (d defaultPathProcessor) ExtractParameters(r *Route, _ *WebService, urlPath
} else {
value = urlParts[i]
}
if strings.HasPrefix(key, "{") { // path-parameter
if r.hasCustomVerb && hasCustomVerb(key) {
key = removeCustomVerb(key)
value = removeCustomVerb(value)
}
if strings.Index(key, "{") > -1 { // path-parameter
if colon := strings.Index(key, ":"); colon != -1 {
// extract by regex
regPart := key[colon+1 : len(key)-1]
......@@ -42,7 +47,13 @@ func (d defaultPathProcessor) ExtractParameters(r *Route, _ *WebService, urlPath
}
} else {
// without enclosing {}
pathParameters[key[1:len(key)-1]] = value
startIndex := strings.Index(key, "{")
endKeyIndex := strings.Index(key, "}")
suffixLength := len(key) - endKeyIndex - 1
endValueIndex := len(value) - suffixLength
pathParameters[key[startIndex+1:endKeyIndex]] = value[startIndex:endValueIndex]
}
}
}
......
......@@ -174,15 +174,16 @@ func (r *Response) WriteHeaderAndJson(status int, value interface{}, contentType
return writeJSON(r, status, contentType, value)
}
// WriteError write the http status and the error string on the response. err can be nil.
func (r *Response) WriteError(httpStatus int, err error) error {
// WriteError writes the http status and the error string on the response. err can be nil.
// Return an error if writing was not succesful.
func (r *Response) WriteError(httpStatus int, err error) (writeErr error) {
r.err = err
if err == nil {
r.WriteErrorString(httpStatus, "")
writeErr = r.WriteErrorString(httpStatus, "")
} else {
r.WriteErrorString(httpStatus, err.Error())
writeErr = r.WriteErrorString(httpStatus, err.Error())
}
return err
return writeErr
}
// WriteServiceError is a convenience method for a responding with a status and a ServiceError
......
......@@ -49,11 +49,20 @@ type Route struct {
//Overrides the container.contentEncodingEnabled
contentEncodingEnabled *bool
// indicate route path has custom verb
hasCustomVerb bool
// if a request does not include a content-type header then
// depending on the method, it may return a 415 Unsupported Media
// Must have uppercase HTTP Method names such as GET,HEAD,OPTIONS,...
allowedMethodsWithoutContentType []string
}
// Initialize for Route
func (r *Route) postBuild() {
r.pathParts = tokenizePath(r.Path)
r.hasCustomVerb = hasCustomVerb(r.Path)
}
// Create Request and Response from their http versions
......@@ -67,17 +76,6 @@ func (r *Route) wrapRequestResponse(httpWriter http.ResponseWriter, httpRequest
return wrappedRequest, wrappedResponse
}
// dispatchWithFilters call the function after passing through its own filters
func (r *Route) dispatchWithFilters(wrappedRequest *Request, wrappedResponse *Response) {
if len(r.Filters) > 0 {
chain := FilterChain{Filters: r.Filters, Target: r.Function}
chain.ProcessFilter(wrappedRequest, wrappedResponse)
} else {
// unfiltered
r.Function(wrappedRequest, wrappedResponse)
}
}
func stringTrimSpaceCutset(r rune) bool {
return r == ' '
}
......@@ -121,8 +119,17 @@ func (r Route) matchesContentType(mimeTypes string) bool {
if len(mimeTypes) == 0 {
// idempotent methods with (most-likely or guaranteed) empty content match missing Content-Type
m := r.Method
if m == "GET" || m == "HEAD" || m == "OPTIONS" || m == "DELETE" || m == "TRACE" {
return true
// if route specifies less or non-idempotent methods then use that
if len(r.allowedMethodsWithoutContentType) > 0 {
for _, each := range r.allowedMethodsWithoutContentType {
if m == each {
return true
}
}
} else {
if m == "GET" || m == "HEAD" || m == "OPTIONS" || m == "DELETE" || m == "TRACE" {
return true
}
}
// proceed with default
mimeTypes = MIME_OCTET
......
......@@ -17,14 +17,15 @@ import (
// RouteBuilder is a helper to construct Routes.
type RouteBuilder struct {
rootPath string
currentPath string
produces []string
consumes []string
httpMethod string // required
function RouteFunction // required
filters []FilterFunction
conditions []RouteSelectionConditionFunction
rootPath string
currentPath string
produces []string
consumes []string
httpMethod string // required
function RouteFunction // required
filters []FilterFunction
conditions []RouteSelectionConditionFunction
allowedMethodsWithoutContentType []string // see Route
typeNameHandleFunc TypeNameHandleFunction // required
......@@ -176,6 +177,15 @@ func (b *RouteBuilder) Returns(code int, message string, model interface{}) *Rou
return b
}
// ReturnsWithHeaders is similar to Returns, but can specify response headers
func (b *RouteBuilder) ReturnsWithHeaders(code int, message string, model interface{}, headers map[string]Header) *RouteBuilder {
b.Returns(code, message, model)
err := b.errorMap[code]
err.Headers = headers
b.errorMap[code] = err
return b
}
// DefaultReturns is a special Returns call that sets the default of the response.
func (b *RouteBuilder) DefaultReturns(message string, model interface{}) *RouteBuilder {
b.defaultResponse = &ResponseError{
......@@ -200,14 +210,41 @@ func (b *RouteBuilder) Deprecate() *RouteBuilder {
return b
}
// AllowedMethodsWithoutContentType overides the default list GET,HEAD,OPTIONS,DELETE,TRACE
// If a request does not include a content-type header then
// depending on the method, it may return a 415 Unsupported Media.
// Must have uppercase HTTP Method names such as GET,HEAD,OPTIONS,...
func (b *RouteBuilder) AllowedMethodsWithoutContentType(methods []string) *RouteBuilder {
b.allowedMethodsWithoutContentType = methods
return b
}
// ResponseError represents a response; not necessarily an error.
type ResponseError struct {
Code int
Message string
Model interface{}
Headers map[string]Header
IsDefault bool
}
// Header describes a header for a response of the API
//
// For more information: http://goo.gl/8us55a#headerObject
type Header struct {
*Items
Description string
}
// Items describe swagger simple schemas for headers
type Items struct {
Type string
Format string
Items *Items
CollectionFormat string
Default interface{}
}
func (b *RouteBuilder) servicePath(path string) *RouteBuilder {
b.rootPath = path
return b
......@@ -276,26 +313,27 @@ func (b *RouteBuilder) Build() Route {
operationName = nameOfFunction(b.function)
}
route := Route{
Method: b.httpMethod,
Path: concatPath(b.rootPath, b.currentPath),
Produces: b.produces,
Consumes: b.consumes,
Function: b.function,
Filters: b.filters,
If: b.conditions,
relativePath: b.currentPath,
pathExpr: pathExpr,
Doc: b.doc,
Notes: b.notes,
Operation: operationName,
ParameterDocs: b.parameters,
ResponseErrors: b.errorMap,
DefaultResponse: b.defaultResponse,
ReadSample: b.readSample,
WriteSample: b.writeSample,
Metadata: b.metadata,
Deprecated: b.deprecated,
contentEncodingEnabled: b.contentEncodingEnabled,
Method: b.httpMethod,
Path: concatPath(b.rootPath, b.currentPath),
Produces: b.produces,
Consumes: b.consumes,
Function: b.function,
Filters: b.filters,
If: b.conditions,
relativePath: b.currentPath,
pathExpr: pathExpr,
Doc: b.doc,
Notes: b.notes,
Operation: operationName,
ParameterDocs: b.parameters,
ResponseErrors: b.errorMap,
DefaultResponse: b.defaultResponse,
ReadSample: b.readSample,
WriteSample: b.writeSample,
Metadata: b.metadata,
Deprecated: b.deprecated,
contentEncodingEnabled: b.contentEncodingEnabled,
allowedMethodsWithoutContentType: b.allowedMethodsWithoutContentType,
}
route.postBuild()
return route
......
......@@ -4,12 +4,16 @@ package restful
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import "fmt"
import (
"fmt"
"net/http"
)
// ServiceError is a transport object to pass information about a non-Http error occurred in a WebService while processing a request.
type ServiceError struct {
Code int
Message string
Header http.Header
}
// NewError returns a ServiceError using the code and reason
......@@ -17,6 +21,11 @@ func NewError(code int, message string) ServiceError {
return ServiceError{Code: code, Message: message}
}
// NewErrorWithHeader returns a ServiceError using the code, reason and header
func NewErrorWithHeader(code int, message string, header http.Header) ServiceError {
return ServiceError{Code: code, Message: message, Header: header}
}
// Error returns a text representation of the service error
func (s ServiceError) Error() string {
return fmt.Sprintf("[ServiceError:%v] %v", s.Code, s.Message)
......
......@@ -188,7 +188,7 @@ func (w *WebService) RemoveRoute(path, method string) error {
continue
}
newRoutes[current] = w.routes[ix]
current = current + 1
current++
}
w.routes = newRoutes
return nil
......
......@@ -134,9 +134,9 @@ github.com/elastic/go-elasticsearch/v7
github.com/elastic/go-elasticsearch/v7/esapi
github.com/elastic/go-elasticsearch/v7/estransport
github.com/elastic/go-elasticsearch/v7/internal/version
# github.com/emicklei/go-restful v2.11.1+incompatible => github.com/emicklei/go-restful v2.9.5+incompatible
# github.com/emicklei/go-restful v2.14.3+incompatible => github.com/emicklei/go-restful v2.14.3+incompatible
github.com/emicklei/go-restful
# github.com/emicklei/go-restful-openapi v1.0.0 => github.com/emicklei/go-restful-openapi v1.0.0
# github.com/emicklei/go-restful-openapi v1.4.1 => github.com/emicklei/go-restful-openapi v1.4.1
github.com/emicklei/go-restful-openapi
github.com/emicklei/go-restful/log
# github.com/emirpasic/gods v1.12.0 => github.com/emirpasic/gods v1.12.0
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册