From a6637f18c8676abd5f6795805af756592da15121 Mon Sep 17 00:00:00 2001 From: Javier Adriel Date: Fri, 10 Feb 2023 13:48:47 -0600 Subject: [PATCH] Add fields to read json metadata (#2652) --- models/release_author.go | 118 ++++++++ models/release_info.go | 63 ++++- models/release_metadata.go | 166 +++++++++++ restapi/admin_releases.go | 29 +- restapi/admin_releases_test.go | 2 +- restapi/embedded_spec.go | 266 +++++++++++++++++- restapi/operations/release/list_releases.go | 2 +- .../release/list_releases_parameters.go | 54 ++++ .../release/list_releases_urlbuilder.go | 20 +- swagger-console.yml | 88 +++++- 10 files changed, 773 insertions(+), 35 deletions(-) create mode 100644 models/release_author.go create mode 100644 models/release_metadata.go diff --git a/models/release_author.go b/models/release_author.go new file mode 100644 index 000000000..72ce852fd --- /dev/null +++ b/models/release_author.go @@ -0,0 +1,118 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2023 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// ReleaseAuthor release author +// +// swagger:model releaseAuthor +type ReleaseAuthor struct { + + // avatar url + AvatarURL string `json:"avatar_url,omitempty"` + + // events url + EventsURL string `json:"events_url,omitempty"` + + // followers url + FollowersURL string `json:"followers_url,omitempty"` + + // following url + FollowingURL string `json:"following_url,omitempty"` + + // gists url + GistsURL string `json:"gists_url,omitempty"` + + // gravatar id + GravatarID string `json:"gravatar_id,omitempty"` + + // html url + HTMLURL string `json:"html_url,omitempty"` + + // id + ID int64 `json:"id,omitempty"` + + // login + Login string `json:"login,omitempty"` + + // node id + NodeID string `json:"node_id,omitempty"` + + // organizations url + OrganizationsURL string `json:"organizations_url,omitempty"` + + // received events url + ReceivedEventsURL string `json:"receivedEvents_url,omitempty"` + + // repos url + ReposURL string `json:"repos_url,omitempty"` + + // site admin + SiteAdmin bool `json:"site_admin,omitempty"` + + // starred url + StarredURL string `json:"starred_url,omitempty"` + + // subscriptions url + SubscriptionsURL string `json:"subscriptions_url,omitempty"` + + // type + Type string `json:"type,omitempty"` + + // url + URL string `json:"url,omitempty"` +} + +// Validate validates this release author +func (m *ReleaseAuthor) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this release author based on context it is used +func (m *ReleaseAuthor) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *ReleaseAuthor) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ReleaseAuthor) UnmarshalBinary(b []byte) error { + var res ReleaseAuthor + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/release_info.go b/models/release_info.go index 8d5f7a31e..aa84bae7a 100644 --- a/models/release_info.go +++ b/models/release_info.go @@ -25,6 +25,7 @@ package models import ( "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) @@ -40,8 +41,8 @@ type ReleaseInfo struct { // context content ContextContent string `json:"contextContent,omitempty"` - // name - Name string `json:"name,omitempty"` + // metadata + Metadata *ReleaseMetadata `json:"metadata,omitempty"` // new features content NewFeaturesContent string `json:"newFeaturesContent,omitempty"` @@ -51,18 +52,68 @@ type ReleaseInfo struct { // security content SecurityContent string `json:"securityContent,omitempty"` - - // tag - Tag string `json:"tag,omitempty"` } // Validate validates this release info func (m *ReleaseInfo) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateMetadata(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } return nil } -// ContextValidate validates this release info based on context it is used +func (m *ReleaseInfo) validateMetadata(formats strfmt.Registry) error { + if swag.IsZero(m.Metadata) { // not required + return nil + } + + if m.Metadata != nil { + if err := m.Metadata.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("metadata") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("metadata") + } + return err + } + } + + return nil +} + +// ContextValidate validate this release info based on the context it is used func (m *ReleaseInfo) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateMetadata(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ReleaseInfo) contextValidateMetadata(ctx context.Context, formats strfmt.Registry) error { + + if m.Metadata != nil { + if err := m.Metadata.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("metadata") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("metadata") + } + return err + } + } + return nil } diff --git a/models/release_metadata.go b/models/release_metadata.go new file mode 100644 index 000000000..68364ff4c --- /dev/null +++ b/models/release_metadata.go @@ -0,0 +1,166 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2023 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// ReleaseMetadata release metadata +// +// swagger:model releaseMetadata +type ReleaseMetadata struct { + + // assets url + AssetsURL string `json:"assets_url,omitempty"` + + // author + Author *ReleaseAuthor `json:"author,omitempty"` + + // created at + CreatedAt string `json:"created_at,omitempty"` + + // draft + Draft bool `json:"draft,omitempty"` + + // html url + HTMLURL string `json:"html_url,omitempty"` + + // id + ID int64 `json:"id,omitempty"` + + // name + Name string `json:"name,omitempty"` + + // node id + NodeID string `json:"node_id,omitempty"` + + // prerelease + Prerelease bool `json:"prerelease,omitempty"` + + // published at + PublishedAt string `json:"published_at,omitempty"` + + // tag name + TagName string `json:"tag_name,omitempty"` + + // tarball url + TarballURL string `json:"tarball_url,omitempty"` + + // target commitish + TargetCommitish string `json:"target_commitish,omitempty"` + + // upload url + UploadURL string `json:"upload_url,omitempty"` + + // url + URL string `json:"url,omitempty"` + + // zipball url + ZipballURL string `json:"zipball_url,omitempty"` +} + +// Validate validates this release metadata +func (m *ReleaseMetadata) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAuthor(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ReleaseMetadata) validateAuthor(formats strfmt.Registry) error { + if swag.IsZero(m.Author) { // not required + return nil + } + + if m.Author != nil { + if err := m.Author.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("author") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("author") + } + return err + } + } + + return nil +} + +// ContextValidate validate this release metadata based on the context it is used +func (m *ReleaseMetadata) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateAuthor(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ReleaseMetadata) contextValidateAuthor(ctx context.Context, formats strfmt.Registry) error { + + if m.Author != nil { + if err := m.Author.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("author") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("author") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ReleaseMetadata) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ReleaseMetadata) UnmarshalBinary(b []byte) error { + var res ReleaseMetadata + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/restapi/admin_releases.go b/restapi/admin_releases.go index ebe0d75a5..f1a4af288 100644 --- a/restapi/admin_releases.go +++ b/restapi/admin_releases.go @@ -53,18 +53,24 @@ func GetReleaseListResponse(session *models.Principal, params release.ListReleas if params.Current != nil { currentRelease = *params.Current } - return releaseList(ctx, repo, currentRelease) + search := "" + if params.Search != nil { + search = *params.Search + } + filter := "" + if params.Filter != nil { + filter = *params.Filter + } + return releaseList(ctx, repo, currentRelease, search, filter) } -func releaseList(ctx context.Context, repo, currentRelease string) (*models.ReleaseListResponse, *models.Error) { +func releaseList(ctx context.Context, repo, currentRelease, search, filter string) (*models.ReleaseListResponse, *models.Error) { serviceURL := getReleaseServiceURL() - releases, err := getReleases(serviceURL, repo, currentRelease) + releases, err := getReleases(serviceURL, repo, currentRelease, search, filter) if err != nil { return nil, ErrorWithContext(ctx, err) } - return &models.ReleaseListResponse{ - Results: releases, - }, nil + return releases, nil } func getReleaseServiceURL() string { @@ -72,12 +78,16 @@ func getReleaseServiceURL() string { return fmt.Sprintf("%s/releases", host) } -func getReleases(url, repo, currentRelease string) ([]*models.ReleaseInfo, error) { +func getReleases(url, repo, currentRelease, search, filter string) (*models.ReleaseListResponse, error) { + rl := &models.ReleaseListResponse{} client := &http.Client{Timeout: time.Second * 5} req, err := http.NewRequest("GET", url, nil) q := req.URL.Query() q.Add("repo", repo) q.Add("current", currentRelease) + q.Add("search", search) + q.Add("filter", filter) + req.URL.RawQuery = q.Encode() if err != nil { return nil, err } @@ -90,10 +100,9 @@ func getReleases(url, repo, currentRelease string) ([]*models.ReleaseInfo, error if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("error getting releases: %s", resp.Status) } - var releases []*models.ReleaseInfo - err = json.NewDecoder(resp.Body).Decode(&releases) + err = json.NewDecoder(resp.Body).Decode(&rl) if err != nil { return nil, err } - return releases, nil + return rl, nil } diff --git a/restapi/admin_releases_test.go b/restapi/admin_releases_test.go index ea3f8a379..506e593ca 100644 --- a/restapi/admin_releases_test.go +++ b/restapi/admin_releases_test.go @@ -62,7 +62,7 @@ func (suite *ReleasesTestSuite) getHandler( w.WriteHeader(400) } else { w.WriteHeader(200) - response := []*models.ReleaseListResponse{{}} + response := &models.ReleaseListResponse{} bytes, _ := json.Marshal(response) fmt.Fprint(w, string(bytes)) } diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index ee6315aec..5f43ff67d 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -4056,7 +4056,7 @@ func init() { } } }, - "/releases/": { + "/releases": { "get": { "tags": [ "release" @@ -4076,6 +4076,18 @@ func init() { "description": "Current Release", "name": "current", "in": "query" + }, + { + "type": "string", + "description": "search content", + "name": "search", + "in": "query" + }, + { + "type": "string", + "description": "filter releases", + "name": "filter", + "in": "query" } ], "responses": { @@ -7424,6 +7436,65 @@ func init() { } } }, + "releaseAuthor": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "events_url": { + "type": "string" + }, + "followers_url": { + "type": "string" + }, + "following_url": { + "type": "string" + }, + "gists_url": { + "type": "string" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string" + }, + "receivedEvents_url": { + "type": "string" + }, + "repos_url": { + "type": "string" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string" + }, + "subscriptions_url": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, "releaseInfo": { "type": "object", "properties": { @@ -7433,8 +7504,8 @@ func init() { "contextContent": { "type": "string" }, - "name": { - "type": "string" + "metadata": { + "$ref": "#/definitions/releaseMetadata" }, "newFeaturesContent": { "type": "string" @@ -7444,9 +7515,6 @@ func init() { }, "securityContent": { "type": "string" - }, - "tag": { - "type": "string" } } }, @@ -7461,6 +7529,59 @@ func init() { } } }, + "releaseMetadata": { + "type": "object", + "properties": { + "assets_url": { + "type": "string" + }, + "author": { + "$ref": "#/definitions/releaseAuthor" + }, + "created_at": { + "type": "string" + }, + "draft": { + "type": "boolean" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "prerelease": { + "type": "boolean" + }, + "published_at": { + "type": "string" + }, + "tag_name": { + "type": "string" + }, + "tarball_url": { + "type": "string" + }, + "target_commitish": { + "type": "string" + }, + "upload_url": { + "type": "string" + }, + "url": { + "type": "string" + }, + "zipball_url": { + "type": "string" + } + } + }, "remoteBucket": { "type": "object", "required": [ @@ -12628,7 +12749,7 @@ func init() { } } }, - "/releases/": { + "/releases": { "get": { "tags": [ "release" @@ -12648,6 +12769,18 @@ func init() { "description": "Current Release", "name": "current", "in": "query" + }, + { + "type": "string", + "description": "search content", + "name": "search", + "in": "query" + }, + { + "type": "string", + "description": "filter releases", + "name": "filter", + "in": "query" } ], "responses": { @@ -16122,6 +16255,65 @@ func init() { } } }, + "releaseAuthor": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "events_url": { + "type": "string" + }, + "followers_url": { + "type": "string" + }, + "following_url": { + "type": "string" + }, + "gists_url": { + "type": "string" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string" + }, + "receivedEvents_url": { + "type": "string" + }, + "repos_url": { + "type": "string" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string" + }, + "subscriptions_url": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, "releaseInfo": { "type": "object", "properties": { @@ -16131,8 +16323,8 @@ func init() { "contextContent": { "type": "string" }, - "name": { - "type": "string" + "metadata": { + "$ref": "#/definitions/releaseMetadata" }, "newFeaturesContent": { "type": "string" @@ -16142,9 +16334,6 @@ func init() { }, "securityContent": { "type": "string" - }, - "tag": { - "type": "string" } } }, @@ -16159,6 +16348,59 @@ func init() { } } }, + "releaseMetadata": { + "type": "object", + "properties": { + "assets_url": { + "type": "string" + }, + "author": { + "$ref": "#/definitions/releaseAuthor" + }, + "created_at": { + "type": "string" + }, + "draft": { + "type": "boolean" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "prerelease": { + "type": "boolean" + }, + "published_at": { + "type": "string" + }, + "tag_name": { + "type": "string" + }, + "tarball_url": { + "type": "string" + }, + "target_commitish": { + "type": "string" + }, + "upload_url": { + "type": "string" + }, + "url": { + "type": "string" + }, + "zipball_url": { + "type": "string" + } + } + }, "remoteBucket": { "type": "object", "required": [ diff --git a/restapi/operations/release/list_releases.go b/restapi/operations/release/list_releases.go index bbdaef2b6..bb2e55b1a 100644 --- a/restapi/operations/release/list_releases.go +++ b/restapi/operations/release/list_releases.go @@ -49,7 +49,7 @@ func NewListReleases(ctx *middleware.Context, handler ListReleasesHandler) *List } /* - ListReleases swagger:route GET /releases/ release listReleases + ListReleases swagger:route GET /releases release listReleases Get repo releases for a given version */ diff --git a/restapi/operations/release/list_releases_parameters.go b/restapi/operations/release/list_releases_parameters.go index 3d3e9e335..38fed7b96 100644 --- a/restapi/operations/release/list_releases_parameters.go +++ b/restapi/operations/release/list_releases_parameters.go @@ -53,11 +53,19 @@ type ListReleasesParams struct { In: query */ Current *string + /*filter releases + In: query + */ + Filter *string /*repo name Required: true In: query */ Repo string + /*search content + In: query + */ + Search *string } // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface @@ -76,10 +84,20 @@ func (o *ListReleasesParams) BindRequest(r *http.Request, route *middleware.Matc res = append(res, err) } + qFilter, qhkFilter, _ := qs.GetOK("filter") + if err := o.bindFilter(qFilter, qhkFilter, route.Formats); err != nil { + res = append(res, err) + } + qRepo, qhkRepo, _ := qs.GetOK("repo") if err := o.bindRepo(qRepo, qhkRepo, route.Formats); err != nil { res = append(res, err) } + + qSearch, qhkSearch, _ := qs.GetOK("search") + if err := o.bindSearch(qSearch, qhkSearch, route.Formats); err != nil { + res = append(res, err) + } if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -104,6 +122,24 @@ func (o *ListReleasesParams) bindCurrent(rawData []string, hasKey bool, formats return nil } +// bindFilter binds and validates parameter Filter from query. +func (o *ListReleasesParams) bindFilter(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + o.Filter = &raw + + return nil +} + // bindRepo binds and validates parameter Repo from query. func (o *ListReleasesParams) bindRepo(rawData []string, hasKey bool, formats strfmt.Registry) error { if !hasKey { @@ -124,3 +160,21 @@ func (o *ListReleasesParams) bindRepo(rawData []string, hasKey bool, formats str return nil } + +// bindSearch binds and validates parameter Search from query. +func (o *ListReleasesParams) bindSearch(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + o.Search = &raw + + return nil +} diff --git a/restapi/operations/release/list_releases_urlbuilder.go b/restapi/operations/release/list_releases_urlbuilder.go index 6f56f4816..ed8ea272b 100644 --- a/restapi/operations/release/list_releases_urlbuilder.go +++ b/restapi/operations/release/list_releases_urlbuilder.go @@ -31,7 +31,9 @@ import ( // ListReleasesURL generates an URL for the list releases operation type ListReleasesURL struct { Current *string + Filter *string Repo string + Search *string _basePath string // avoid unkeyed usage @@ -57,7 +59,7 @@ func (o *ListReleasesURL) SetBasePath(bp string) { func (o *ListReleasesURL) Build() (*url.URL, error) { var _result url.URL - var _path = "/releases/" + var _path = "/releases" _basePath := o._basePath if _basePath == "" { @@ -75,11 +77,27 @@ func (o *ListReleasesURL) Build() (*url.URL, error) { qs.Set("current", currentQ) } + var filterQ string + if o.Filter != nil { + filterQ = *o.Filter + } + if filterQ != "" { + qs.Set("filter", filterQ) + } + repoQ := o.Repo if repoQ != "" { qs.Set("repo", repoQ) } + var searchQ string + if o.Search != nil { + searchQ = *o.Search + } + if searchQ != "" { + qs.Set("search", searchQ) + } + _result.RawQuery = qs.Encode() return &_result, nil diff --git a/swagger-console.yml b/swagger-console.yml index 3dc1433d3..0c6ffd61c 100644 --- a/swagger-console.yml +++ b/swagger-console.yml @@ -3432,6 +3432,14 @@ paths: description: Current Release in: query type: string + - name: search + description: search content + in: query + type: string + - name: filter + description: filter releases + in: query + type: string responses: 200: description: A successful response. @@ -5842,10 +5850,8 @@ definitions: releaseInfo: type: object properties: - tag: - type: string - name: - type: string + metadata: + $ref: "#/definitions/releaseMetadata" notesContent: type: string securityContent: @@ -5856,3 +5862,77 @@ definitions: type: string newFeaturesContent: type: string + releaseMetadata: + type: object + properties: + tag_name: + type: string + target_commitish: + type: string + name: + type: string + draft: + type: boolean + prerelease: + type: boolean + id: + type: integer + created_at: + type: string + published_at: + type: string + url: + type: string + html_url: + type: string + assets_url: + type: string + upload_url: + type: string + zipball_url: + type: string + tarball_url: + type: string + author: + $ref: "#/definitions/releaseAuthor" + node_id: + type: string + releaseAuthor: + type: object + properties: + login: + type: string + id: + type: integer + node_id: + type: string + avatar_url: + type: string + html_url: + type: string + gravatar_id: + type: string + type: + type: string + site_admin: + type: boolean + url: + type: string + events_url: + type: string + following_url: + type: string + followers_url: + type: string + gists_url: + type: string + organizations_url: + type: string + receivedEvents_url: + type: string + repos_url: + type: string + starred_url: + type: string + subscriptions_url: + type: string \ No newline at end of file