├── .cobra.yml ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .golangci.yml ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── cmd.go ├── cmd_test.go └── testdata │ └── submit_dryrun_out.golden ├── go.mod ├── go.sum ├── main.go ├── openapi-generator-config.yml ├── pkg ├── article │ ├── article.go │ ├── article_test.go │ ├── client.go │ ├── client_test.go │ ├── frontmatter.go │ ├── frontmatter_test.go │ ├── mock │ │ └── mock_client.go │ └── testdata │ │ ├── empty.md │ │ ├── real_article.golden │ │ ├── real_article.md │ │ ├── testdata.md │ │ └── testdata_nocoverimage.md ├── config │ ├── config.go │ ├── config_test.go │ └── testdata │ │ ├── bad.yml │ │ ├── devto.yml │ │ └── empty.yml ├── devto │ ├── .gitignore │ ├── .openapi-generator-ignore │ ├── .openapi-generator │ │ └── VERSION │ ├── .travis.yml │ ├── README.md │ ├── api │ │ └── openapi.yaml │ ├── api_articles.go │ ├── api_users.go │ ├── api_webhooks.go │ ├── client.go │ ├── configuration.go │ ├── docs │ │ ├── ApiError.md │ │ ├── ArticleCreate.md │ │ ├── ArticleCreateArticle.md │ │ ├── ArticleFlareTag.md │ │ ├── ArticleIndex.md │ │ ├── ArticleMe.md │ │ ├── ArticleOrganization.md │ │ ├── ArticleShow.md │ │ ├── ArticleUpdate.md │ │ ├── ArticleUpdateArticle.md │ │ ├── ArticleUser.md │ │ ├── ArticlesApi.md │ │ ├── UsersApi.md │ │ ├── WebhookCreate.md │ │ ├── WebhookCreateWebhookEndpoint.md │ │ ├── WebhookIndex.md │ │ ├── WebhookShow.md │ │ └── WebhooksApi.md │ ├── model_api_error.go │ ├── model_article_create.go │ ├── model_article_create_article.go │ ├── model_article_flare_tag.go │ ├── model_article_index.go │ ├── model_article_me.go │ ├── model_article_organization.go │ ├── model_article_show.go │ ├── model_article_update.go │ ├── model_article_update_article.go │ ├── model_article_user.go │ ├── model_webhook_create.go │ ├── model_webhook_create_webhook_endpoint.go │ ├── model_webhook_index.go │ ├── model_webhook_show.go │ └── response.go └── links │ ├── links.go │ ├── links_test.go │ ├── root.go │ └── root_test.go └── tools.go /.cobra.yml: -------------------------------------------------------------------------------- 1 | author: Shi Han NG 2 | license: MIT 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | pull_request: 7 | branches: 8 | - develop 9 | 10 | jobs: 11 | tests: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up Go 15 | uses: actions/setup-go@v1 16 | with: 17 | go-version: 1.13.x 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v1 21 | 22 | - name: Run tests 23 | run: make test 24 | 25 | - name: Send coverage 26 | env: 27 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: | 29 | GO111MODULE=off go get github.com/mattn/goveralls 30 | $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github 31 | 32 | lints: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Check out code into the Go module directory 36 | uses: actions/checkout@v1 37 | 38 | - name: Set up golangci-lint 39 | uses: "actions-contrib/golangci-lint@master" 40 | with: 41 | args: "run -v" 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "**" 7 | tags: 8 | - "v*.*.*" 9 | 10 | jobs: 11 | goreleaser: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v1 16 | - name: Set up Go 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: 1.13.x 20 | - name: Login to DockerHub 21 | uses: docker/login-action@v1 22 | with: 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_PAT }} 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v1 27 | with: 28 | version: latest 29 | args: release --rm-dist 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - pkg/devto 4 | 5 | linters: 6 | enable-all: true 7 | 8 | issues: 9 | exclude-rules: 10 | - path: _test\.go 11 | linters: 12 | - scopelint 13 | text: Using the variable on range scope `(tc|tt)` in function literal 14 | 15 | # Usually we have table driven tests which make the test function too long. 16 | - path: _test\.go 17 | linters: 18 | - funlen 19 | text: Function '.+' is too long 20 | 21 | - path: cmd.go 22 | linters: 23 | - gomnd 24 | text: "Magic number: \\d+, in detected" 25 | 26 | # Closed in github.com/shihanng/devto/pkg/devto 27 | - path: pkg/article/client.go 28 | linters: 29 | - bodyclose 30 | text: "response body must be closed" 31 | 32 | # Generated in github.com/shihanng/devto/pkg/devto 33 | - path: pkg/article/client.go 34 | linters: 35 | - lll 36 | text: "line is 121 characters" 37 | 38 | - path: _test\.go 39 | linters: 40 | - gomnd 41 | 42 | - path: cmd/cmd.go 43 | linters: 44 | - funlen 45 | text: "Function 'New' is too long" 46 | 47 | - path: main.go 48 | linters: 49 | - gomnd 50 | text: "mnd: Magic number: 1, in detected" 51 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | - CGO_ENABLED=0 4 | 5 | builds: 6 | - goos: 7 | - darwin 8 | - windows 9 | - linux 10 | goarch: 11 | - amd64 12 | - "386" 13 | ldflags: 14 | - -s -w -X main.version={{.Version}} 15 | ignore: 16 | - goos: darwin 17 | goarch: "386" 18 | 19 | archives: 20 | - format: tar.gz 21 | name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 22 | files: 23 | - LICENSE* 24 | - README* 25 | 26 | brews: 27 | - tap: 28 | owner: shihanng 29 | name: homebrew-devto 30 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" 31 | commit_author: 32 | name: "Shi Han NG" 33 | email: shihanng@gmail.com 34 | description: "CLI tool to publish article to https://dev.to/" 35 | homepage: "https://github.com/shihanng/devto" 36 | install: | 37 | bin.install Dir['devto'] 38 | test: | 39 | system "#{bin}/devto" 40 | nfpms: 41 | - homepage: https://github.com/shihanng/devto 42 | maintainer: Shi Han NG 43 | file_name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 44 | description: "CLI tool to publish article to https://dev.to/" 45 | bindir: /usr/local/bin 46 | license: MIT 47 | formats: 48 | - deb 49 | - rpm 50 | 51 | dockers: 52 | - goos: linux 53 | goarch: amd64 54 | binaries: 55 | - devto 56 | image_templates: 57 | - "shihanng/devto:latest" 58 | - "shihanng/devto:{{ .Tag }}" 59 | 60 | checksum: 61 | name_template: "{{ .ProjectName }}_checksums.txt" 62 | 63 | snapshot: 64 | name_template: "{{ .Tag }}-next" 65 | 66 | changelog: 67 | skip: true 68 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY devto / 3 | ENTRYPOINT ["/devto"] 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2019 Shi Han NG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LDFLAGS := -ldflags="-s -w" 2 | 3 | generator_version = v4.2.2 4 | api_doc = https://raw.githubusercontent.com/thepracticaldev/dev.to/7d0aeeefe5cf6250c5b58ae6b631bfe41fe5bf4a/docs/api_v0.yml 5 | 6 | .PHONY: test gen lint mod-check install 7 | 8 | help: 9 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 10 | 11 | test: ## Run tests 12 | go test -race -covermode atomic -coverprofile=profile.cov -v ./... -count=1 13 | 14 | gen: 15 | docker run --rm \ 16 | --user "$$(id -u):$$(id -g)" \ 17 | -v $(PWD):/local openapitools/openapi-generator-cli:$(generator_version) generate \ 18 | -i $(api_doc) \ 19 | -g go \ 20 | -o /local/pkg/devto \ 21 | -c /local/openapi-generator-config.yml && \ 22 | gofmt -w pkg/devto/.. 23 | 24 | lint: ## Run GolangCI-Lint 25 | docker run --rm -v $$(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -v 26 | 27 | mod-check: ## Run check on go mod tidy 28 | go mod tidy && git --no-pager diff --exit-code -- go.mod go.sum 29 | 30 | devto: ## Build devto binary 31 | go build $(LDFLAGS) -o devto 32 | 33 | install: ## Install devto into $GOBIN 34 | go install $(LDFLAGS) 35 | 36 | clean: ## Cleanup 37 | rm devto 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # devto -- publish to [dev.to](https://dev.to) from your terminal 2 | 3 | [![CI](https://github.com/shihanng/devto/workflows/main/badge.svg?branch=develop)](https://github.com/shihanng/devto/actions?query=workflow%3Amain) 4 | [![Release](https://github.com/shihanng/devto/workflows/release/badge.svg)](https://github.com/shihanng/devto/actions?query=workflow%3Arelease) 5 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/shihanng/devto)](https://github.com/shihanng/devto/releases) 6 | [![Coverage Status](https://coveralls.io/repos/github/shihanng/devto/badge.svg?branch=develop)](https://coveralls.io/github/shihanng/devto?branch=develop) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/shihanng/devto)](https://goreportcard.com/report/github.com/shihanng/devto) 8 | [![GitHub](https://img.shields.io/github/license/shihanng/devto)](./LICENSE) 9 | 10 | ## What is this? 11 | 12 | `devto` is a CLI tool that helps submit articles to DEV from the terminal. It makes use of the [APIs that DEV kindly provides in OpenAPI specification](https://docs.dev.to/api/). `devto` mainly does two things: 13 | 14 | 1. It collects all image links from the Markdown file into a `devto.yml` file with the `generate` subcommand. For example, if we have `./image-1.png` and `./image-2.png` in the Markdown file, we will get the following: 15 | 16 | ```yml 17 | images: 18 | ./image-1.png: "" 19 | ./image-2.png: "" 20 | ``` 21 | 22 | 2. It submits the article to DEV with the `submit` subcommand. The `submit` subcommand creates a new article in DEV and updates the `devto.yml` with the resulting `article_id`. `devto` will use this `article_id` in the following execution to perform an update operation instead of creating a new entry for the same article. 23 | 24 | The DEV API does not have a way of uploading images yet. If we submit a Markdown content with relative paths of image links, DEV will not be able to show those images. As a workaround of this problem, we need to provide a full path for the images either manually via the `devto.yml` file or using the `--prefix` flag. 25 | 26 | The Markdown file must contains at least the title property of the Jekyll front matter, like in: 27 | 28 | ``` 29 | --- 30 | title: An example title 31 | description: ... 32 | tags: ... 33 | cover_image: ... 34 | --- 35 | ``` 36 | 37 | You can find more information about the usage via the `--help` flag. 38 | 39 | ```sh 40 | devto --help 41 | ``` 42 | 43 | ## Installation 44 | 45 | ### [Homebrew (macOS)](https://github.com/shihanng/homebrew-devto) 46 | 47 | ```sh 48 | brew install shihanng/devto/devto 49 | ``` 50 | 51 | ### Debian, Ubuntu 52 | 53 | ```sh 54 | curl -sLO https://github.com/shihanng/devto/releases/latest/download/devto_linux_amd64.deb 55 | dpkg -i devto_linux_amd64.deb 56 | ``` 57 | 58 | ### RedHat, CentOS 59 | 60 | ```sh 61 | rpm -ivh https://github.com/shihanng/devto/releases/latest/download/devto_linux_amd64.rpm 62 | ``` 63 | 64 | ### Binaries 65 | 66 | The [release page](https://github.com/shihanng/devto/releases) contains binaries built for various platforms. Download the version matches your environment (e.g. `linux_amd64`) and place the binary in the executable `$PATH` e.g. `/usr/local/bin`: 67 | 68 | ```sh 69 | curl -sL https://github.com/shihanng/devto/releases/latest/download/devto_linux_amd64.tar.gz | \ 70 | tar xz -C /usr/local/bin/ devto 71 | ``` 72 | 73 | ### For Gophers 74 | 75 | With [Go](https://golang.org/doc/install) already installed in your system, use `go get` 76 | 77 | ```sh 78 | go get github.com/shihanng/devto 79 | ``` 80 | 81 | or clone this repo and `make install` 82 | 83 | ```sh 84 | git clone https://github.com/shihanng/devto.git 85 | cd devto 86 | make install 87 | ``` 88 | 89 | ## Configuration 90 | 91 | | Description | CLI Flag | Environment Variable | `config.yml` | 92 | | ---------------------------------------------------------------------------------------------- | ----------- | -------------------- | ------------ | 93 | | [DEV API key](https://docs.dev.to/api/#section/Authentication) is needed to talk with DEV API. | `--api-key` | `DEVTO_API_KEY` | `api-key` | 94 | 95 | ### Sample config in YAML 96 | 97 | ```yaml 98 | api-key: abcd1234 99 | ``` 100 | 101 | ## Contributing 102 | 103 | Want to add missing feature? Found bug :bug:? Pull requests and issues are welcome. For major changes, please open an issue first to discuss what you would like to change :heart:. 104 | 105 | ```sh 106 | make lint 107 | make test 108 | ``` 109 | 110 | should help with the idiomatic Go styles and unit-tests. 111 | 112 | ### How to generate [DEV's API](https://docs.dev.to/api/) client 113 | 114 | ```sh 115 | make gen 116 | ``` 117 | 118 | See [`pkg/devto`](./pkg/devto) for client documentation. 119 | 120 | ## License 121 | 122 | [MIT](./LICENSE) 123 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/cockroachdb/errors" 11 | "github.com/shihanng/devto/pkg/article" 12 | "github.com/shihanng/devto/pkg/config" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | const ( 19 | flagAPIKey = "api-key" 20 | flagDebug = "debug" 21 | flagDryRun = "dry-run" 22 | flagForce = "force" 23 | flagPrefix = "prefix" 24 | flagPublished = "published" 25 | ) 26 | 27 | func New(out io.Writer) (*cobra.Command, func()) { 28 | viper.SetConfigName("config") 29 | viper.AddConfigPath(".") 30 | viper.SetEnvPrefix("DEVTO") 31 | viper.AutomaticEnv() 32 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 33 | 34 | r := &runner{ 35 | out: out, 36 | } 37 | 38 | listCmd := &cobra.Command{ 39 | Use: "list", 40 | Short: "List published articles (maximum 30) on dev.to", 41 | Long: `List published articles (maximum 30) on dev.to in the following format: 42 | 43 | [] 44 | `, 45 | RunE: r.listRunE, 46 | Args: cobra.ExactArgs(0), 47 | } 48 | 49 | submitCmd := &cobra.Command{ 50 | Use: "submit <Markdown file>", 51 | Short: "Submit article to dev.to", 52 | Long: `Submit an aricle to dev.to. 53 | 54 | If not exist, devto.yml config will be created on the same directory with the <Markdown file>. 55 | devto.yml has the follwing format: 56 | 57 | article_id: 1234 58 | images: 59 | "./image-1.png": "./new-image-1.png" 60 | "./image-2.png": "" 61 | 62 | If article_id (in devto.yml) is 0, new post will be created on dev.to and 63 | the resulting article id will be stored as article_id. 64 | All image links in the <Markdown file> will be replaced according to the key-value pairs 65 | in images. If the value of a key is an empty string, it will not be replaced, e.g. 66 | 67 | ![](./image-1.png) --> ![](./new-image-1.png) 68 | ![](./image-2.png) --> ![](./image-2.png) 69 | `, 70 | RunE: r.submitRunE, 71 | Args: cobra.ExactArgs(1), 72 | } 73 | submitCmd.PersistentFlags().StringP(flagPrefix, "p", "", "Prefix image links with the given value") 74 | submitCmd.PersistentFlags().Bool( 75 | flagPublished, false, "Publish article with this flag. Front matter in markdown takes precedence") 76 | submitCmd.PersistentFlags().Bool( 77 | flagDryRun, false, "Print information instead of submit to dev.to") 78 | 79 | generateCmd := &cobra.Command{ 80 | Use: "generate <Markdown file>", 81 | Short: "Genenerate a devto.yml configuration file for the <Markdown file>", 82 | RunE: r.generateRunE, 83 | Args: cobra.ExactArgs(1), 84 | } 85 | generateCmd.PersistentFlags().StringP(flagPrefix, "p", "", "Prefix image links with the given value") 86 | generateCmd.PersistentFlags().BoolP( 87 | flagForce, "f", false, "Use with -p to override existing values in the devto.yml file") 88 | 89 | rootCmd := &cobra.Command{ 90 | Use: "devto", 91 | Short: "A tool to help you publish to dev.to from your terminal", 92 | PersistentPreRunE: r.rootRunE, 93 | } 94 | 95 | rootCmd.PersistentFlags().String(flagAPIKey, "", "API key for authentication") 96 | rootCmd.PersistentFlags().BoolP(flagDebug, "d", false, "Print debug log on stderr") 97 | rootCmd.AddCommand( 98 | listCmd, 99 | submitCmd, 100 | generateCmd, 101 | ) 102 | 103 | _ = viper.BindPFlag(flagAPIKey, rootCmd.PersistentFlags().Lookup(flagAPIKey)) 104 | 105 | return rootCmd, func() { 106 | if r.log != nil { 107 | _ = r.log.Sync() 108 | } 109 | } 110 | } 111 | 112 | type runner struct { 113 | out io.Writer 114 | log *zap.SugaredLogger 115 | } 116 | 117 | func (r *runner) rootRunE(cmd *cobra.Command, args []string) error { 118 | // Setup logger 119 | logConfig := zap.NewDevelopmentConfig() 120 | 121 | isDebug, err := cmd.Parent().PersistentFlags().GetBool(flagDebug) 122 | if err != nil { 123 | return errors.Wrap(err, "cmd: get flag --debug") 124 | } 125 | 126 | if !isDebug { 127 | logConfig.Level = zap.NewAtomicLevelAt(zap.InfoLevel) 128 | } 129 | 130 | logger, err := logConfig.Build() 131 | if err != nil { 132 | return errors.Wrap(err, "cmd: create new logger") 133 | } 134 | 135 | r.log = logger.Sugar() 136 | 137 | if err := viper.ReadInConfig(); err != nil { 138 | if !errors.As(err, &viper.ConfigFileNotFoundError{}) { 139 | return errors.Wrap(err, "cmd: read config") 140 | } 141 | } 142 | 143 | config := struct { 144 | APIKey string `mapstructure:"api-key"` 145 | }{} 146 | 147 | if err := viper.Unmarshal(&config); err != nil { 148 | return errors.Wrap(err, "cmd: unmarshal config") 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func (r *runner) listRunE(cmd *cobra.Command, args []string) error { 155 | client, err := article.NewClient(viper.GetString(flagAPIKey)) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | return client.ListArticle(os.Stdout) 161 | } 162 | 163 | func (r *runner) submitRunE(cmd *cobra.Command, args []string) error { 164 | filename := args[0] 165 | 166 | prefix, err := cmd.PersistentFlags().GetString(flagPrefix) 167 | if err != nil { 168 | return errors.Wrap(err, "cmd: fail to get prefix flag") 169 | } 170 | 171 | cfg, err := config.New(configFrom(filename)) 172 | if err != nil { 173 | return err 174 | } 175 | 176 | dryRun, err := cmd.PersistentFlags().GetBool(flagDryRun) 177 | if err != nil { 178 | return errors.Wrap(err, "cmd: fail to get force flag") 179 | } 180 | 181 | client, err := article.NewClient(viper.GetString(flagAPIKey), article.SetConfig(cfg)) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | published, err := cmd.PersistentFlags().GetBool(flagPublished) 187 | if err != nil { 188 | return errors.Wrap(err, "cmd: fail to get published flag") 189 | } 190 | 191 | if dryRun { 192 | submitArticleDryRun(r.out, cfg, filename, published, prefix) 193 | return nil 194 | } 195 | 196 | return client.SubmitArticle(filename, published, prefix) 197 | } 198 | 199 | func (r *runner) generateRunE(cmd *cobra.Command, args []string) error { 200 | filename := args[0] 201 | 202 | prefix, err := cmd.PersistentFlags().GetString(flagPrefix) 203 | if err != nil { 204 | return errors.Wrap(err, "cmd: fail to get prefix flag") 205 | } 206 | 207 | override, err := cmd.PersistentFlags().GetBool(flagForce) 208 | if err != nil { 209 | return errors.Wrap(err, "cmd: fail to get force flag") 210 | } 211 | 212 | cfg, err := config.New(configFrom(filename)) 213 | if err != nil { 214 | return err 215 | } 216 | 217 | client, err := article.NewClient(viper.GetString(flagAPIKey), article.SetConfig(cfg)) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | return client.GenerateImageLinks(filename, prefix, override) 223 | } 224 | 225 | func configFrom(filename string) string { 226 | return filepath.Join(filepath.Dir(filename), "devto.yml") 227 | } 228 | 229 | func submitArticleDryRun(w io.Writer, cfg *config.Config, filename string, published bool, prefix string) { 230 | fmt.Fprintln(w, "This is a dry run. Remove --dry-run to submit to dev.to") 231 | fmt.Fprintln(w, "---") 232 | fmt.Fprintf(w, "Filename: %s\n", filename) 233 | fmt.Fprintf(w, "Published: %t\n", published) 234 | fmt.Fprintf(w, "Prefixed: %s\n", prefix) 235 | } 236 | -------------------------------------------------------------------------------- /cmd/cmd_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/sebdah/goldie/v2" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | const testContent = `--- 16 | title: HELLO! 17 | description: "hallo" 18 | cover_image: "./cv.jpg" 19 | --- 20 | 21 | ![lili](./image.png) 22 | ![lili](./image.png) 23 | ` 24 | 25 | func TestSubmit_DryRun(t *testing.T) { 26 | dir, err := ioutil.TempDir("", "devto-cmd-test-") 27 | require.NoError(t, err) 28 | 29 | defer os.RemoveAll(dir) 30 | 31 | tmpfn := filepath.Join(dir, "test_article.md") 32 | require.NoError(t, ioutil.WriteFile(tmpfn, []byte(testContent), 0666)) 33 | 34 | os.Args = []string{"devto", "submit", "--dry-run", "-p", "test/", tmpfn} 35 | 36 | var out bytes.Buffer 37 | 38 | cmd, sync := New(&out) 39 | defer sync() 40 | 41 | require.NoError(t, cmd.Execute()) 42 | 43 | data := struct { 44 | Dir string 45 | }{ 46 | Dir: dir, 47 | } 48 | 49 | g := goldie.New(t) 50 | g.AssertWithTemplate(t, "submit_dryrun_out", data, out.Bytes()) 51 | } 52 | 53 | func TestGenerate(t *testing.T) { 54 | dir, err := ioutil.TempDir("", "devto-cmd-test-") 55 | require.NoError(t, err) 56 | 57 | defer os.RemoveAll(dir) 58 | 59 | tmpfn := filepath.Join(dir, "test_article.md") 60 | require.NoError(t, ioutil.WriteFile(tmpfn, []byte(testContent), 0666)) 61 | 62 | os.Args = []string{"devto", "generate", tmpfn} 63 | 64 | cmd, sync := New(os.Stdout) 65 | defer sync() 66 | 67 | require.NoError(t, cmd.Execute()) 68 | 69 | actual, err := ioutil.ReadFile(filepath.Join(dir, "devto.yml")) 70 | require.NoError(t, err) 71 | 72 | expected := []byte(`images: 73 | ./cv.jpg: "" 74 | ./image.png: "" 75 | `) 76 | assert.Equal(t, expected, actual) 77 | } 78 | 79 | func TestGenerate_Prefix(t *testing.T) { 80 | dir, err := ioutil.TempDir("", "devto-cmd-test-") 81 | require.NoError(t, err) 82 | 83 | defer os.RemoveAll(dir) 84 | 85 | tmpfn := filepath.Join(dir, "test_article.md") 86 | require.NoError(t, ioutil.WriteFile(tmpfn, []byte(testContent), 0666)) 87 | 88 | os.Args = []string{"devto", "generate", "-p", "test/", tmpfn} 89 | 90 | cmd, sync := New(os.Stdout) 91 | defer sync() 92 | 93 | require.NoError(t, cmd.Execute()) 94 | 95 | actual, err := ioutil.ReadFile(filepath.Join(dir, "devto.yml")) 96 | require.NoError(t, err) 97 | 98 | expected := []byte(`images: 99 | ./cv.jpg: test/./cv.jpg 100 | ./image.png: test/./image.png 101 | `) 102 | assert.Equal(t, expected, actual) 103 | } 104 | -------------------------------------------------------------------------------- /cmd/testdata/submit_dryrun_out.golden: -------------------------------------------------------------------------------- 1 | This is a dry run. Remove --dry-run to submit to dev.to 2 | --- 3 | Filename: {{ .Dir }}/test_article.md 4 | Published: false 5 | Prefixed: test/ 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shihanng/devto 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/antihax/optional v1.0.0 7 | github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40 // indirect 8 | github.com/cockroachdb/errors v1.2.4 9 | github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f // indirect 10 | github.com/fsnotify/fsnotify v1.4.9 // indirect 11 | github.com/getsentry/raven-go v0.2.0 // indirect 12 | github.com/gogo/protobuf v1.3.1 // indirect 13 | github.com/gohugoio/hugo v0.74.3 14 | github.com/golang/mock v1.4.4 15 | github.com/golang/protobuf v1.4.2 // indirect 16 | github.com/google/go-cmp v0.5.2 // indirect 17 | github.com/magiconair/properties v1.8.2 // indirect 18 | github.com/mitchellh/mapstructure v1.3.3 19 | github.com/niklasfasching/go-org v1.3.2 // indirect 20 | github.com/pelletier/go-toml v1.8.0 // indirect 21 | github.com/sebdah/goldie/v2 v2.5.1 22 | github.com/sergi/go-diff v1.1.0 // indirect 23 | github.com/shihanng/md v0.0.0-20200117114204-0507bf4476c0 24 | github.com/spf13/afero v1.3.4 // indirect 25 | github.com/spf13/cobra v1.0.0 26 | github.com/spf13/viper v1.7.1 27 | github.com/stretchr/testify v1.6.1 28 | github.com/yuin/goldmark v1.2.1 29 | go.uber.org/zap v1.13.0 30 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect 31 | golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect 32 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d 33 | golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8 // indirect 34 | golang.org/x/tools v0.0.0-20200826040757-bc8aaaa29e06 // indirect 35 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 36 | google.golang.org/appengine v1.6.6 // indirect 37 | google.golang.org/protobuf v1.25.0 // indirect 38 | gopkg.in/ini.v1 v1.60.1 // indirect 39 | gopkg.in/src-d/go-git.v4 v4.13.1 40 | honnef.co/go/tools v0.0.1-2020.1.4 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/shihanng/devto/cmd" 7 | ) 8 | 9 | func main() { 10 | c, sync := cmd.New(os.Stdout) 11 | defer sync() 12 | 13 | if err := c.Execute(); err != nil { 14 | os.Exit(1) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /openapi-generator-config.yml: -------------------------------------------------------------------------------- 1 | packageName: "devto" 2 | isGoSubmodule: true 3 | structPrefix: true 4 | -------------------------------------------------------------------------------- /pkg/article/article.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/cockroachdb/errors" 7 | "github.com/shihanng/md" 8 | "github.com/yuin/goldmark/ast" 9 | "github.com/yuin/goldmark/parser" 10 | "github.com/yuin/goldmark/renderer" 11 | "github.com/yuin/goldmark/text" 12 | "github.com/yuin/goldmark/util" 13 | ) 14 | 15 | func SetImageLinks(filename string, images map[string]string, prefix string) (string, error) { 16 | parsed, n, err := read(filename) 17 | if err != nil { 18 | return "", err 19 | } 20 | 21 | if replace, ok := images[parsed.frontMatter.CoverImage]; ok && replace != "" { 22 | parsed.frontMatter.CoverImage = replace 23 | } else if parsed.frontMatter.CoverImage != "" { 24 | parsed.frontMatter.CoverImage = prefix + parsed.frontMatter.CoverImage 25 | } 26 | 27 | r := renderer.NewRenderer(renderer.WithNodeRenderers(util.Prioritized(&mdRender{}, 100))) 28 | 29 | if err := ast.Walk(n, func(node ast.Node, entering bool) (ast.WalkStatus, error) { 30 | if entering && node.Kind() == ast.KindImage { 31 | n := node.(*ast.Image) 32 | if replace, ok := images[string(n.Destination)]; ok && replace != "" { 33 | n.Destination = []byte(replace) 34 | } else if len(n.Destination) > 0 { 35 | n.Destination = []byte(prefix + string(n.Destination)) 36 | } 37 | } 38 | return ast.WalkContinue, nil 39 | }); err != nil { 40 | return "", errors.Wrap(err, "article: walk ast for Read") 41 | } 42 | 43 | var buf bytes.Buffer 44 | 45 | if err := r.Render(&buf, parsed.content, n); err != nil { 46 | return "", errors.Wrap(err, "article: render markdown") 47 | } 48 | 49 | parsed.content = buf.Bytes() 50 | 51 | return parsed.Content() 52 | } 53 | 54 | func GetImageLinks(filename string) (map[string]string, error) { 55 | parsed, n, err := read(filename) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | images := make(map[string]string) 61 | 62 | if parsed.frontMatter.CoverImage != "" { 63 | images[parsed.frontMatter.CoverImage] = "" 64 | } 65 | 66 | if err := ast.Walk(n, func(node ast.Node, entering bool) (ast.WalkStatus, error) { 67 | if entering && node.Kind() == ast.KindImage { 68 | n := node.(*ast.Image) 69 | if len(n.Destination) > 0 { 70 | images[string(n.Destination)] = "" 71 | } 72 | } 73 | return ast.WalkContinue, nil 74 | }); err != nil { 75 | return nil, errors.Wrap(err, "article: walk ast for GetImageLinks") 76 | } 77 | 78 | return images, nil 79 | } 80 | 81 | func read(filename string) (*Parsed, ast.Node, error) { 82 | parsed, err := Parse(filename) 83 | if err != nil { 84 | return nil, nil, err 85 | } 86 | 87 | p := parser.NewParser( 88 | parser.WithBlockParsers( 89 | []util.PrioritizedValue{ 90 | util.Prioritized(md.NewRawParagraphParser(), 100), 91 | }...), 92 | parser.WithInlineParsers( 93 | []util.PrioritizedValue{ 94 | util.Prioritized(parser.NewLinkParser(), 100), 95 | }...), 96 | ) 97 | 98 | reader := text.NewReader(parsed.content) 99 | 100 | return parsed, p.Parse(reader), nil 101 | } 102 | 103 | func PrefixLinks(links map[string]string, prefix string, force bool) map[string]string { 104 | results := make(map[string]string, len(links)) 105 | 106 | for k, v := range links { 107 | if v != "" && !force { 108 | results[k] = v 109 | continue 110 | } 111 | 112 | results[k] = prefix + k 113 | } 114 | 115 | return results 116 | } 117 | 118 | type mdRender struct{} 119 | 120 | // RegisterFuncs implements github.com/yuin/goldmark/renderer NodeRenderer.RegisterFuncs. 121 | func (r *mdRender) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 122 | reg.Register(ast.KindParagraph, md.RawRenderParagraph) 123 | 124 | reg.Register(ast.KindImage, md.RenderImage) 125 | reg.Register(ast.KindLink, md.RenderLink) 126 | reg.Register(ast.KindText, md.RawRenderText) 127 | } 128 | -------------------------------------------------------------------------------- /pkg/article/article_test.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sebdah/goldie/v2" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSetImageLinks(t *testing.T) { 11 | type args struct { 12 | filename string 13 | images map[string]string 14 | prefix string 15 | } 16 | 17 | tests := []struct { 18 | name string 19 | args args 20 | want string 21 | assertion assert.ErrorAssertionFunc 22 | }{ 23 | { 24 | name: "normal", 25 | args: args{ 26 | filename: "./testdata/testdata.md", 27 | images: map[string]string{ 28 | "./image.png": "./a/image.png", 29 | "./image-2.png": "", 30 | }, 31 | prefix: "www.example.com/", 32 | }, 33 | want: `--- 34 | title: A title 35 | published: false 36 | description: A description 37 | tags: tag-one, tag-two 38 | cover_image: www.example.com/./cv.jpg 39 | --- 40 | ![image](./a/image.png) 41 | [Google](www.google.com) 42 | ![image](www.example.com/./image-2.png) 43 | `, 44 | assertion: assert.NoError, 45 | }, 46 | { 47 | name: "prefix cover_image", 48 | args: args{ 49 | filename: "./testdata/testdata.md", 50 | images: map[string]string{ 51 | "./cv.jpg": "test/./cv.jpg", 52 | }, 53 | }, 54 | want: `--- 55 | title: A title 56 | published: false 57 | description: A description 58 | tags: tag-one, tag-two 59 | cover_image: test/./cv.jpg 60 | --- 61 | ![image](./image.png) 62 | [Google](www.google.com) 63 | ![image](./image-2.png) 64 | `, 65 | assertion: assert.NoError, 66 | }, 67 | { 68 | name: "prefix with no entries in config", 69 | args: args{ 70 | filename: "./testdata/testdata_nocoverimage.md", 71 | images: map[string]string{}, 72 | prefix: "test/", 73 | }, 74 | want: `--- 75 | title: A title 76 | published: false 77 | description: A description 78 | tags: tag-one, tag-two 79 | --- 80 | ![image]() 81 | [Google](www.google.com) 82 | ![image](test/./image-2.png) 83 | `, 84 | assertion: assert.NoError, 85 | }, 86 | { 87 | name: "not found", 88 | args: args{ 89 | filename: "./testdata/unknown.md", 90 | images: map[string]string{"./image.png": "./a/image.png"}, 91 | }, 92 | want: "", 93 | assertion: assert.Error, 94 | }, 95 | } 96 | 97 | for _, tt := range tests { 98 | t.Run(tt.name, func(t *testing.T) { 99 | got, err := SetImageLinks(tt.args.filename, tt.args.images, tt.args.prefix) 100 | tt.assertion(t, err) 101 | assert.Equal(t, tt.want, got) 102 | }) 103 | } 104 | } 105 | 106 | func TestSetImageLinks_golden(t *testing.T) { 107 | content, err := SetImageLinks("./testdata/real_article.md", map[string]string{}, "example.com/") 108 | assert.NoError(t, err) 109 | 110 | g := goldie.New(t) 111 | g.Assert(t, "real_article", []byte(content)) 112 | } 113 | 114 | func TestGetImageLinks(t *testing.T) { 115 | type args struct { 116 | filename string 117 | } 118 | 119 | tests := []struct { 120 | name string 121 | args args 122 | want map[string]string 123 | assertion assert.ErrorAssertionFunc 124 | }{ 125 | { 126 | name: "normal", 127 | args: args{filename: "./testdata/testdata.md"}, 128 | want: map[string]string{ 129 | "./image.png": "", 130 | "./image-2.png": "", 131 | "./cv.jpg": "", 132 | }, 133 | assertion: assert.NoError, 134 | }, 135 | { 136 | name: "normal", 137 | args: args{filename: "./testdata/testdata_nocoverimage.md"}, 138 | want: map[string]string{ 139 | "./image-2.png": "", 140 | }, 141 | assertion: assert.NoError, 142 | }, 143 | { 144 | name: "not found", 145 | args: args{filename: "./testdata/unknown.md"}, 146 | want: nil, 147 | assertion: assert.Error, 148 | }, 149 | } 150 | 151 | for _, tt := range tests { 152 | t.Run(tt.name, func(t *testing.T) { 153 | gotLinks, err := GetImageLinks(tt.args.filename) 154 | tt.assertion(t, err) 155 | assert.Equal(t, tt.want, gotLinks) 156 | }) 157 | } 158 | } 159 | 160 | func TestPrefixLinks(t *testing.T) { 161 | type args struct { 162 | links map[string]string 163 | prefix string 164 | force bool 165 | } 166 | 167 | tests := []struct { 168 | name string 169 | args args 170 | want map[string]string 171 | }{ 172 | { 173 | name: "normal", 174 | args: args{ 175 | links: map[string]string{ 176 | "./image/image.png": "image.png", 177 | "./image/picture.jpg": "", 178 | }, 179 | prefix: "https://raw.githubusercontent.com/repo/user/", 180 | }, 181 | want: map[string]string{ 182 | "./image/image.png": "image.png", 183 | "./image/picture.jpg": "https://raw.githubusercontent.com/repo/user/./image/picture.jpg", 184 | }, 185 | }, 186 | { 187 | name: "force", 188 | args: args{ 189 | links: map[string]string{ 190 | "./image/image.png": "image.png", 191 | "./image/picture.jpg": "", 192 | }, 193 | prefix: "https://raw.githubusercontent.com/repo/user/", 194 | force: true, 195 | }, 196 | want: map[string]string{ 197 | "./image/image.png": "https://raw.githubusercontent.com/repo/user/./image/image.png", 198 | "./image/picture.jpg": "https://raw.githubusercontent.com/repo/user/./image/picture.jpg", 199 | }, 200 | }, 201 | } 202 | 203 | for _, tt := range tests { 204 | t.Run(tt.name, func(t *testing.T) { 205 | assert.Equal(t, tt.want, PrefixLinks(tt.args.links, tt.args.prefix, tt.args.force)) 206 | }) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /pkg/article/client.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=client.go -destination=mock/mock_client.go 2 | package article 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/antihax/optional" 11 | "github.com/cockroachdb/errors" 12 | "github.com/shihanng/devto/pkg/devto" 13 | ) 14 | 15 | type apiClient interface { 16 | CreateArticle(context.Context, *devto.ArticlesApiCreateArticleOpts) (devto.ArticleShow, *http.Response, error) 17 | UpdateArticle(context.Context, int32, *devto.ArticlesApiUpdateArticleOpts) (devto.ArticleShow, *http.Response, error) 18 | GetUserAllArticles(context.Context, *devto.ArticlesApiGetUserAllArticlesOpts) ([]devto.ArticleMe, *http.Response, error) 19 | } 20 | 21 | type configer interface { 22 | Save() error 23 | ImageLinks() map[string]string 24 | SetImageLinks(map[string]string) 25 | ArticleID() int32 26 | SetArticleID(int32) 27 | } 28 | 29 | type Client struct { 30 | api apiClient 31 | apiKey string 32 | config configer 33 | } 34 | 35 | func NewClient(apiKey string, opts ...Option) (*Client, error) { 36 | c := Client{ 37 | api: devto.NewAPIClient(devto.NewConfiguration()).ArticlesApi, 38 | apiKey: apiKey, 39 | } 40 | 41 | for _, opt := range opts { 42 | opt(&c) 43 | } 44 | 45 | return &c, nil 46 | } 47 | 48 | type Option func(*Client) 49 | 50 | func SetConfig(cfg configer) Option { 51 | return func(c *Client) { 52 | c.config = cfg 53 | } 54 | } 55 | 56 | func (c *Client) SubmitArticle(filename string, published bool, prefix string) error { 57 | body, err := SetImageLinks(filename, c.config.ImageLinks(), prefix) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | switch c.config.ArticleID() { 63 | case 0: 64 | article := &devto.ArticlesApiCreateArticleOpts{ 65 | ArticleCreate: optional.NewInterface(devto.ArticleCreate{ 66 | Article: devto.ArticleCreateArticle{ 67 | BodyMarkdown: body, 68 | Published: published, 69 | }, 70 | }, 71 | ), 72 | } 73 | 74 | submitted, _, err := c.api.CreateArticle(c.contextWithAPIKey(), article) 75 | if err != nil { 76 | return errors.Wrap(err, "article: create article in dev.to") 77 | } 78 | 79 | c.config.SetArticleID(submitted.Id) 80 | 81 | return c.config.Save() 82 | default: 83 | articleID := c.config.ArticleID() 84 | 85 | article := &devto.ArticlesApiUpdateArticleOpts{ 86 | ArticleUpdate: optional.NewInterface(devto.ArticleUpdate{ 87 | Article: devto.ArticleUpdateArticle{ 88 | BodyMarkdown: body, 89 | Published: published, 90 | }, 91 | }, 92 | ), 93 | } 94 | 95 | _, _, err := c.api.UpdateArticle(c.contextWithAPIKey(), articleID, article) 96 | 97 | return errors.Wrapf(err, "article: update article %d in dev.to", articleID) 98 | } 99 | } 100 | 101 | func (c *Client) ListArticle(w io.Writer) error { 102 | articles, _, err := c.api.GetUserAllArticles(c.contextWithAPIKey(), nil) 103 | if err != nil { 104 | return errors.Wrap(err, "article: list articles in dev.to") 105 | } 106 | 107 | for _, a := range articles { 108 | fmt.Fprintf(w, "[%d] %s\n", a.Id, a.Title) 109 | } 110 | 111 | return nil 112 | } 113 | 114 | func (c *Client) GenerateImageLinks(filename, prefix string, override bool) error { 115 | links, err := GetImageLinks(filename) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | links = mergeLinks(c.config.ImageLinks(), links) 121 | 122 | if prefix != "" { 123 | for key, link := range links { 124 | if link == "" || override { 125 | links[key] = prefix + key 126 | } 127 | } 128 | } 129 | 130 | c.config.SetImageLinks(links) 131 | 132 | return c.config.Save() 133 | } 134 | 135 | func (c *Client) contextWithAPIKey() context.Context { 136 | return context.WithValue(context.Background(), devto.ContextAPIKey, devto.APIKey{ 137 | Key: c.apiKey, 138 | }) 139 | } 140 | 141 | func mergeLinks(old, latest map[string]string) map[string]string { 142 | for k := range latest { 143 | if v, ok := old[k]; ok { 144 | latest[k] = v 145 | } 146 | } 147 | 148 | return latest 149 | } 150 | -------------------------------------------------------------------------------- /pkg/article/client_test.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/antihax/optional" 8 | "github.com/golang/mock/gomock" 9 | mock_article "github.com/shihanng/devto/pkg/article/mock" 10 | "github.com/shihanng/devto/pkg/devto" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | const ( 15 | apiKey = "abc1234" 16 | articleID int32 = 123 17 | emptyArticle = "" 18 | ) 19 | 20 | func TestSubmitArticle_Create(t *testing.T) { 21 | ctrl := gomock.NewController(t) 22 | defer ctrl.Finish() 23 | 24 | mockAPIClient := mock_article.NewMockapiClient(ctrl) 25 | mockConfig := mock_article.NewMockconfiger(ctrl) 26 | 27 | c, err := NewClient(apiKey, SetConfig(mockConfig)) 28 | assert.NoError(t, err) 29 | 30 | c.api = mockAPIClient 31 | 32 | { 33 | mockConfig.EXPECT().ImageLinks().Return(nil) 34 | mockConfig.EXPECT().ArticleID().Return(int32(0)) 35 | mockAPIClient.EXPECT().CreateArticle(c.contextWithAPIKey(), &devto.ArticlesApiCreateArticleOpts{ 36 | ArticleCreate: optional.NewInterface(devto.ArticleCreate{ 37 | Article: devto.ArticleCreateArticle{ 38 | BodyMarkdown: emptyArticle, 39 | Published: true, 40 | }, 41 | }, 42 | ), 43 | }).Return(devto.ArticleShow{Id: articleID}, nil, nil) 44 | mockConfig.EXPECT().SetArticleID(articleID) 45 | mockConfig.EXPECT().Save().Return(nil) 46 | 47 | assert.NoError(t, c.SubmitArticle("./testdata/empty.md", true, "")) 48 | } 49 | } 50 | 51 | func TestSubmitArticle_Update(t *testing.T) { 52 | ctrl := gomock.NewController(t) 53 | defer ctrl.Finish() 54 | 55 | mockAPIClient := mock_article.NewMockapiClient(ctrl) 56 | mockConfig := mock_article.NewMockconfiger(ctrl) 57 | 58 | c, err := NewClient(apiKey, SetConfig(mockConfig)) 59 | assert.NoError(t, err) 60 | 61 | c.api = mockAPIClient 62 | 63 | mockConfig.EXPECT().ImageLinks().Return(nil) 64 | mockConfig.EXPECT().ArticleID().Return(articleID) 65 | mockConfig.EXPECT().ArticleID().Return(articleID) 66 | mockAPIClient.EXPECT().UpdateArticle(c.contextWithAPIKey(), articleID, &devto.ArticlesApiUpdateArticleOpts{ 67 | ArticleUpdate: optional.NewInterface(devto.ArticleUpdate{ 68 | Article: devto.ArticleUpdateArticle{ 69 | BodyMarkdown: emptyArticle, 70 | Published: true, 71 | }, 72 | }, 73 | ), 74 | }).Return(devto.ArticleShow{Id: articleID}, nil, nil) 75 | 76 | assert.NoError(t, c.SubmitArticle("./testdata/empty.md", true, "")) 77 | } 78 | 79 | func TestListArticle(t *testing.T) { 80 | c, err := NewClient(apiKey) 81 | assert.NoError(t, err) 82 | 83 | ctrl := gomock.NewController(t) 84 | defer ctrl.Finish() 85 | 86 | mockAPIClient := mock_article.NewMockapiClient(ctrl) 87 | mockAPIClient.EXPECT().GetUserAllArticles(c.contextWithAPIKey(), nil). 88 | Return([]devto.ArticleMe{{Title: "A title", Id: 1}}, nil, nil) 89 | 90 | c.api = mockAPIClient 91 | 92 | actual := bytes.Buffer{} 93 | expected := "[1] A title\n" 94 | 95 | assert.NoError(t, c.ListArticle(&actual)) 96 | assert.Equal(t, expected, actual.String()) 97 | } 98 | 99 | func TestGenerateImageLinks(t *testing.T) { 100 | ctrl := gomock.NewController(t) 101 | defer ctrl.Finish() 102 | 103 | mockConfig := mock_article.NewMockconfiger(ctrl) 104 | 105 | c, err := NewClient(apiKey, SetConfig(mockConfig)) 106 | assert.NoError(t, err) 107 | 108 | mockConfig.EXPECT().ImageLinks().Return(map[string]string{ 109 | "./image.png": "image-1.png", 110 | "./image-3.png": "image-3.png", 111 | "./cv.jpg": "cv.jpg", 112 | }) 113 | mockConfig.EXPECT().SetImageLinks(map[string]string{ 114 | "./image.png": "image-1.png", 115 | "./image-2.png": "", 116 | "./cv.jpg": "cv.jpg", 117 | }) 118 | mockConfig.EXPECT().Save().Return(nil) 119 | 120 | assert.NoError(t, c.GenerateImageLinks("./testdata/testdata.md", "", false)) 121 | } 122 | 123 | func TestGenerateImageLinks_NoCoverImage(t *testing.T) { 124 | ctrl := gomock.NewController(t) 125 | defer ctrl.Finish() 126 | 127 | mockConfig := mock_article.NewMockconfiger(ctrl) 128 | 129 | c, err := NewClient(apiKey, SetConfig(mockConfig)) 130 | assert.NoError(t, err) 131 | 132 | mockConfig.EXPECT().ImageLinks().Return(nil) 133 | mockConfig.EXPECT().SetImageLinks(map[string]string{}) 134 | mockConfig.EXPECT().Save().Return(nil) 135 | 136 | assert.NoError(t, c.GenerateImageLinks("./testdata/empty.md", "", false)) 137 | } 138 | 139 | func TestGenerateImageLinks_WithPrefix(t *testing.T) { 140 | ctrl := gomock.NewController(t) 141 | defer ctrl.Finish() 142 | 143 | mockConfig := mock_article.NewMockconfiger(ctrl) 144 | 145 | c, err := NewClient(apiKey, SetConfig(mockConfig)) 146 | assert.NoError(t, err) 147 | 148 | mockConfig.EXPECT().ImageLinks().Return(map[string]string{ 149 | "./image.png": "image-1.png", 150 | "./image-3.png": "image-3.png", 151 | }) 152 | mockConfig.EXPECT().SetImageLinks(map[string]string{ 153 | "./image.png": "image-1.png", 154 | "./image-2.png": "prefix/./image-2.png", 155 | "./cv.jpg": "prefix/./cv.jpg", 156 | }) 157 | mockConfig.EXPECT().Save().Return(nil) 158 | 159 | assert.NoError(t, c.GenerateImageLinks("./testdata/testdata.md", "prefix/", false)) 160 | } 161 | 162 | func TestGenerateImageLinks_WithPrefixOverride(t *testing.T) { 163 | ctrl := gomock.NewController(t) 164 | defer ctrl.Finish() 165 | 166 | mockConfig := mock_article.NewMockconfiger(ctrl) 167 | 168 | c, err := NewClient(apiKey, SetConfig(mockConfig)) 169 | assert.NoError(t, err) 170 | 171 | mockConfig.EXPECT().ImageLinks().Return(map[string]string{ 172 | "./image.png": "image-1.png", 173 | "./image-3.png": "image-3.png", 174 | }) 175 | mockConfig.EXPECT().SetImageLinks(map[string]string{ 176 | "./image.png": "prefix/./image.png", 177 | "./image-2.png": "prefix/./image-2.png", 178 | "./cv.jpg": "prefix/./cv.jpg", 179 | }) 180 | mockConfig.EXPECT().Save().Return(nil) 181 | 182 | assert.NoError(t, c.GenerateImageLinks("./testdata/testdata.md", "prefix/", true)) 183 | } 184 | -------------------------------------------------------------------------------- /pkg/article/frontmatter.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | 7 | "github.com/cockroachdb/errors" 8 | "github.com/gohugoio/hugo/parser" 9 | "github.com/gohugoio/hugo/parser/metadecoders" 10 | "github.com/gohugoio/hugo/parser/pageparser" 11 | "github.com/mitchellh/mapstructure" 12 | ) 13 | 14 | // Parse parses the article and divides the content into front matter and markdown. 15 | // Heavily inspired by: 16 | // https://github.com/gohugoio/hugo/blob/94cfdf6befd657e46c9458b23f17d851cd2f7037/commands/convert.go#L207-L250 17 | func Parse(filename string) (*Parsed, error) { 18 | f, err := os.Open(filename) 19 | if err != nil { 20 | return nil, errors.Wrap(err, "article: open file") 21 | } 22 | 23 | defer f.Close() 24 | 25 | cfm, err := pageparser.ParseFrontMatterAndContent(f) 26 | if err != nil { 27 | return nil, errors.Wrap(err, "article: parse file") 28 | } 29 | 30 | parsed := Parsed{ 31 | content: cfm.Content, 32 | frontMatterFormat: cfm.FrontMatterFormat, 33 | } 34 | 35 | if err := mapstructure.Decode(cfm.FrontMatter, &parsed.frontMatter); err != nil { 36 | return nil, errors.Wrap(err, "article: decode front matter") 37 | } 38 | 39 | return &parsed, nil 40 | } 41 | 42 | type Parsed struct { 43 | content []byte 44 | frontMatterFormat metadecoders.Format 45 | frontMatter FrontMatter 46 | } 47 | 48 | // Content merges the front mattter and markdown source and returns it as string. 49 | func (p *Parsed) Content() (string, error) { 50 | eb := errBuffer{ 51 | b: &bytes.Buffer{}, 52 | } 53 | 54 | var buf bytes.Buffer 55 | 56 | if err := parser.InterfaceToFrontMatter(p.frontMatter, p.frontMatterFormat, &buf); err != nil { 57 | return "", errors.Wrap(eb.err, "article: marshal frontMatter to YAML") 58 | } 59 | 60 | eb.Write(buf.Bytes()) 61 | eb.Write(p.content) 62 | 63 | if eb.err != nil { 64 | return "", errors.Wrap(eb.err, "article: output parsed content") 65 | } 66 | 67 | return eb.b.String(), nil 68 | } 69 | 70 | // FrontMatter as described in https://dev.to/p/editor_guide 71 | type FrontMatter struct { 72 | Title string `yaml:"title,omitempty"` 73 | Published *bool `yaml:"published,omitempty"` 74 | Description string `yaml:"description,omitempty"` 75 | Tags string `yaml:"tags,omitempty"` 76 | CanonicalURL string `yaml:"canonical_url,omitempty" mapstructure:"canonical_url"` 77 | CoverImage string `yaml:"cover_image,omitempty" mapstructure:"cover_image"` 78 | Series string `yaml:"series,omitempty"` 79 | } 80 | 81 | type errBuffer struct { 82 | b *bytes.Buffer 83 | err error 84 | } 85 | 86 | func (eb *errBuffer) Write(p []byte) { 87 | if eb.err != nil { 88 | return 89 | } 90 | 91 | _, eb.err = eb.b.Write(p) 92 | } 93 | -------------------------------------------------------------------------------- /pkg/article/frontmatter_test.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gohugoio/hugo/parser/metadecoders" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParse(t *testing.T) { 11 | nope := false 12 | actual, err := Parse("./testdata/testdata.md") 13 | expected := &Parsed{ 14 | frontMatterFormat: metadecoders.YAML, 15 | content: []byte(` 16 | ![image](./image.png) 17 | [Google](www.google.com) 18 | ![image](./image-2.png) 19 | `), 20 | frontMatter: FrontMatter{ 21 | Title: "A title", 22 | Published: &nope, 23 | Description: "A description", 24 | Tags: "tag-one, tag-two", 25 | CoverImage: "./cv.jpg", 26 | }, 27 | } 28 | 29 | assert.NoError(t, err) 30 | assert.Equal(t, expected, actual) 31 | 32 | expectedContent := `--- 33 | title: A title 34 | published: false 35 | description: A description 36 | tags: tag-one, tag-two 37 | cover_image: ./cv.jpg 38 | --- 39 | 40 | ![image](./image.png) 41 | [Google](www.google.com) 42 | ![image](./image-2.png) 43 | ` 44 | actualContent, err := actual.Content() 45 | assert.NoError(t, err) 46 | assert.Equal(t, expectedContent, actualContent) 47 | } 48 | 49 | func TestParse_NotFound(t *testing.T) { 50 | actual, err := Parse("./testdata/unknown.md") 51 | assert.Error(t, err) 52 | assert.Nil(t, actual) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/article/mock/mock_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: client.go 3 | 4 | // Package mock_article is a generated GoMock package. 5 | package mock_article 6 | 7 | import ( 8 | context "context" 9 | gomock "github.com/golang/mock/gomock" 10 | devto "github.com/shihanng/devto/pkg/devto" 11 | http "net/http" 12 | reflect "reflect" 13 | ) 14 | 15 | // MockapiClient is a mock of apiClient interface 16 | type MockapiClient struct { 17 | ctrl *gomock.Controller 18 | recorder *MockapiClientMockRecorder 19 | } 20 | 21 | // MockapiClientMockRecorder is the mock recorder for MockapiClient 22 | type MockapiClientMockRecorder struct { 23 | mock *MockapiClient 24 | } 25 | 26 | // NewMockapiClient creates a new mock instance 27 | func NewMockapiClient(ctrl *gomock.Controller) *MockapiClient { 28 | mock := &MockapiClient{ctrl: ctrl} 29 | mock.recorder = &MockapiClientMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use 34 | func (m *MockapiClient) EXPECT() *MockapiClientMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // CreateArticle mocks base method 39 | func (m *MockapiClient) CreateArticle(arg0 context.Context, arg1 *devto.ArticlesApiCreateArticleOpts) (devto.ArticleShow, *http.Response, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "CreateArticle", arg0, arg1) 42 | ret0, _ := ret[0].(devto.ArticleShow) 43 | ret1, _ := ret[1].(*http.Response) 44 | ret2, _ := ret[2].(error) 45 | return ret0, ret1, ret2 46 | } 47 | 48 | // CreateArticle indicates an expected call of CreateArticle 49 | func (mr *MockapiClientMockRecorder) CreateArticle(arg0, arg1 interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateArticle", reflect.TypeOf((*MockapiClient)(nil).CreateArticle), arg0, arg1) 52 | } 53 | 54 | // UpdateArticle mocks base method 55 | func (m *MockapiClient) UpdateArticle(arg0 context.Context, arg1 int32, arg2 *devto.ArticlesApiUpdateArticleOpts) (devto.ArticleShow, *http.Response, error) { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "UpdateArticle", arg0, arg1, arg2) 58 | ret0, _ := ret[0].(devto.ArticleShow) 59 | ret1, _ := ret[1].(*http.Response) 60 | ret2, _ := ret[2].(error) 61 | return ret0, ret1, ret2 62 | } 63 | 64 | // UpdateArticle indicates an expected call of UpdateArticle 65 | func (mr *MockapiClientMockRecorder) UpdateArticle(arg0, arg1, arg2 interface{}) *gomock.Call { 66 | mr.mock.ctrl.T.Helper() 67 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateArticle", reflect.TypeOf((*MockapiClient)(nil).UpdateArticle), arg0, arg1, arg2) 68 | } 69 | 70 | // GetUserAllArticles mocks base method 71 | func (m *MockapiClient) GetUserAllArticles(arg0 context.Context, arg1 *devto.ArticlesApiGetUserAllArticlesOpts) ([]devto.ArticleMe, *http.Response, error) { 72 | m.ctrl.T.Helper() 73 | ret := m.ctrl.Call(m, "GetUserAllArticles", arg0, arg1) 74 | ret0, _ := ret[0].([]devto.ArticleMe) 75 | ret1, _ := ret[1].(*http.Response) 76 | ret2, _ := ret[2].(error) 77 | return ret0, ret1, ret2 78 | } 79 | 80 | // GetUserAllArticles indicates an expected call of GetUserAllArticles 81 | func (mr *MockapiClientMockRecorder) GetUserAllArticles(arg0, arg1 interface{}) *gomock.Call { 82 | mr.mock.ctrl.T.Helper() 83 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserAllArticles", reflect.TypeOf((*MockapiClient)(nil).GetUserAllArticles), arg0, arg1) 84 | } 85 | 86 | // Mockconfiger is a mock of configer interface 87 | type Mockconfiger struct { 88 | ctrl *gomock.Controller 89 | recorder *MockconfigerMockRecorder 90 | } 91 | 92 | // MockconfigerMockRecorder is the mock recorder for Mockconfiger 93 | type MockconfigerMockRecorder struct { 94 | mock *Mockconfiger 95 | } 96 | 97 | // NewMockconfiger creates a new mock instance 98 | func NewMockconfiger(ctrl *gomock.Controller) *Mockconfiger { 99 | mock := &Mockconfiger{ctrl: ctrl} 100 | mock.recorder = &MockconfigerMockRecorder{mock} 101 | return mock 102 | } 103 | 104 | // EXPECT returns an object that allows the caller to indicate expected use 105 | func (m *Mockconfiger) EXPECT() *MockconfigerMockRecorder { 106 | return m.recorder 107 | } 108 | 109 | // Save mocks base method 110 | func (m *Mockconfiger) Save() error { 111 | m.ctrl.T.Helper() 112 | ret := m.ctrl.Call(m, "Save") 113 | ret0, _ := ret[0].(error) 114 | return ret0 115 | } 116 | 117 | // Save indicates an expected call of Save 118 | func (mr *MockconfigerMockRecorder) Save() *gomock.Call { 119 | mr.mock.ctrl.T.Helper() 120 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*Mockconfiger)(nil).Save)) 121 | } 122 | 123 | // ImageLinks mocks base method 124 | func (m *Mockconfiger) ImageLinks() map[string]string { 125 | m.ctrl.T.Helper() 126 | ret := m.ctrl.Call(m, "ImageLinks") 127 | ret0, _ := ret[0].(map[string]string) 128 | return ret0 129 | } 130 | 131 | // ImageLinks indicates an expected call of ImageLinks 132 | func (mr *MockconfigerMockRecorder) ImageLinks() *gomock.Call { 133 | mr.mock.ctrl.T.Helper() 134 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageLinks", reflect.TypeOf((*Mockconfiger)(nil).ImageLinks)) 135 | } 136 | 137 | // SetImageLinks mocks base method 138 | func (m *Mockconfiger) SetImageLinks(arg0 map[string]string) { 139 | m.ctrl.T.Helper() 140 | m.ctrl.Call(m, "SetImageLinks", arg0) 141 | } 142 | 143 | // SetImageLinks indicates an expected call of SetImageLinks 144 | func (mr *MockconfigerMockRecorder) SetImageLinks(arg0 interface{}) *gomock.Call { 145 | mr.mock.ctrl.T.Helper() 146 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetImageLinks", reflect.TypeOf((*Mockconfiger)(nil).SetImageLinks), arg0) 147 | } 148 | 149 | // ArticleID mocks base method 150 | func (m *Mockconfiger) ArticleID() int32 { 151 | m.ctrl.T.Helper() 152 | ret := m.ctrl.Call(m, "ArticleID") 153 | ret0, _ := ret[0].(int32) 154 | return ret0 155 | } 156 | 157 | // ArticleID indicates an expected call of ArticleID 158 | func (mr *MockconfigerMockRecorder) ArticleID() *gomock.Call { 159 | mr.mock.ctrl.T.Helper() 160 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ArticleID", reflect.TypeOf((*Mockconfiger)(nil).ArticleID)) 161 | } 162 | 163 | // SetArticleID mocks base method 164 | func (m *Mockconfiger) SetArticleID(arg0 int32) { 165 | m.ctrl.T.Helper() 166 | m.ctrl.Call(m, "SetArticleID", arg0) 167 | } 168 | 169 | // SetArticleID indicates an expected call of SetArticleID 170 | func (mr *MockconfigerMockRecorder) SetArticleID(arg0 interface{}) *gomock.Call { 171 | mr.mock.ctrl.T.Helper() 172 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetArticleID", reflect.TypeOf((*Mockconfiger)(nil).SetArticleID), arg0) 173 | } 174 | -------------------------------------------------------------------------------- /pkg/article/testdata/empty.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shihanng/devto/062835c89a417d1801231af9a99ea812f8d9da28/pkg/article/testdata/empty.md -------------------------------------------------------------------------------- /pkg/article/testdata/real_article.golden: -------------------------------------------------------------------------------- 1 | --- 2 | title: Memory Reservation in Amazon Elastic Container Service 3 | published: true 4 | tags: aws, docker, learning, beginners 5 | canonical_url: test.jp 6 | cover_image: example.com/cover.jpg 7 | --- 8 | We use [**task definition**](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html) to describe how we want a Docker container to be deployed in an ECS cluster. `memoryReservation` is one of the **container definitions** that need to be specified when writing the task definition, see [Task definition parameters by AWS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definitions): 9 | 10 | > If a task-level memory value is not specified, you must specify a non-zero integer for one or both of memory or memoryReservation in a container definition. 11 | 12 | ```json 13 | [ 14 | { 15 | "name": "worker", 16 | "image": "alexeiled/stress-ng:latest", 17 | "command": [ 18 | "--vm-bytes", 19 | "300m", 20 | "--vm-keep", 21 | "--vm", 22 | "1", 23 | "-t", 24 | "1d", 25 | "-l", 26 | "0" 27 | ], 28 | "memoryReservation": 400 29 | } 30 | ] 31 | ``` 32 | 33 | Essentially, this task definition will launch a container that is constantly consuming `300 MB / 1.049 = 286.102 MiB` of memory (see [`stress-ng`](https://manpages.ubuntu.com/manpages/artful/man1/stress-ng.1.html)). We can think of this as a memory hungry worker. The Terraform configuration of the set up is available in GitHub: 34 | 35 | {% github shihanng/ecs-resource-exp %} 36 | -------------------------------------------------------------------------------- /pkg/article/testdata/real_article.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Memory Reservation in Amazon Elastic Container Service 3 | published: true 4 | tags: aws, docker, learning, beginners 5 | cover_image: cover.jpg 6 | canonical_url: test.jp 7 | --- 8 | 9 | We use [**task definition**](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html) to describe how we want a Docker container to be deployed in an ECS cluster. `memoryReservation` is one of the **container definitions** that need to be specified when writing the task definition, see [Task definition parameters by AWS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definitions): 10 | 11 | > If a task-level memory value is not specified, you must specify a non-zero integer for one or both of memory or memoryReservation in a container definition. 12 | 13 | ```json 14 | [ 15 | { 16 | "name": "worker", 17 | "image": "alexeiled/stress-ng:latest", 18 | "command": [ 19 | "--vm-bytes", 20 | "300m", 21 | "--vm-keep", 22 | "--vm", 23 | "1", 24 | "-t", 25 | "1d", 26 | "-l", 27 | "0" 28 | ], 29 | "memoryReservation": 400 30 | } 31 | ] 32 | ``` 33 | 34 | Essentially, this task definition will launch a container that is constantly consuming `300 MB / 1.049 = 286.102 MiB` of memory (see [`stress-ng`](https://manpages.ubuntu.com/manpages/artful/man1/stress-ng.1.html)). We can think of this as a memory hungry worker. The Terraform configuration of the set up is available in GitHub: 35 | 36 | {% github shihanng/ecs-resource-exp %} 37 | -------------------------------------------------------------------------------- /pkg/article/testdata/testdata.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "A title" 3 | published: false 4 | description: "A description" 5 | tags: "tag-one, tag-two" 6 | cover_image: "./cv.jpg" 7 | --- 8 | 9 | ![image](./image.png) 10 | [Google](www.google.com) 11 | ![image](./image-2.png) 12 | -------------------------------------------------------------------------------- /pkg/article/testdata/testdata_nocoverimage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "A title" 3 | published: false 4 | description: "A description" 5 | tags: "tag-one, tag-two" 6 | --- 7 | 8 | ![image]() 9 | [Google](www.google.com) 10 | ![image](./image-2.png) 11 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/cockroachdb/errors" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | type Config struct { 11 | filename string 12 | viper *viper.Viper 13 | } 14 | 15 | func New(filename string) (*Config, error) { 16 | c := Config{ 17 | filename: filename, 18 | viper: viper.NewWithOptions(viper.KeyDelimiter("|")), 19 | } 20 | 21 | c.viper.SetConfigFile(filename) 22 | 23 | if err := c.viper.ReadInConfig(); err != nil { 24 | if !os.IsNotExist(err) && !errors.As(err, &viper.ConfigFileNotFoundError{}) { 25 | return nil, errors.Wrap(err, "config: read") 26 | } 27 | } 28 | 29 | return &c, nil 30 | } 31 | 32 | func (c *Config) Save() error { 33 | return errors.Wrap(c.viper.WriteConfigAs(c.filename), "config: update") 34 | } 35 | 36 | func (c *Config) ImageLinks() map[string]string { 37 | return c.viper.GetStringMapString("images") 38 | } 39 | 40 | func (c *Config) SetImageLinks(links map[string]string) { 41 | if len(links) == 0 { 42 | c.viper.Set("images", "") 43 | } else { 44 | c.viper.Set("images", links) 45 | } 46 | } 47 | 48 | func (c *Config) ArticleID() int32 { 49 | return c.viper.GetInt32("article_id") 50 | } 51 | 52 | func (c *Config) SetArticleID(id int32) { 53 | c.viper.Set("article_id", id) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNew(t *testing.T) { 13 | type args struct { 14 | filename string 15 | } 16 | 17 | tests := []struct { 18 | name string 19 | args args 20 | valueAssertion assert.ValueAssertionFunc 21 | errAssertion assert.ErrorAssertionFunc 22 | }{ 23 | { 24 | name: "missing config file", 25 | args: args{ 26 | filename: "./testdata/unknown.yml", 27 | }, 28 | valueAssertion: assert.NotNil, 29 | errAssertion: assert.NoError, 30 | }, 31 | { 32 | name: "empty config file", 33 | args: args{ 34 | filename: "./testdata/empty.yml", 35 | }, 36 | valueAssertion: assert.NotNil, 37 | errAssertion: assert.NoError, 38 | }, 39 | { 40 | name: "bad config file", 41 | args: args{ 42 | filename: "./testdata/bad.yml", 43 | }, 44 | valueAssertion: assert.Nil, 45 | errAssertion: assert.Error, 46 | }, 47 | } 48 | 49 | for _, tt := range tests { 50 | t.Run(tt.name, func(t *testing.T) { 51 | got, err := New(tt.args.filename) 52 | tt.valueAssertion(t, got) 53 | tt.errAssertion(t, err) 54 | }) 55 | } 56 | } 57 | 58 | func TestConfig_Save(t *testing.T) { 59 | type fields struct { 60 | filename string 61 | } 62 | 63 | tests := []struct { 64 | name string 65 | fields fields 66 | assertion assert.ErrorAssertionFunc 67 | }{ 68 | { 69 | name: "no config file", 70 | fields: fields{ 71 | filename: "", 72 | }, 73 | assertion: assert.Error, 74 | }, 75 | } 76 | 77 | for _, tt := range tests { 78 | t.Run(tt.name, func(t *testing.T) { 79 | c, err := New(tt.fields.filename) 80 | require.NoError(t, err) 81 | tt.assertion(t, c.Save()) 82 | }) 83 | } 84 | } 85 | 86 | func TestGetters(t *testing.T) { 87 | c, err := New("./testdata/devto.yml") 88 | require.NoError(t, err) 89 | 90 | { 91 | var expected int32 = 1985 92 | assert.Equal(t, expected, c.ArticleID()) 93 | } 94 | 95 | { 96 | expected := map[string]string{ 97 | "key-1": "value-a", 98 | "key-2": "value-b", 99 | } 100 | assert.Equal(t, expected, c.ImageLinks()) 101 | } 102 | } 103 | 104 | func TestSetters(t *testing.T) { 105 | tmpfile, err := ioutil.TempFile("", "temp.*.yml") 106 | require.NoError(t, err) 107 | 108 | defer os.Remove(tmpfile.Name()) // clean up 109 | 110 | c, err := New(tmpfile.Name()) 111 | require.NoError(t, err) 112 | 113 | c.SetArticleID(1985) 114 | c.SetImageLinks(map[string]string{ 115 | "key-1": "value-a", 116 | "key-2": "value-b", 117 | }) 118 | 119 | require.NoError(t, c.Save()) 120 | 121 | expected := []byte(`article_id: 1985 122 | images: 123 | key-1: value-a 124 | key-2: value-b 125 | `) 126 | 127 | actual, err := ioutil.ReadFile(tmpfile.Name()) 128 | require.NoError(t, err) 129 | 130 | assert.Equal(t, expected, actual) 131 | } 132 | 133 | func TestConfig_SetImageLinks(t *testing.T) { 134 | type args struct { 135 | links map[string]string 136 | } 137 | 138 | tests := []struct { 139 | name string 140 | args args 141 | expected []byte 142 | }{ 143 | { 144 | name: "empty", 145 | args: args{}, 146 | expected: []byte(`images: "" 147 | `), 148 | }, 149 | { 150 | name: "with values", 151 | args: args{ 152 | links: map[string]string{ 153 | "./image-1.png": "", 154 | "./image-2.jpg": "./mod/image-2.jpg", 155 | }, 156 | }, 157 | expected: []byte(`images: 158 | ./image-1.png: "" 159 | ./image-2.jpg: ./mod/image-2.jpg 160 | `), 161 | }, 162 | } 163 | for _, tt := range tests { 164 | t.Run(tt.name, func(t *testing.T) { 165 | tmpfile, err := ioutil.TempFile("", "temp.*.yml") 166 | require.NoError(t, err) 167 | 168 | defer os.Remove(tmpfile.Name()) // clean up 169 | 170 | c, err := New(tmpfile.Name()) 171 | require.NoError(t, err) 172 | 173 | c.SetImageLinks(tt.args.links) 174 | 175 | require.NoError(t, c.Save()) 176 | 177 | actual, err := ioutil.ReadFile(tmpfile.Name()) 178 | require.NoError(t, err) 179 | 180 | assert.Equal(t, tt.expected, actual) 181 | }) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /pkg/config/testdata/bad.yml: -------------------------------------------------------------------------------- 1 | bad 2 | -------------------------------------------------------------------------------- /pkg/config/testdata/devto.yml: -------------------------------------------------------------------------------- 1 | article_id: 1985 2 | cover_image: ./test.jpg 3 | images: 4 | key-1: value-a 5 | key-2: value-b 6 | -------------------------------------------------------------------------------- /pkg/config/testdata/empty.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shihanng/devto/062835c89a417d1801231af9a99ea812f8d9da28/pkg/config/testdata/empty.yml -------------------------------------------------------------------------------- /pkg/devto/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /pkg/devto/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | go.mod 25 | go.sum 26 | git_push.sh 27 | -------------------------------------------------------------------------------- /pkg/devto/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 4.2.2 -------------------------------------------------------------------------------- /pkg/devto/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | install: 4 | - go get -d -v . 5 | 6 | script: 7 | - go build -v ./ 8 | 9 | -------------------------------------------------------------------------------- /pkg/devto/README.md: -------------------------------------------------------------------------------- 1 | # Go API client for devto 2 | 3 | Access DEV articles, comments and other resources via API 4 | 5 | ## Overview 6 | This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client. 7 | 8 | - API version: 0.5.9 9 | - Package version: 1.0.0 10 | - Build package: org.openapitools.codegen.languages.GoClientCodegen 11 | For more information, please visit [https://dev.to/contact](https://dev.to/contact) 12 | 13 | ## Installation 14 | 15 | Install the following dependencies: 16 | 17 | ```shell 18 | go get github.com/stretchr/testify/assert 19 | go get golang.org/x/oauth2 20 | go get golang.org/x/net/context 21 | go get github.com/antihax/optional 22 | ``` 23 | 24 | Put the package under your project folder and add the following in import: 25 | 26 | ```golang 27 | import "./devto" 28 | ``` 29 | 30 | ## Documentation for API Endpoints 31 | 32 | All URIs are relative to *https://dev.to/api* 33 | 34 | Class | Method | HTTP request | Description 35 | ------------ | ------------- | ------------- | ------------- 36 | *ArticlesApi* | [**CreateArticle**](docs/ArticlesApi.md#createarticle) | **Post** /articles | Create a new article 37 | *ArticlesApi* | [**GetArticleById**](docs/ArticlesApi.md#getarticlebyid) | **Get** /articles/{id} | A published article 38 | *ArticlesApi* | [**GetArticles**](docs/ArticlesApi.md#getarticles) | **Get** /articles | Published articles 39 | *ArticlesApi* | [**GetUserAllArticles**](docs/ArticlesApi.md#getuserallarticles) | **Get** /articles/me/all | User's all articles 40 | *ArticlesApi* | [**GetUserArticles**](docs/ArticlesApi.md#getuserarticles) | **Get** /articles/me | User's articles 41 | *ArticlesApi* | [**GetUserPublishedArticles**](docs/ArticlesApi.md#getuserpublishedarticles) | **Get** /articles/me/published | User's published articles 42 | *ArticlesApi* | [**GetUserUnpublishedArticles**](docs/ArticlesApi.md#getuserunpublishedarticles) | **Get** /articles/me/unpublished | User's unpublished articles 43 | *ArticlesApi* | [**UpdateArticle**](docs/ArticlesApi.md#updatearticle) | **Put** /articles/{id} | Update an article 44 | *UsersApi* | [**GetUserAllArticles**](docs/UsersApi.md#getuserallarticles) | **Get** /articles/me/all | User's all articles 45 | *UsersApi* | [**GetUserArticles**](docs/UsersApi.md#getuserarticles) | **Get** /articles/me | User's articles 46 | *UsersApi* | [**GetUserPublishedArticles**](docs/UsersApi.md#getuserpublishedarticles) | **Get** /articles/me/published | User's published articles 47 | *UsersApi* | [**GetUserUnpublishedArticles**](docs/UsersApi.md#getuserunpublishedarticles) | **Get** /articles/me/unpublished | User's unpublished articles 48 | *WebhooksApi* | [**CreateWebhook**](docs/WebhooksApi.md#createwebhook) | **Post** /webhooks | Create a new webhook 49 | *WebhooksApi* | [**DeleteWebhook**](docs/WebhooksApi.md#deletewebhook) | **Delete** /webhooks/{id} | A webhook endpoint 50 | *WebhooksApi* | [**GetWebhookById**](docs/WebhooksApi.md#getwebhookbyid) | **Get** /webhooks/{id} | A webhook endpoint 51 | *WebhooksApi* | [**GetWebhooks**](docs/WebhooksApi.md#getwebhooks) | **Get** /webhooks | Webhooks 52 | 53 | 54 | ## Documentation For Models 55 | 56 | - [ApiError](docs/ApiError.md) 57 | - [ArticleCreate](docs/ArticleCreate.md) 58 | - [ArticleCreateArticle](docs/ArticleCreateArticle.md) 59 | - [ArticleFlareTag](docs/ArticleFlareTag.md) 60 | - [ArticleIndex](docs/ArticleIndex.md) 61 | - [ArticleMe](docs/ArticleMe.md) 62 | - [ArticleOrganization](docs/ArticleOrganization.md) 63 | - [ArticleShow](docs/ArticleShow.md) 64 | - [ArticleUpdate](docs/ArticleUpdate.md) 65 | - [ArticleUpdateArticle](docs/ArticleUpdateArticle.md) 66 | - [ArticleUser](docs/ArticleUser.md) 67 | - [WebhookCreate](docs/WebhookCreate.md) 68 | - [WebhookCreateWebhookEndpoint](docs/WebhookCreateWebhookEndpoint.md) 69 | - [WebhookIndex](docs/WebhookIndex.md) 70 | - [WebhookShow](docs/WebhookShow.md) 71 | 72 | 73 | ## Documentation For Authorization 74 | 75 | 76 | 77 | ## api_key 78 | 79 | - **Type**: API key 80 | 81 | Example 82 | 83 | ```golang 84 | auth := context.WithValue(context.Background(), sw.ContextAPIKey, sw.APIKey{ 85 | Key: "APIKEY", 86 | Prefix: "Bearer", // Omit if not necessary. 87 | }) 88 | r, err := client.Service.Operation(auth, args) 89 | ``` 90 | 91 | 92 | ## oauth2 93 | 94 | 95 | - **Type**: OAuth 96 | - **Flow**: application 97 | - **Authorization URL**: 98 | - **Scopes**: N/A 99 | 100 | Example 101 | 102 | ```golang 103 | auth := context.WithValue(context.Background(), sw.ContextAccessToken, "ACCESSTOKENSTRING") 104 | r, err := client.Service.Operation(auth, args) 105 | ``` 106 | 107 | Or via OAuth2 module to automatically refresh tokens and perform user authentication. 108 | 109 | ```golang 110 | import "golang.org/x/oauth2" 111 | 112 | /* Perform OAuth2 round trip request and obtain a token */ 113 | 114 | tokenSource := oauth2cfg.TokenSource(createContext(httpClient), &token) 115 | auth := context.WithValue(oauth2.NoContext, sw.ContextOAuth2, tokenSource) 116 | r, err := client.Service.Operation(auth, args) 117 | ``` 118 | 119 | 120 | ## Author 121 | 122 | yo@dev.to 123 | 124 | -------------------------------------------------------------------------------- /pkg/devto/api_users.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | import ( 14 | _context "context" 15 | "github.com/antihax/optional" 16 | _ioutil "io/ioutil" 17 | _nethttp "net/http" 18 | _neturl "net/url" 19 | ) 20 | 21 | // Linger please 22 | var ( 23 | _ _context.Context 24 | ) 25 | 26 | // UsersApiService UsersApi service 27 | type UsersApiService service 28 | 29 | // UsersApiGetUserAllArticlesOpts Optional parameters for the method 'GetUserAllArticles' 30 | type UsersApiGetUserAllArticlesOpts struct { 31 | Page optional.Int32 32 | PerPage optional.Int32 33 | } 34 | 35 | /* 36 | GetUserAllArticles User's all articles 37 | This endpoint allows the client to retrieve a list of all articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. It will return both published and unpublished articles with pagination. Unpublished articles will be at the top of the list in reverse chronological creation order. Published articles will follow in reverse chronological publication order. By default a page will contain `30` articles. 38 | * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). 39 | * @param optional nil or *UsersApiGetUserAllArticlesOpts - Optional Parameters: 40 | * @param "Page" (optional.Int32) - Pagination page. 41 | * @param "PerPage" (optional.Int32) - Page size (defaults to 30 with a maximum of 1000). 42 | @return []ArticleMe 43 | */ 44 | func (a *UsersApiService) GetUserAllArticles(ctx _context.Context, localVarOptionals *UsersApiGetUserAllArticlesOpts) ([]ArticleMe, *_nethttp.Response, error) { 45 | var ( 46 | localVarHTTPMethod = _nethttp.MethodGet 47 | localVarPostBody interface{} 48 | localVarFormFileName string 49 | localVarFileName string 50 | localVarFileBytes []byte 51 | localVarReturnValue []ArticleMe 52 | ) 53 | 54 | // create path and map variables 55 | localVarPath := a.client.cfg.BasePath + "/articles/me/all" 56 | 57 | localVarHeaderParams := make(map[string]string) 58 | localVarQueryParams := _neturl.Values{} 59 | localVarFormParams := _neturl.Values{} 60 | 61 | if localVarOptionals != nil && localVarOptionals.Page.IsSet() { 62 | localVarQueryParams.Add("page", parameterToString(localVarOptionals.Page.Value(), "")) 63 | } 64 | if localVarOptionals != nil && localVarOptionals.PerPage.IsSet() { 65 | localVarQueryParams.Add("per_page", parameterToString(localVarOptionals.PerPage.Value(), "")) 66 | } 67 | // to determine the Content-Type header 68 | localVarHTTPContentTypes := []string{} 69 | 70 | // set Content-Type header 71 | localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) 72 | if localVarHTTPContentType != "" { 73 | localVarHeaderParams["Content-Type"] = localVarHTTPContentType 74 | } 75 | 76 | // to determine the Accept header 77 | localVarHTTPHeaderAccepts := []string{"application/json"} 78 | 79 | // set Accept header 80 | localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) 81 | if localVarHTTPHeaderAccept != "" { 82 | localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept 83 | } 84 | if ctx != nil { 85 | // API Key Authentication 86 | if auth, ok := ctx.Value(ContextAPIKey).(APIKey); ok { 87 | var key string 88 | if auth.Prefix != "" { 89 | key = auth.Prefix + " " + auth.Key 90 | } else { 91 | key = auth.Key 92 | } 93 | localVarHeaderParams["api-key"] = key 94 | } 95 | } 96 | r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) 97 | if err != nil { 98 | return localVarReturnValue, nil, err 99 | } 100 | 101 | localVarHTTPResponse, err := a.client.callAPI(r) 102 | if err != nil || localVarHTTPResponse == nil { 103 | return localVarReturnValue, localVarHTTPResponse, err 104 | } 105 | 106 | localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) 107 | localVarHTTPResponse.Body.Close() 108 | if err != nil { 109 | return localVarReturnValue, localVarHTTPResponse, err 110 | } 111 | 112 | if localVarHTTPResponse.StatusCode >= 300 { 113 | newErr := GenericOpenAPIError{ 114 | body: localVarBody, 115 | error: localVarHTTPResponse.Status, 116 | } 117 | if localVarHTTPResponse.StatusCode == 200 { 118 | var v []ArticleMe 119 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 120 | if err != nil { 121 | newErr.error = err.Error() 122 | return localVarReturnValue, localVarHTTPResponse, newErr 123 | } 124 | newErr.model = v 125 | } 126 | return localVarReturnValue, localVarHTTPResponse, newErr 127 | } 128 | 129 | err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 130 | if err != nil { 131 | newErr := GenericOpenAPIError{ 132 | body: localVarBody, 133 | error: err.Error(), 134 | } 135 | return localVarReturnValue, localVarHTTPResponse, newErr 136 | } 137 | 138 | return localVarReturnValue, localVarHTTPResponse, nil 139 | } 140 | 141 | // UsersApiGetUserArticlesOpts Optional parameters for the method 'GetUserArticles' 142 | type UsersApiGetUserArticlesOpts struct { 143 | Page optional.Int32 144 | PerPage optional.Int32 145 | } 146 | 147 | /* 148 | GetUserArticles User's articles 149 | This endpoint allows the client to retrieve a list of published articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. Published articles will be in reverse chronological publication order. It will return published articles with pagination. By default a page will contain `30` articles. 150 | * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). 151 | * @param optional nil or *UsersApiGetUserArticlesOpts - Optional Parameters: 152 | * @param "Page" (optional.Int32) - Pagination page. 153 | * @param "PerPage" (optional.Int32) - Page size (defaults to 30 with a maximum of 1000). 154 | @return []ArticleMe 155 | */ 156 | func (a *UsersApiService) GetUserArticles(ctx _context.Context, localVarOptionals *UsersApiGetUserArticlesOpts) ([]ArticleMe, *_nethttp.Response, error) { 157 | var ( 158 | localVarHTTPMethod = _nethttp.MethodGet 159 | localVarPostBody interface{} 160 | localVarFormFileName string 161 | localVarFileName string 162 | localVarFileBytes []byte 163 | localVarReturnValue []ArticleMe 164 | ) 165 | 166 | // create path and map variables 167 | localVarPath := a.client.cfg.BasePath + "/articles/me" 168 | 169 | localVarHeaderParams := make(map[string]string) 170 | localVarQueryParams := _neturl.Values{} 171 | localVarFormParams := _neturl.Values{} 172 | 173 | if localVarOptionals != nil && localVarOptionals.Page.IsSet() { 174 | localVarQueryParams.Add("page", parameterToString(localVarOptionals.Page.Value(), "")) 175 | } 176 | if localVarOptionals != nil && localVarOptionals.PerPage.IsSet() { 177 | localVarQueryParams.Add("per_page", parameterToString(localVarOptionals.PerPage.Value(), "")) 178 | } 179 | // to determine the Content-Type header 180 | localVarHTTPContentTypes := []string{} 181 | 182 | // set Content-Type header 183 | localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) 184 | if localVarHTTPContentType != "" { 185 | localVarHeaderParams["Content-Type"] = localVarHTTPContentType 186 | } 187 | 188 | // to determine the Accept header 189 | localVarHTTPHeaderAccepts := []string{"application/json"} 190 | 191 | // set Accept header 192 | localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) 193 | if localVarHTTPHeaderAccept != "" { 194 | localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept 195 | } 196 | if ctx != nil { 197 | // API Key Authentication 198 | if auth, ok := ctx.Value(ContextAPIKey).(APIKey); ok { 199 | var key string 200 | if auth.Prefix != "" { 201 | key = auth.Prefix + " " + auth.Key 202 | } else { 203 | key = auth.Key 204 | } 205 | localVarHeaderParams["api-key"] = key 206 | } 207 | } 208 | r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) 209 | if err != nil { 210 | return localVarReturnValue, nil, err 211 | } 212 | 213 | localVarHTTPResponse, err := a.client.callAPI(r) 214 | if err != nil || localVarHTTPResponse == nil { 215 | return localVarReturnValue, localVarHTTPResponse, err 216 | } 217 | 218 | localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) 219 | localVarHTTPResponse.Body.Close() 220 | if err != nil { 221 | return localVarReturnValue, localVarHTTPResponse, err 222 | } 223 | 224 | if localVarHTTPResponse.StatusCode >= 300 { 225 | newErr := GenericOpenAPIError{ 226 | body: localVarBody, 227 | error: localVarHTTPResponse.Status, 228 | } 229 | if localVarHTTPResponse.StatusCode == 200 { 230 | var v []ArticleMe 231 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 232 | if err != nil { 233 | newErr.error = err.Error() 234 | return localVarReturnValue, localVarHTTPResponse, newErr 235 | } 236 | newErr.model = v 237 | } 238 | return localVarReturnValue, localVarHTTPResponse, newErr 239 | } 240 | 241 | err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 242 | if err != nil { 243 | newErr := GenericOpenAPIError{ 244 | body: localVarBody, 245 | error: err.Error(), 246 | } 247 | return localVarReturnValue, localVarHTTPResponse, newErr 248 | } 249 | 250 | return localVarReturnValue, localVarHTTPResponse, nil 251 | } 252 | 253 | // UsersApiGetUserPublishedArticlesOpts Optional parameters for the method 'GetUserPublishedArticles' 254 | type UsersApiGetUserPublishedArticlesOpts struct { 255 | Page optional.Int32 256 | PerPage optional.Int32 257 | } 258 | 259 | /* 260 | GetUserPublishedArticles User's published articles 261 | This endpoint allows the client to retrieve a list of published articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. Published articles will be in reverse chronological publication order. It will return published articles with pagination. By default a page will contain `30` articles. 262 | * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). 263 | * @param optional nil or *UsersApiGetUserPublishedArticlesOpts - Optional Parameters: 264 | * @param "Page" (optional.Int32) - Pagination page. 265 | * @param "PerPage" (optional.Int32) - Page size (defaults to 30 with a maximum of 1000). 266 | @return []ArticleMe 267 | */ 268 | func (a *UsersApiService) GetUserPublishedArticles(ctx _context.Context, localVarOptionals *UsersApiGetUserPublishedArticlesOpts) ([]ArticleMe, *_nethttp.Response, error) { 269 | var ( 270 | localVarHTTPMethod = _nethttp.MethodGet 271 | localVarPostBody interface{} 272 | localVarFormFileName string 273 | localVarFileName string 274 | localVarFileBytes []byte 275 | localVarReturnValue []ArticleMe 276 | ) 277 | 278 | // create path and map variables 279 | localVarPath := a.client.cfg.BasePath + "/articles/me/published" 280 | 281 | localVarHeaderParams := make(map[string]string) 282 | localVarQueryParams := _neturl.Values{} 283 | localVarFormParams := _neturl.Values{} 284 | 285 | if localVarOptionals != nil && localVarOptionals.Page.IsSet() { 286 | localVarQueryParams.Add("page", parameterToString(localVarOptionals.Page.Value(), "")) 287 | } 288 | if localVarOptionals != nil && localVarOptionals.PerPage.IsSet() { 289 | localVarQueryParams.Add("per_page", parameterToString(localVarOptionals.PerPage.Value(), "")) 290 | } 291 | // to determine the Content-Type header 292 | localVarHTTPContentTypes := []string{} 293 | 294 | // set Content-Type header 295 | localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) 296 | if localVarHTTPContentType != "" { 297 | localVarHeaderParams["Content-Type"] = localVarHTTPContentType 298 | } 299 | 300 | // to determine the Accept header 301 | localVarHTTPHeaderAccepts := []string{"application/json"} 302 | 303 | // set Accept header 304 | localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) 305 | if localVarHTTPHeaderAccept != "" { 306 | localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept 307 | } 308 | if ctx != nil { 309 | // API Key Authentication 310 | if auth, ok := ctx.Value(ContextAPIKey).(APIKey); ok { 311 | var key string 312 | if auth.Prefix != "" { 313 | key = auth.Prefix + " " + auth.Key 314 | } else { 315 | key = auth.Key 316 | } 317 | localVarHeaderParams["api-key"] = key 318 | } 319 | } 320 | r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) 321 | if err != nil { 322 | return localVarReturnValue, nil, err 323 | } 324 | 325 | localVarHTTPResponse, err := a.client.callAPI(r) 326 | if err != nil || localVarHTTPResponse == nil { 327 | return localVarReturnValue, localVarHTTPResponse, err 328 | } 329 | 330 | localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) 331 | localVarHTTPResponse.Body.Close() 332 | if err != nil { 333 | return localVarReturnValue, localVarHTTPResponse, err 334 | } 335 | 336 | if localVarHTTPResponse.StatusCode >= 300 { 337 | newErr := GenericOpenAPIError{ 338 | body: localVarBody, 339 | error: localVarHTTPResponse.Status, 340 | } 341 | if localVarHTTPResponse.StatusCode == 200 { 342 | var v []ArticleMe 343 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 344 | if err != nil { 345 | newErr.error = err.Error() 346 | return localVarReturnValue, localVarHTTPResponse, newErr 347 | } 348 | newErr.model = v 349 | } 350 | return localVarReturnValue, localVarHTTPResponse, newErr 351 | } 352 | 353 | err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 354 | if err != nil { 355 | newErr := GenericOpenAPIError{ 356 | body: localVarBody, 357 | error: err.Error(), 358 | } 359 | return localVarReturnValue, localVarHTTPResponse, newErr 360 | } 361 | 362 | return localVarReturnValue, localVarHTTPResponse, nil 363 | } 364 | 365 | // UsersApiGetUserUnpublishedArticlesOpts Optional parameters for the method 'GetUserUnpublishedArticles' 366 | type UsersApiGetUserUnpublishedArticlesOpts struct { 367 | Page optional.Int32 368 | PerPage optional.Int32 369 | } 370 | 371 | /* 372 | GetUserUnpublishedArticles User's unpublished articles 373 | This endpoint allows the client to retrieve a list of unpublished articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. Unpublished articles will be in reverse chronological creation order. It will return unpublished articles with pagination. By default a page will contain `30` articles. 374 | * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). 375 | * @param optional nil or *UsersApiGetUserUnpublishedArticlesOpts - Optional Parameters: 376 | * @param "Page" (optional.Int32) - Pagination page. 377 | * @param "PerPage" (optional.Int32) - Page size (defaults to 30 with a maximum of 1000). 378 | @return []ArticleMe 379 | */ 380 | func (a *UsersApiService) GetUserUnpublishedArticles(ctx _context.Context, localVarOptionals *UsersApiGetUserUnpublishedArticlesOpts) ([]ArticleMe, *_nethttp.Response, error) { 381 | var ( 382 | localVarHTTPMethod = _nethttp.MethodGet 383 | localVarPostBody interface{} 384 | localVarFormFileName string 385 | localVarFileName string 386 | localVarFileBytes []byte 387 | localVarReturnValue []ArticleMe 388 | ) 389 | 390 | // create path and map variables 391 | localVarPath := a.client.cfg.BasePath + "/articles/me/unpublished" 392 | 393 | localVarHeaderParams := make(map[string]string) 394 | localVarQueryParams := _neturl.Values{} 395 | localVarFormParams := _neturl.Values{} 396 | 397 | if localVarOptionals != nil && localVarOptionals.Page.IsSet() { 398 | localVarQueryParams.Add("page", parameterToString(localVarOptionals.Page.Value(), "")) 399 | } 400 | if localVarOptionals != nil && localVarOptionals.PerPage.IsSet() { 401 | localVarQueryParams.Add("per_page", parameterToString(localVarOptionals.PerPage.Value(), "")) 402 | } 403 | // to determine the Content-Type header 404 | localVarHTTPContentTypes := []string{} 405 | 406 | // set Content-Type header 407 | localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) 408 | if localVarHTTPContentType != "" { 409 | localVarHeaderParams["Content-Type"] = localVarHTTPContentType 410 | } 411 | 412 | // to determine the Accept header 413 | localVarHTTPHeaderAccepts := []string{"application/json"} 414 | 415 | // set Accept header 416 | localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) 417 | if localVarHTTPHeaderAccept != "" { 418 | localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept 419 | } 420 | if ctx != nil { 421 | // API Key Authentication 422 | if auth, ok := ctx.Value(ContextAPIKey).(APIKey); ok { 423 | var key string 424 | if auth.Prefix != "" { 425 | key = auth.Prefix + " " + auth.Key 426 | } else { 427 | key = auth.Key 428 | } 429 | localVarHeaderParams["api-key"] = key 430 | } 431 | } 432 | r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) 433 | if err != nil { 434 | return localVarReturnValue, nil, err 435 | } 436 | 437 | localVarHTTPResponse, err := a.client.callAPI(r) 438 | if err != nil || localVarHTTPResponse == nil { 439 | return localVarReturnValue, localVarHTTPResponse, err 440 | } 441 | 442 | localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) 443 | localVarHTTPResponse.Body.Close() 444 | if err != nil { 445 | return localVarReturnValue, localVarHTTPResponse, err 446 | } 447 | 448 | if localVarHTTPResponse.StatusCode >= 300 { 449 | newErr := GenericOpenAPIError{ 450 | body: localVarBody, 451 | error: localVarHTTPResponse.Status, 452 | } 453 | if localVarHTTPResponse.StatusCode == 200 { 454 | var v []ArticleMe 455 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 456 | if err != nil { 457 | newErr.error = err.Error() 458 | return localVarReturnValue, localVarHTTPResponse, newErr 459 | } 460 | newErr.model = v 461 | } 462 | return localVarReturnValue, localVarHTTPResponse, newErr 463 | } 464 | 465 | err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 466 | if err != nil { 467 | newErr := GenericOpenAPIError{ 468 | body: localVarBody, 469 | error: err.Error(), 470 | } 471 | return localVarReturnValue, localVarHTTPResponse, newErr 472 | } 473 | 474 | return localVarReturnValue, localVarHTTPResponse, nil 475 | } 476 | -------------------------------------------------------------------------------- /pkg/devto/api_webhooks.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | import ( 14 | _context "context" 15 | "fmt" 16 | "github.com/antihax/optional" 17 | _ioutil "io/ioutil" 18 | _nethttp "net/http" 19 | _neturl "net/url" 20 | "strings" 21 | ) 22 | 23 | // Linger please 24 | var ( 25 | _ _context.Context 26 | ) 27 | 28 | // WebhooksApiService WebhooksApi service 29 | type WebhooksApiService service 30 | 31 | // WebhooksApiCreateWebhookOpts Optional parameters for the method 'CreateWebhook' 32 | type WebhooksApiCreateWebhookOpts struct { 33 | WebhookCreate optional.Interface 34 | } 35 | 36 | /* 37 | CreateWebhook Create a new webhook 38 | This endpoint allows the client to create a new webhook. \"Webhooks\" are used to register HTTP endpoints that will be called once a relevant event is triggered inside the web application, events like `article_created`, `article_updated`. 39 | * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). 40 | * @param optional nil or *WebhooksApiCreateWebhookOpts - Optional Parameters: 41 | * @param "WebhookCreate" (optional.Interface of WebhookCreate) - Webhook to create 42 | @return WebhookShow 43 | */ 44 | func (a *WebhooksApiService) CreateWebhook(ctx _context.Context, localVarOptionals *WebhooksApiCreateWebhookOpts) (WebhookShow, *_nethttp.Response, error) { 45 | var ( 46 | localVarHTTPMethod = _nethttp.MethodPost 47 | localVarPostBody interface{} 48 | localVarFormFileName string 49 | localVarFileName string 50 | localVarFileBytes []byte 51 | localVarReturnValue WebhookShow 52 | ) 53 | 54 | // create path and map variables 55 | localVarPath := a.client.cfg.BasePath + "/webhooks" 56 | 57 | localVarHeaderParams := make(map[string]string) 58 | localVarQueryParams := _neturl.Values{} 59 | localVarFormParams := _neturl.Values{} 60 | 61 | // to determine the Content-Type header 62 | localVarHTTPContentTypes := []string{"application/json"} 63 | 64 | // set Content-Type header 65 | localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) 66 | if localVarHTTPContentType != "" { 67 | localVarHeaderParams["Content-Type"] = localVarHTTPContentType 68 | } 69 | 70 | // to determine the Accept header 71 | localVarHTTPHeaderAccepts := []string{"application/json"} 72 | 73 | // set Accept header 74 | localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) 75 | if localVarHTTPHeaderAccept != "" { 76 | localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept 77 | } 78 | // body params 79 | if localVarOptionals != nil && localVarOptionals.WebhookCreate.IsSet() { 80 | localVarOptionalWebhookCreate, localVarOptionalWebhookCreateok := localVarOptionals.WebhookCreate.Value().(WebhookCreate) 81 | if !localVarOptionalWebhookCreateok { 82 | return localVarReturnValue, nil, reportError("webhookCreate should be WebhookCreate") 83 | } 84 | localVarPostBody = &localVarOptionalWebhookCreate 85 | } 86 | 87 | if ctx != nil { 88 | // API Key Authentication 89 | if auth, ok := ctx.Value(ContextAPIKey).(APIKey); ok { 90 | var key string 91 | if auth.Prefix != "" { 92 | key = auth.Prefix + " " + auth.Key 93 | } else { 94 | key = auth.Key 95 | } 96 | localVarHeaderParams["api-key"] = key 97 | } 98 | } 99 | r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) 100 | if err != nil { 101 | return localVarReturnValue, nil, err 102 | } 103 | 104 | localVarHTTPResponse, err := a.client.callAPI(r) 105 | if err != nil || localVarHTTPResponse == nil { 106 | return localVarReturnValue, localVarHTTPResponse, err 107 | } 108 | 109 | localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) 110 | localVarHTTPResponse.Body.Close() 111 | if err != nil { 112 | return localVarReturnValue, localVarHTTPResponse, err 113 | } 114 | 115 | if localVarHTTPResponse.StatusCode >= 300 { 116 | newErr := GenericOpenAPIError{ 117 | body: localVarBody, 118 | error: localVarHTTPResponse.Status, 119 | } 120 | if localVarHTTPResponse.StatusCode == 201 { 121 | var v WebhookShow 122 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 123 | if err != nil { 124 | newErr.error = err.Error() 125 | return localVarReturnValue, localVarHTTPResponse, newErr 126 | } 127 | newErr.model = v 128 | return localVarReturnValue, localVarHTTPResponse, newErr 129 | } 130 | if localVarHTTPResponse.StatusCode == 400 { 131 | var v ApiError 132 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 133 | if err != nil { 134 | newErr.error = err.Error() 135 | return localVarReturnValue, localVarHTTPResponse, newErr 136 | } 137 | newErr.model = v 138 | return localVarReturnValue, localVarHTTPResponse, newErr 139 | } 140 | if localVarHTTPResponse.StatusCode == 401 { 141 | var v ApiError 142 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 143 | if err != nil { 144 | newErr.error = err.Error() 145 | return localVarReturnValue, localVarHTTPResponse, newErr 146 | } 147 | newErr.model = v 148 | return localVarReturnValue, localVarHTTPResponse, newErr 149 | } 150 | if localVarHTTPResponse.StatusCode == 422 { 151 | var v ApiError 152 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 153 | if err != nil { 154 | newErr.error = err.Error() 155 | return localVarReturnValue, localVarHTTPResponse, newErr 156 | } 157 | newErr.model = v 158 | } 159 | return localVarReturnValue, localVarHTTPResponse, newErr 160 | } 161 | 162 | err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 163 | if err != nil { 164 | newErr := GenericOpenAPIError{ 165 | body: localVarBody, 166 | error: err.Error(), 167 | } 168 | return localVarReturnValue, localVarHTTPResponse, newErr 169 | } 170 | 171 | return localVarReturnValue, localVarHTTPResponse, nil 172 | } 173 | 174 | /* 175 | DeleteWebhook A webhook endpoint 176 | This endpoint allows the client to delete a single webhook given its `id`. 177 | * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). 178 | * @param id Id of the webhook 179 | */ 180 | func (a *WebhooksApiService) DeleteWebhook(ctx _context.Context, id int32) (*_nethttp.Response, error) { 181 | var ( 182 | localVarHTTPMethod = _nethttp.MethodDelete 183 | localVarPostBody interface{} 184 | localVarFormFileName string 185 | localVarFileName string 186 | localVarFileBytes []byte 187 | ) 188 | 189 | // create path and map variables 190 | localVarPath := a.client.cfg.BasePath + "/webhooks/{id}" 191 | localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", _neturl.QueryEscape(fmt.Sprintf("%v", id)), -1) 192 | 193 | localVarHeaderParams := make(map[string]string) 194 | localVarQueryParams := _neturl.Values{} 195 | localVarFormParams := _neturl.Values{} 196 | 197 | // to determine the Content-Type header 198 | localVarHTTPContentTypes := []string{} 199 | 200 | // set Content-Type header 201 | localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) 202 | if localVarHTTPContentType != "" { 203 | localVarHeaderParams["Content-Type"] = localVarHTTPContentType 204 | } 205 | 206 | // to determine the Accept header 207 | localVarHTTPHeaderAccepts := []string{"application/json"} 208 | 209 | // set Accept header 210 | localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) 211 | if localVarHTTPHeaderAccept != "" { 212 | localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept 213 | } 214 | if ctx != nil { 215 | // API Key Authentication 216 | if auth, ok := ctx.Value(ContextAPIKey).(APIKey); ok { 217 | var key string 218 | if auth.Prefix != "" { 219 | key = auth.Prefix + " " + auth.Key 220 | } else { 221 | key = auth.Key 222 | } 223 | localVarHeaderParams["api-key"] = key 224 | } 225 | } 226 | r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) 227 | if err != nil { 228 | return nil, err 229 | } 230 | 231 | localVarHTTPResponse, err := a.client.callAPI(r) 232 | if err != nil || localVarHTTPResponse == nil { 233 | return localVarHTTPResponse, err 234 | } 235 | 236 | localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) 237 | localVarHTTPResponse.Body.Close() 238 | if err != nil { 239 | return localVarHTTPResponse, err 240 | } 241 | 242 | if localVarHTTPResponse.StatusCode >= 300 { 243 | newErr := GenericOpenAPIError{ 244 | body: localVarBody, 245 | error: localVarHTTPResponse.Status, 246 | } 247 | if localVarHTTPResponse.StatusCode == 401 { 248 | var v ApiError 249 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 250 | if err != nil { 251 | newErr.error = err.Error() 252 | return localVarHTTPResponse, newErr 253 | } 254 | newErr.model = v 255 | return localVarHTTPResponse, newErr 256 | } 257 | if localVarHTTPResponse.StatusCode == 404 { 258 | var v ApiError 259 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 260 | if err != nil { 261 | newErr.error = err.Error() 262 | return localVarHTTPResponse, newErr 263 | } 264 | newErr.model = v 265 | } 266 | return localVarHTTPResponse, newErr 267 | } 268 | 269 | return localVarHTTPResponse, nil 270 | } 271 | 272 | /* 273 | GetWebhookById A webhook endpoint 274 | This endpoint allows the client to retrieve a single webhook given its `id`. 275 | * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). 276 | * @param id Id of the webhook 277 | @return WebhookShow 278 | */ 279 | func (a *WebhooksApiService) GetWebhookById(ctx _context.Context, id int32) (WebhookShow, *_nethttp.Response, error) { 280 | var ( 281 | localVarHTTPMethod = _nethttp.MethodGet 282 | localVarPostBody interface{} 283 | localVarFormFileName string 284 | localVarFileName string 285 | localVarFileBytes []byte 286 | localVarReturnValue WebhookShow 287 | ) 288 | 289 | // create path and map variables 290 | localVarPath := a.client.cfg.BasePath + "/webhooks/{id}" 291 | localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", _neturl.QueryEscape(fmt.Sprintf("%v", id)), -1) 292 | 293 | localVarHeaderParams := make(map[string]string) 294 | localVarQueryParams := _neturl.Values{} 295 | localVarFormParams := _neturl.Values{} 296 | 297 | // to determine the Content-Type header 298 | localVarHTTPContentTypes := []string{} 299 | 300 | // set Content-Type header 301 | localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) 302 | if localVarHTTPContentType != "" { 303 | localVarHeaderParams["Content-Type"] = localVarHTTPContentType 304 | } 305 | 306 | // to determine the Accept header 307 | localVarHTTPHeaderAccepts := []string{"application/json"} 308 | 309 | // set Accept header 310 | localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) 311 | if localVarHTTPHeaderAccept != "" { 312 | localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept 313 | } 314 | if ctx != nil { 315 | // API Key Authentication 316 | if auth, ok := ctx.Value(ContextAPIKey).(APIKey); ok { 317 | var key string 318 | if auth.Prefix != "" { 319 | key = auth.Prefix + " " + auth.Key 320 | } else { 321 | key = auth.Key 322 | } 323 | localVarHeaderParams["api-key"] = key 324 | } 325 | } 326 | r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) 327 | if err != nil { 328 | return localVarReturnValue, nil, err 329 | } 330 | 331 | localVarHTTPResponse, err := a.client.callAPI(r) 332 | if err != nil || localVarHTTPResponse == nil { 333 | return localVarReturnValue, localVarHTTPResponse, err 334 | } 335 | 336 | localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) 337 | localVarHTTPResponse.Body.Close() 338 | if err != nil { 339 | return localVarReturnValue, localVarHTTPResponse, err 340 | } 341 | 342 | if localVarHTTPResponse.StatusCode >= 300 { 343 | newErr := GenericOpenAPIError{ 344 | body: localVarBody, 345 | error: localVarHTTPResponse.Status, 346 | } 347 | if localVarHTTPResponse.StatusCode == 200 { 348 | var v WebhookShow 349 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 350 | if err != nil { 351 | newErr.error = err.Error() 352 | return localVarReturnValue, localVarHTTPResponse, newErr 353 | } 354 | newErr.model = v 355 | return localVarReturnValue, localVarHTTPResponse, newErr 356 | } 357 | if localVarHTTPResponse.StatusCode == 401 { 358 | var v ApiError 359 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 360 | if err != nil { 361 | newErr.error = err.Error() 362 | return localVarReturnValue, localVarHTTPResponse, newErr 363 | } 364 | newErr.model = v 365 | return localVarReturnValue, localVarHTTPResponse, newErr 366 | } 367 | if localVarHTTPResponse.StatusCode == 404 { 368 | var v ApiError 369 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 370 | if err != nil { 371 | newErr.error = err.Error() 372 | return localVarReturnValue, localVarHTTPResponse, newErr 373 | } 374 | newErr.model = v 375 | } 376 | return localVarReturnValue, localVarHTTPResponse, newErr 377 | } 378 | 379 | err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 380 | if err != nil { 381 | newErr := GenericOpenAPIError{ 382 | body: localVarBody, 383 | error: err.Error(), 384 | } 385 | return localVarReturnValue, localVarHTTPResponse, newErr 386 | } 387 | 388 | return localVarReturnValue, localVarHTTPResponse, nil 389 | } 390 | 391 | /* 392 | GetWebhooks Webhooks 393 | This endpoint allows the client to retrieve a list of webhooks they have previously registered. \"Webhooks\" are used to register HTTP endpoints that will be called once a relevant event is triggered inside the web application, events like `article_created`, `article_updated`. It will return all webhooks, without pagination. 394 | * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). 395 | @return []WebhookIndex 396 | */ 397 | func (a *WebhooksApiService) GetWebhooks(ctx _context.Context) ([]WebhookIndex, *_nethttp.Response, error) { 398 | var ( 399 | localVarHTTPMethod = _nethttp.MethodGet 400 | localVarPostBody interface{} 401 | localVarFormFileName string 402 | localVarFileName string 403 | localVarFileBytes []byte 404 | localVarReturnValue []WebhookIndex 405 | ) 406 | 407 | // create path and map variables 408 | localVarPath := a.client.cfg.BasePath + "/webhooks" 409 | 410 | localVarHeaderParams := make(map[string]string) 411 | localVarQueryParams := _neturl.Values{} 412 | localVarFormParams := _neturl.Values{} 413 | 414 | // to determine the Content-Type header 415 | localVarHTTPContentTypes := []string{} 416 | 417 | // set Content-Type header 418 | localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) 419 | if localVarHTTPContentType != "" { 420 | localVarHeaderParams["Content-Type"] = localVarHTTPContentType 421 | } 422 | 423 | // to determine the Accept header 424 | localVarHTTPHeaderAccepts := []string{"application/json"} 425 | 426 | // set Accept header 427 | localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) 428 | if localVarHTTPHeaderAccept != "" { 429 | localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept 430 | } 431 | if ctx != nil { 432 | // API Key Authentication 433 | if auth, ok := ctx.Value(ContextAPIKey).(APIKey); ok { 434 | var key string 435 | if auth.Prefix != "" { 436 | key = auth.Prefix + " " + auth.Key 437 | } else { 438 | key = auth.Key 439 | } 440 | localVarHeaderParams["api-key"] = key 441 | } 442 | } 443 | r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) 444 | if err != nil { 445 | return localVarReturnValue, nil, err 446 | } 447 | 448 | localVarHTTPResponse, err := a.client.callAPI(r) 449 | if err != nil || localVarHTTPResponse == nil { 450 | return localVarReturnValue, localVarHTTPResponse, err 451 | } 452 | 453 | localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) 454 | localVarHTTPResponse.Body.Close() 455 | if err != nil { 456 | return localVarReturnValue, localVarHTTPResponse, err 457 | } 458 | 459 | if localVarHTTPResponse.StatusCode >= 300 { 460 | newErr := GenericOpenAPIError{ 461 | body: localVarBody, 462 | error: localVarHTTPResponse.Status, 463 | } 464 | if localVarHTTPResponse.StatusCode == 200 { 465 | var v []WebhookIndex 466 | err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 467 | if err != nil { 468 | newErr.error = err.Error() 469 | return localVarReturnValue, localVarHTTPResponse, newErr 470 | } 471 | newErr.model = v 472 | } 473 | return localVarReturnValue, localVarHTTPResponse, newErr 474 | } 475 | 476 | err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) 477 | if err != nil { 478 | newErr := GenericOpenAPIError{ 479 | body: localVarBody, 480 | error: err.Error(), 481 | } 482 | return localVarReturnValue, localVarHTTPResponse, newErr 483 | } 484 | 485 | return localVarReturnValue, localVarHTTPResponse, nil 486 | } 487 | -------------------------------------------------------------------------------- /pkg/devto/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | import ( 14 | "bytes" 15 | "context" 16 | "encoding/json" 17 | "encoding/xml" 18 | "errors" 19 | "fmt" 20 | "io" 21 | "log" 22 | "mime/multipart" 23 | "net/http" 24 | "net/http/httputil" 25 | "net/url" 26 | "os" 27 | "path/filepath" 28 | "reflect" 29 | "regexp" 30 | "strconv" 31 | "strings" 32 | "time" 33 | "unicode/utf8" 34 | 35 | "golang.org/x/oauth2" 36 | ) 37 | 38 | var ( 39 | jsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`) 40 | xmlCheck = regexp.MustCompile(`(?i:(?:application|text)/xml)`) 41 | ) 42 | 43 | // APIClient manages communication with the DEV API (beta) API v0.5.9 44 | // In most cases there should be only one, shared, APIClient. 45 | type APIClient struct { 46 | cfg *Configuration 47 | common service // Reuse a single struct instead of allocating one for each service on the heap. 48 | 49 | // API Services 50 | 51 | ArticlesApi *ArticlesApiService 52 | 53 | UsersApi *UsersApiService 54 | 55 | WebhooksApi *WebhooksApiService 56 | } 57 | 58 | type service struct { 59 | client *APIClient 60 | } 61 | 62 | // NewAPIClient creates a new API client. Requires a userAgent string describing your application. 63 | // optionally a custom http.Client to allow for advanced features such as caching. 64 | func NewAPIClient(cfg *Configuration) *APIClient { 65 | if cfg.HTTPClient == nil { 66 | cfg.HTTPClient = http.DefaultClient 67 | } 68 | 69 | c := &APIClient{} 70 | c.cfg = cfg 71 | c.common.client = c 72 | 73 | // API Services 74 | c.ArticlesApi = (*ArticlesApiService)(&c.common) 75 | c.UsersApi = (*UsersApiService)(&c.common) 76 | c.WebhooksApi = (*WebhooksApiService)(&c.common) 77 | 78 | return c 79 | } 80 | 81 | func atoi(in string) (int, error) { 82 | return strconv.Atoi(in) 83 | } 84 | 85 | // selectHeaderContentType select a content type from the available list. 86 | func selectHeaderContentType(contentTypes []string) string { 87 | if len(contentTypes) == 0 { 88 | return "" 89 | } 90 | if contains(contentTypes, "application/json") { 91 | return "application/json" 92 | } 93 | return contentTypes[0] // use the first content type specified in 'consumes' 94 | } 95 | 96 | // selectHeaderAccept join all accept types and return 97 | func selectHeaderAccept(accepts []string) string { 98 | if len(accepts) == 0 { 99 | return "" 100 | } 101 | 102 | if contains(accepts, "application/json") { 103 | return "application/json" 104 | } 105 | 106 | return strings.Join(accepts, ",") 107 | } 108 | 109 | // contains is a case insenstive match, finding needle in a haystack 110 | func contains(haystack []string, needle string) bool { 111 | for _, a := range haystack { 112 | if strings.ToLower(a) == strings.ToLower(needle) { 113 | return true 114 | } 115 | } 116 | return false 117 | } 118 | 119 | // Verify optional parameters are of the correct type. 120 | func typeCheckParameter(obj interface{}, expected string, name string) error { 121 | // Make sure there is an object. 122 | if obj == nil { 123 | return nil 124 | } 125 | 126 | // Check the type is as expected. 127 | if reflect.TypeOf(obj).String() != expected { 128 | return fmt.Errorf("Expected %s to be of type %s but received %s.", name, expected, reflect.TypeOf(obj).String()) 129 | } 130 | return nil 131 | } 132 | 133 | // parameterToString convert interface{} parameters to string, using a delimiter if format is provided. 134 | func parameterToString(obj interface{}, collectionFormat string) string { 135 | var delimiter string 136 | 137 | switch collectionFormat { 138 | case "pipes": 139 | delimiter = "|" 140 | case "ssv": 141 | delimiter = " " 142 | case "tsv": 143 | delimiter = "\t" 144 | case "csv": 145 | delimiter = "," 146 | } 147 | 148 | if reflect.TypeOf(obj).Kind() == reflect.Slice { 149 | return strings.Trim(strings.Replace(fmt.Sprint(obj), " ", delimiter, -1), "[]") 150 | } else if t, ok := obj.(time.Time); ok { 151 | return t.Format(time.RFC3339) 152 | } 153 | 154 | return fmt.Sprintf("%v", obj) 155 | } 156 | 157 | // helper for converting interface{} parameters to json strings 158 | func parameterToJson(obj interface{}) (string, error) { 159 | jsonBuf, err := json.Marshal(obj) 160 | if err != nil { 161 | return "", err 162 | } 163 | return string(jsonBuf), err 164 | } 165 | 166 | // callAPI do the request. 167 | func (c *APIClient) callAPI(request *http.Request) (*http.Response, error) { 168 | if c.cfg.Debug { 169 | dump, err := httputil.DumpRequestOut(request, true) 170 | if err != nil { 171 | return nil, err 172 | } 173 | log.Printf("\n%s\n", string(dump)) 174 | } 175 | 176 | resp, err := c.cfg.HTTPClient.Do(request) 177 | if err != nil { 178 | return resp, err 179 | } 180 | 181 | if c.cfg.Debug { 182 | dump, err := httputil.DumpResponse(resp, true) 183 | if err != nil { 184 | return resp, err 185 | } 186 | log.Printf("\n%s\n", string(dump)) 187 | } 188 | 189 | return resp, err 190 | } 191 | 192 | // ChangeBasePath changes base path to allow switching to mocks 193 | func (c *APIClient) ChangeBasePath(path string) { 194 | c.cfg.BasePath = path 195 | } 196 | 197 | // Allow modification of underlying config for alternate implementations and testing 198 | // Caution: modifying the configuration while live can cause data races and potentially unwanted behavior 199 | func (c *APIClient) GetConfig() *Configuration { 200 | return c.cfg 201 | } 202 | 203 | // prepareRequest build the request 204 | func (c *APIClient) prepareRequest( 205 | ctx context.Context, 206 | path string, method string, 207 | postBody interface{}, 208 | headerParams map[string]string, 209 | queryParams url.Values, 210 | formParams url.Values, 211 | formFileName string, 212 | fileName string, 213 | fileBytes []byte) (localVarRequest *http.Request, err error) { 214 | 215 | var body *bytes.Buffer 216 | 217 | // Detect postBody type and post. 218 | if postBody != nil { 219 | contentType := headerParams["Content-Type"] 220 | if contentType == "" { 221 | contentType = detectContentType(postBody) 222 | headerParams["Content-Type"] = contentType 223 | } 224 | 225 | body, err = setBody(postBody, contentType) 226 | if err != nil { 227 | return nil, err 228 | } 229 | } 230 | 231 | // add form parameters and file if available. 232 | if strings.HasPrefix(headerParams["Content-Type"], "multipart/form-data") && len(formParams) > 0 || (len(fileBytes) > 0 && fileName != "") { 233 | if body != nil { 234 | return nil, errors.New("Cannot specify postBody and multipart form at the same time.") 235 | } 236 | body = &bytes.Buffer{} 237 | w := multipart.NewWriter(body) 238 | 239 | for k, v := range formParams { 240 | for _, iv := range v { 241 | if strings.HasPrefix(k, "@") { // file 242 | err = addFile(w, k[1:], iv) 243 | if err != nil { 244 | return nil, err 245 | } 246 | } else { // form value 247 | w.WriteField(k, iv) 248 | } 249 | } 250 | } 251 | if len(fileBytes) > 0 && fileName != "" { 252 | w.Boundary() 253 | //_, fileNm := filepath.Split(fileName) 254 | part, err := w.CreateFormFile(formFileName, filepath.Base(fileName)) 255 | if err != nil { 256 | return nil, err 257 | } 258 | _, err = part.Write(fileBytes) 259 | if err != nil { 260 | return nil, err 261 | } 262 | } 263 | 264 | // Set the Boundary in the Content-Type 265 | headerParams["Content-Type"] = w.FormDataContentType() 266 | 267 | // Set Content-Length 268 | headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) 269 | w.Close() 270 | } 271 | 272 | if strings.HasPrefix(headerParams["Content-Type"], "application/x-www-form-urlencoded") && len(formParams) > 0 { 273 | if body != nil { 274 | return nil, errors.New("Cannot specify postBody and x-www-form-urlencoded form at the same time.") 275 | } 276 | body = &bytes.Buffer{} 277 | body.WriteString(formParams.Encode()) 278 | // Set Content-Length 279 | headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) 280 | } 281 | 282 | // Setup path and query parameters 283 | url, err := url.Parse(path) 284 | if err != nil { 285 | return nil, err 286 | } 287 | 288 | // Override request host, if applicable 289 | if c.cfg.Host != "" { 290 | url.Host = c.cfg.Host 291 | } 292 | 293 | // Override request scheme, if applicable 294 | if c.cfg.Scheme != "" { 295 | url.Scheme = c.cfg.Scheme 296 | } 297 | 298 | // Adding Query Param 299 | query := url.Query() 300 | for k, v := range queryParams { 301 | for _, iv := range v { 302 | query.Add(k, iv) 303 | } 304 | } 305 | 306 | // Encode the parameters. 307 | url.RawQuery = query.Encode() 308 | 309 | // Generate a new request 310 | if body != nil { 311 | localVarRequest, err = http.NewRequest(method, url.String(), body) 312 | } else { 313 | localVarRequest, err = http.NewRequest(method, url.String(), nil) 314 | } 315 | if err != nil { 316 | return nil, err 317 | } 318 | 319 | // add header parameters, if any 320 | if len(headerParams) > 0 { 321 | headers := http.Header{} 322 | for h, v := range headerParams { 323 | headers.Set(h, v) 324 | } 325 | localVarRequest.Header = headers 326 | } 327 | 328 | // Add the user agent to the request. 329 | localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent) 330 | 331 | if ctx != nil { 332 | // add context to the request 333 | localVarRequest = localVarRequest.WithContext(ctx) 334 | 335 | // Walk through any authentication. 336 | 337 | // OAuth2 authentication 338 | if tok, ok := ctx.Value(ContextOAuth2).(oauth2.TokenSource); ok { 339 | // We were able to grab an oauth2 token from the context 340 | var latestToken *oauth2.Token 341 | if latestToken, err = tok.Token(); err != nil { 342 | return nil, err 343 | } 344 | 345 | latestToken.SetAuthHeader(localVarRequest) 346 | } 347 | 348 | // Basic HTTP Authentication 349 | if auth, ok := ctx.Value(ContextBasicAuth).(BasicAuth); ok { 350 | localVarRequest.SetBasicAuth(auth.UserName, auth.Password) 351 | } 352 | 353 | // AccessToken Authentication 354 | if auth, ok := ctx.Value(ContextAccessToken).(string); ok { 355 | localVarRequest.Header.Add("Authorization", "Bearer "+auth) 356 | } 357 | } 358 | 359 | for header, value := range c.cfg.DefaultHeader { 360 | localVarRequest.Header.Add(header, value) 361 | } 362 | 363 | return localVarRequest, nil 364 | } 365 | 366 | func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) { 367 | if len(b) == 0 { 368 | return nil 369 | } 370 | if s, ok := v.(*string); ok { 371 | *s = string(b) 372 | return nil 373 | } 374 | if xmlCheck.MatchString(contentType) { 375 | if err = xml.Unmarshal(b, v); err != nil { 376 | return err 377 | } 378 | return nil 379 | } 380 | if jsonCheck.MatchString(contentType) { 381 | if err = json.Unmarshal(b, v); err != nil { 382 | return err 383 | } 384 | return nil 385 | } 386 | return errors.New("undefined response type") 387 | } 388 | 389 | // Add a file to the multipart request 390 | func addFile(w *multipart.Writer, fieldName, path string) error { 391 | file, err := os.Open(path) 392 | if err != nil { 393 | return err 394 | } 395 | defer file.Close() 396 | 397 | part, err := w.CreateFormFile(fieldName, filepath.Base(path)) 398 | if err != nil { 399 | return err 400 | } 401 | _, err = io.Copy(part, file) 402 | 403 | return err 404 | } 405 | 406 | // Prevent trying to import "fmt" 407 | func reportError(format string, a ...interface{}) error { 408 | return fmt.Errorf(format, a...) 409 | } 410 | 411 | // Set request body from an interface{} 412 | func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) { 413 | if bodyBuf == nil { 414 | bodyBuf = &bytes.Buffer{} 415 | } 416 | 417 | if reader, ok := body.(io.Reader); ok { 418 | _, err = bodyBuf.ReadFrom(reader) 419 | } else if b, ok := body.([]byte); ok { 420 | _, err = bodyBuf.Write(b) 421 | } else if s, ok := body.(string); ok { 422 | _, err = bodyBuf.WriteString(s) 423 | } else if s, ok := body.(*string); ok { 424 | _, err = bodyBuf.WriteString(*s) 425 | } else if jsonCheck.MatchString(contentType) { 426 | err = json.NewEncoder(bodyBuf).Encode(body) 427 | } else if xmlCheck.MatchString(contentType) { 428 | err = xml.NewEncoder(bodyBuf).Encode(body) 429 | } 430 | 431 | if err != nil { 432 | return nil, err 433 | } 434 | 435 | if bodyBuf.Len() == 0 { 436 | err = fmt.Errorf("Invalid body type %s\n", contentType) 437 | return nil, err 438 | } 439 | return bodyBuf, nil 440 | } 441 | 442 | // detectContentType method is used to figure out `Request.Body` content type for request header 443 | func detectContentType(body interface{}) string { 444 | contentType := "text/plain; charset=utf-8" 445 | kind := reflect.TypeOf(body).Kind() 446 | 447 | switch kind { 448 | case reflect.Struct, reflect.Map, reflect.Ptr: 449 | contentType = "application/json; charset=utf-8" 450 | case reflect.String: 451 | contentType = "text/plain; charset=utf-8" 452 | default: 453 | if b, ok := body.([]byte); ok { 454 | contentType = http.DetectContentType(b) 455 | } else if kind == reflect.Slice { 456 | contentType = "application/json; charset=utf-8" 457 | } 458 | } 459 | 460 | return contentType 461 | } 462 | 463 | // Ripped from https://github.com/gregjones/httpcache/blob/master/httpcache.go 464 | type cacheControl map[string]string 465 | 466 | func parseCacheControl(headers http.Header) cacheControl { 467 | cc := cacheControl{} 468 | ccHeader := headers.Get("Cache-Control") 469 | for _, part := range strings.Split(ccHeader, ",") { 470 | part = strings.Trim(part, " ") 471 | if part == "" { 472 | continue 473 | } 474 | if strings.ContainsRune(part, '=') { 475 | keyval := strings.Split(part, "=") 476 | cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") 477 | } else { 478 | cc[part] = "" 479 | } 480 | } 481 | return cc 482 | } 483 | 484 | // CacheExpires helper function to determine remaining time before repeating a request. 485 | func CacheExpires(r *http.Response) time.Time { 486 | // Figure out when the cache expires. 487 | var expires time.Time 488 | now, err := time.Parse(time.RFC1123, r.Header.Get("date")) 489 | if err != nil { 490 | return time.Now() 491 | } 492 | respCacheControl := parseCacheControl(r.Header) 493 | 494 | if maxAge, ok := respCacheControl["max-age"]; ok { 495 | lifetime, err := time.ParseDuration(maxAge + "s") 496 | if err != nil { 497 | expires = now 498 | } else { 499 | expires = now.Add(lifetime) 500 | } 501 | } else { 502 | expiresHeader := r.Header.Get("Expires") 503 | if expiresHeader != "" { 504 | expires, err = time.Parse(time.RFC1123, expiresHeader) 505 | if err != nil { 506 | expires = now 507 | } 508 | } 509 | } 510 | return expires 511 | } 512 | 513 | func strlen(s string) int { 514 | return utf8.RuneCountInString(s) 515 | } 516 | 517 | // GenericOpenAPIError Provides access to the body, error and model on returned errors. 518 | type GenericOpenAPIError struct { 519 | body []byte 520 | error string 521 | model interface{} 522 | } 523 | 524 | // Error returns non-empty string if there was an error. 525 | func (e GenericOpenAPIError) Error() string { 526 | return e.error 527 | } 528 | 529 | // Body returns the raw bytes of the response 530 | func (e GenericOpenAPIError) Body() []byte { 531 | return e.body 532 | } 533 | 534 | // Model returns the unpacked model of the error 535 | func (e GenericOpenAPIError) Model() interface{} { 536 | return e.model 537 | } 538 | -------------------------------------------------------------------------------- /pkg/devto/configuration.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | import ( 14 | "net/http" 15 | ) 16 | 17 | // contextKeys are used to identify the type of value in the context. 18 | // Since these are string, it is possible to get a short description of the 19 | // context key for logging and debugging using key.String(). 20 | 21 | type contextKey string 22 | 23 | func (c contextKey) String() string { 24 | return "auth " + string(c) 25 | } 26 | 27 | var ( 28 | // ContextOAuth2 takes an oauth2.TokenSource as authentication for the request. 29 | ContextOAuth2 = contextKey("token") 30 | 31 | // ContextBasicAuth takes BasicAuth as authentication for the request. 32 | ContextBasicAuth = contextKey("basic") 33 | 34 | // ContextAccessToken takes a string oauth2 access token as authentication for the request. 35 | ContextAccessToken = contextKey("accesstoken") 36 | 37 | // ContextAPIKey takes an APIKey as authentication for the request 38 | ContextAPIKey = contextKey("apikey") 39 | ) 40 | 41 | // BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth 42 | type BasicAuth struct { 43 | UserName string `json:"userName,omitempty"` 44 | Password string `json:"password,omitempty"` 45 | } 46 | 47 | // APIKey provides API key based authentication to a request passed via context using ContextAPIKey 48 | type APIKey struct { 49 | Key string 50 | Prefix string 51 | } 52 | 53 | // Configuration stores the configuration of the API client 54 | type Configuration struct { 55 | BasePath string `json:"basePath,omitempty"` 56 | Host string `json:"host,omitempty"` 57 | Scheme string `json:"scheme,omitempty"` 58 | DefaultHeader map[string]string `json:"defaultHeader,omitempty"` 59 | UserAgent string `json:"userAgent,omitempty"` 60 | Debug bool `json:"debug,omitempty"` 61 | HTTPClient *http.Client 62 | } 63 | 64 | // NewConfiguration returns a new Configuration object 65 | func NewConfiguration() *Configuration { 66 | cfg := &Configuration{ 67 | BasePath: "https://dev.to/api", 68 | DefaultHeader: make(map[string]string), 69 | UserAgent: "OpenAPI-Generator/1.0.0/go", 70 | Debug: false, 71 | } 72 | return cfg 73 | } 74 | 75 | // AddDefaultHeader adds a new HTTP header to the default header in the request 76 | func (c *Configuration) AddDefaultHeader(key string, value string) { 77 | c.DefaultHeader[key] = value 78 | } 79 | -------------------------------------------------------------------------------- /pkg/devto/docs/ApiError.md: -------------------------------------------------------------------------------- 1 | # ApiError 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **Error** | **string** | | 8 | **Status** | **int32** | | 9 | 10 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 11 | 12 | 13 | -------------------------------------------------------------------------------- /pkg/devto/docs/ArticleCreate.md: -------------------------------------------------------------------------------- 1 | # ArticleCreate 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **Article** | [**ArticleCreateArticle**](ArticleCreate_article.md) | | [optional] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | 12 | -------------------------------------------------------------------------------- /pkg/devto/docs/ArticleCreateArticle.md: -------------------------------------------------------------------------------- 1 | # ArticleCreateArticle 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **Title** | **string** | | 8 | **BodyMarkdown** | **string** | The body of the article. It can contain an optional front matter. For example ```markdown --- title: Hello, World! published: true tags: discuss, help date: 20190701T10:00Z series: Hello series canonical_url: https://example.com/blog/hello cover_image: article_published_cover_image --- ``` `date`, `series` and `canonical_url` are optional. `date` is the publication date-time `series` is the name of the series the article belongs to `canonical_url` is the canonical URL of the article `cover_image` is the main image of the article *If the markdown contains a front matter, it will take precedence on the equivalent params given in the JSON payload.* | [optional] 9 | **Published** | **bool** | True to create a published article, false otherwise. Defaults to false | [optional] 10 | **Series** | **string** | Article series name. All articles belonging to the same series need to have the same name in this parameter. | [optional] 11 | **MainImage** | **string** | | [optional] 12 | **CanonicalUrl** | **string** | | [optional] 13 | **Description** | **string** | | [optional] 14 | **Tags** | **[]string** | | [optional] 15 | **OrganizationId** | **int32** | Only users belonging to an organization can assign the article to it | [optional] 16 | 17 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 18 | 19 | 20 | -------------------------------------------------------------------------------- /pkg/devto/docs/ArticleFlareTag.md: -------------------------------------------------------------------------------- 1 | # ArticleFlareTag 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **Name** | **string** | | [optional] 8 | **BgColorHex** | **string** | Background color (hexadecimal) | [optional] 9 | **TextColorHex** | **string** | Text color (hexadecimal) | [optional] 10 | 11 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 12 | 13 | 14 | -------------------------------------------------------------------------------- /pkg/devto/docs/ArticleIndex.md: -------------------------------------------------------------------------------- 1 | # ArticleIndex 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **TypeOf** | **string** | | 8 | **Id** | **int32** | | 9 | **Title** | **string** | | 10 | **Description** | **string** | | 11 | **CoverImage** | **string** | | 12 | **ReadablePublishDate** | **string** | | 13 | **SocialImage** | **string** | | 14 | **TagList** | **[]string** | | 15 | **Tags** | **string** | | 16 | **Slug** | **string** | | 17 | **Path** | **string** | | 18 | **Url** | **string** | | 19 | **CanonicalUrl** | **string** | | 20 | **CommentsCount** | **int32** | | 21 | **PositiveReactionsCount** | **int32** | | 22 | **CreatedAt** | [**time.Time**](time.Time.md) | | 23 | **EditedAt** | [**time.Time**](time.Time.md) | | 24 | **CrosspostedAt** | [**time.Time**](time.Time.md) | | 25 | **PublishedAt** | [**time.Time**](time.Time.md) | | 26 | **LastCommentAt** | [**time.Time**](time.Time.md) | | 27 | **PublishedTimestamp** | [**time.Time**](time.Time.md) | Crossposting or published date time | 28 | **User** | [**ArticleUser**](ArticleUser.md) | | 29 | **Organization** | [**ArticleOrganization**](ArticleOrganization.md) | | [optional] 30 | **FlareTag** | [**ArticleFlareTag**](ArticleFlareTag.md) | | [optional] 31 | 32 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 33 | 34 | 35 | -------------------------------------------------------------------------------- /pkg/devto/docs/ArticleMe.md: -------------------------------------------------------------------------------- 1 | # ArticleMe 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **TypeOf** | **string** | | 8 | **Id** | **int32** | | 9 | **Title** | **string** | | 10 | **Description** | **string** | | 11 | **CoverImage** | **string** | | 12 | **Published** | **bool** | | 13 | **PublishedAt** | [**time.Time**](time.Time.md) | | 14 | **TagList** | **[]string** | | 15 | **Slug** | **string** | | 16 | **Path** | **string** | | 17 | **Url** | **string** | | 18 | **CanonicalUrl** | **string** | | 19 | **CommentsCount** | **int32** | | 20 | **PositiveReactionsCount** | **int32** | | 21 | **PageViewsCount** | **int32** | | 22 | **PublishedTimestamp** | [**time.Time**](time.Time.md) | Crossposting or published date time | 23 | **User** | [**ArticleUser**](ArticleUser.md) | | 24 | **Organization** | [**ArticleOrganization**](ArticleOrganization.md) | | [optional] 25 | **FlareTag** | [**ArticleFlareTag**](ArticleFlareTag.md) | | [optional] 26 | 27 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 28 | 29 | 30 | -------------------------------------------------------------------------------- /pkg/devto/docs/ArticleOrganization.md: -------------------------------------------------------------------------------- 1 | # ArticleOrganization 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **Name** | **string** | | [optional] 8 | **Username** | **string** | | [optional] 9 | **Slug** | **string** | | [optional] 10 | **ProfileImage** | **string** | Profile image (640x640) | [optional] 11 | **ProfileImage90** | **string** | Profile image (90x90) | [optional] 12 | 13 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 14 | 15 | 16 | -------------------------------------------------------------------------------- /pkg/devto/docs/ArticleShow.md: -------------------------------------------------------------------------------- 1 | # ArticleShow 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **TypeOf** | **string** | | 8 | **Id** | **int32** | | 9 | **Title** | **string** | | 10 | **Description** | **string** | | 11 | **CoverImage** | **string** | | 12 | **ReadablePublishDate** | **string** | | 13 | **SocialImage** | **string** | | 14 | **TagList** | **string** | | 15 | **Tags** | **[]string** | | 16 | **Slug** | **string** | | 17 | **Path** | **string** | | 18 | **Url** | **string** | | 19 | **CanonicalUrl** | **string** | | 20 | **CommentsCount** | **int32** | | 21 | **PositiveReactionsCount** | **int32** | | 22 | **CreatedAt** | [**time.Time**](time.Time.md) | | 23 | **EditedAt** | [**time.Time**](time.Time.md) | | 24 | **CrosspostedAt** | [**time.Time**](time.Time.md) | | 25 | **PublishedAt** | [**time.Time**](time.Time.md) | | 26 | **LastCommentAt** | [**time.Time**](time.Time.md) | | 27 | **PublishedTimestamp** | [**time.Time**](time.Time.md) | Crossposting or published date time | 28 | **BodyHtml** | **string** | | 29 | **BodyMarkdown** | **string** | | 30 | **User** | [**ArticleUser**](ArticleUser.md) | | 31 | **Organization** | [**ArticleOrganization**](ArticleOrganization.md) | | [optional] 32 | **FlareTag** | [**ArticleFlareTag**](ArticleFlareTag.md) | | [optional] 33 | 34 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 35 | 36 | 37 | -------------------------------------------------------------------------------- /pkg/devto/docs/ArticleUpdate.md: -------------------------------------------------------------------------------- 1 | # ArticleUpdate 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **Article** | [**ArticleUpdateArticle**](ArticleUpdate_article.md) | | [optional] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | 12 | -------------------------------------------------------------------------------- /pkg/devto/docs/ArticleUpdateArticle.md: -------------------------------------------------------------------------------- 1 | # ArticleUpdateArticle 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **Title** | **string** | | [optional] 8 | **BodyMarkdown** | **string** | The body of the article. It can contain an optional front matter. For example ```markdown --- title: Hello, World! published: true tags: discuss, help date: 20190701T10:00Z series: Hello series canonical_url: https://example.com/blog/hello cover_image: article_published_cover_image --- ``` `date`, `series` and `canonical_url` are optional. `date` is the publication date-time `series` is the name of the series the article belongs to `canonical_url` is the canonical URL of the article `cover_image` is the main image of the article *If the markdown contains a front matter, it will take precedence on the equivalent params given in the JSON payload.* | [optional] 9 | **Published** | **bool** | True to create a published article, false otherwise. Defaults to false | [optional] 10 | **Series** | **string** | Article series name. All articles belonging to the same series need to have the same name in this parameter. To remove an article from a series, the `null` value can be used. | [optional] 11 | **MainImage** | **string** | | [optional] 12 | **CanonicalUrl** | **string** | | [optional] 13 | **Description** | **string** | | [optional] 14 | **Tags** | **[]string** | | [optional] 15 | **OrganizationId** | **int32** | Only users belonging to an organization can assign the article to it | [optional] 16 | 17 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 18 | 19 | 20 | -------------------------------------------------------------------------------- /pkg/devto/docs/ArticleUser.md: -------------------------------------------------------------------------------- 1 | # ArticleUser 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **Name** | **string** | | [optional] 8 | **Username** | **string** | | [optional] 9 | **TwitterUsername** | **string** | | [optional] 10 | **GithubUsername** | **string** | | [optional] 11 | **WebsiteUrl** | **string** | | [optional] 12 | **ProfileImage** | **string** | Profile image (640x640) | [optional] 13 | **ProfileImage90** | **string** | Profile image (90x90) | [optional] 14 | 15 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 16 | 17 | 18 | -------------------------------------------------------------------------------- /pkg/devto/docs/ArticlesApi.md: -------------------------------------------------------------------------------- 1 | # \ArticlesApi 2 | 3 | All URIs are relative to *https://dev.to/api* 4 | 5 | Method | HTTP request | Description 6 | ------------- | ------------- | ------------- 7 | [**CreateArticle**](ArticlesApi.md#CreateArticle) | **Post** /articles | Create a new article 8 | [**GetArticleById**](ArticlesApi.md#GetArticleById) | **Get** /articles/{id} | A published article 9 | [**GetArticles**](ArticlesApi.md#GetArticles) | **Get** /articles | Published articles 10 | [**GetUserAllArticles**](ArticlesApi.md#GetUserAllArticles) | **Get** /articles/me/all | User's all articles 11 | [**GetUserArticles**](ArticlesApi.md#GetUserArticles) | **Get** /articles/me | User's articles 12 | [**GetUserPublishedArticles**](ArticlesApi.md#GetUserPublishedArticles) | **Get** /articles/me/published | User's published articles 13 | [**GetUserUnpublishedArticles**](ArticlesApi.md#GetUserUnpublishedArticles) | **Get** /articles/me/unpublished | User's unpublished articles 14 | [**UpdateArticle**](ArticlesApi.md#UpdateArticle) | **Put** /articles/{id} | Update an article 15 | 16 | 17 | 18 | ## CreateArticle 19 | 20 | > ArticleShow CreateArticle(ctx, optional) 21 | 22 | Create a new article 23 | 24 | This endpoint allows the client to create a new article. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. ### Rate limiting There is a limit of 10 articles created each 30 seconds by the same user. ### Additional resources - [Rails tests for Articles API](https://github.com/thepracticaldev/dev.to/blob/master/spec/requests/api/v0/articles_spec.rb) 25 | 26 | ### Required Parameters 27 | 28 | 29 | Name | Type | Description | Notes 30 | ------------- | ------------- | ------------- | ------------- 31 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 32 | **optional** | ***CreateArticleOpts** | optional parameters | nil if no parameters 33 | 34 | ### Optional Parameters 35 | 36 | Optional parameters are passed through a pointer to a CreateArticleOpts struct 37 | 38 | 39 | Name | Type | Description | Notes 40 | ------------- | ------------- | ------------- | ------------- 41 | **articleCreate** | [**optional.Interface of ArticleCreate**](ArticleCreate.md)| Article to create | 42 | 43 | ### Return type 44 | 45 | [**ArticleShow**](ArticleShow.md) 46 | 47 | ### Authorization 48 | 49 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 50 | 51 | ### HTTP request headers 52 | 53 | - **Content-Type**: application/json 54 | - **Accept**: application/json 55 | 56 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 57 | [[Back to Model list]](../README.md#documentation-for-models) 58 | [[Back to README]](../README.md) 59 | 60 | 61 | ## GetArticleById 62 | 63 | > ArticleShow GetArticleById(ctx, id) 64 | 65 | A published article 66 | 67 | This endpoint allows the client to retrieve a single published article given its `id`. Responses are cached for 5 minutes. 68 | 69 | ### Required Parameters 70 | 71 | 72 | Name | Type | Description | Notes 73 | ------------- | ------------- | ------------- | ------------- 74 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 75 | **id** | **int32**| Id of the article | 76 | 77 | ### Return type 78 | 79 | [**ArticleShow**](ArticleShow.md) 80 | 81 | ### Authorization 82 | 83 | No authorization required 84 | 85 | ### HTTP request headers 86 | 87 | - **Content-Type**: Not defined 88 | - **Accept**: application/json 89 | 90 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 91 | [[Back to Model list]](../README.md#documentation-for-models) 92 | [[Back to README]](../README.md) 93 | 94 | 95 | ## GetArticles 96 | 97 | > []ArticleIndex GetArticles(ctx, optional) 98 | 99 | Published articles 100 | 101 | This endpoint allows the client to retrieve a list of articles. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. By default it will return featured, published articles ordered by descending popularity. Each page will contain `30` articles. Responses, according to the combination of params, are cached for 24 hours. 102 | 103 | ### Required Parameters 104 | 105 | 106 | Name | Type | Description | Notes 107 | ------------- | ------------- | ------------- | ------------- 108 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 109 | **optional** | ***GetArticlesOpts** | optional parameters | nil if no parameters 110 | 111 | ### Optional Parameters 112 | 113 | Optional parameters are passed through a pointer to a GetArticlesOpts struct 114 | 115 | 116 | Name | Type | Description | Notes 117 | ------------- | ------------- | ------------- | ------------- 118 | **page** | **optional.Int32**| Pagination page. This param can be used in conjuction with all other params (except when asking for fresh and rising articles by themselves). | 119 | **tag** | **optional.String**| Adding this parameter will return articles that contain the requested tag. This param can be used by itself, with `page` or with `top`. | 120 | **username** | **optional.String**| Adding this parameter will return articles belonging to a User or Organization ordered by descending `published_at`. If `state=all` the number of items returned will be `1000` instead of the default `30`. This param can be used by itself or only with `page` and `state`. | 121 | **state** | **optional.String**| Adding this will allow the client to check which articles are fresh or rising. If `state=fresh` the server will return published fresh articles. If `state=rising` the server will return published rising articles. This param can only be used by itself or with `username` if set to `all`. | 122 | **top** | **optional.Int32**| Adding this will allow the client to return the most popular articles in the last `N` days. `top` indicates the number of days since publication of the articles returned. This param can only be used by itself or with `tag`. | 123 | 124 | ### Return type 125 | 126 | [**[]ArticleIndex**](ArticleIndex.md) 127 | 128 | ### Authorization 129 | 130 | No authorization required 131 | 132 | ### HTTP request headers 133 | 134 | - **Content-Type**: Not defined 135 | - **Accept**: application/json 136 | 137 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 138 | [[Back to Model list]](../README.md#documentation-for-models) 139 | [[Back to README]](../README.md) 140 | 141 | 142 | ## GetUserAllArticles 143 | 144 | > []ArticleMe GetUserAllArticles(ctx, optional) 145 | 146 | User's all articles 147 | 148 | This endpoint allows the client to retrieve a list of all articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. It will return both published and unpublished articles with pagination. Unpublished articles will be at the top of the list in reverse chronological creation order. Published articles will follow in reverse chronological publication order. By default a page will contain `30` articles. 149 | 150 | ### Required Parameters 151 | 152 | 153 | Name | Type | Description | Notes 154 | ------------- | ------------- | ------------- | ------------- 155 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 156 | **optional** | ***GetUserAllArticlesOpts** | optional parameters | nil if no parameters 157 | 158 | ### Optional Parameters 159 | 160 | Optional parameters are passed through a pointer to a GetUserAllArticlesOpts struct 161 | 162 | 163 | Name | Type | Description | Notes 164 | ------------- | ------------- | ------------- | ------------- 165 | **page** | **optional.Int32**| Pagination page. | 166 | **perPage** | **optional.Int32**| Page size (defaults to 30 with a maximum of 1000). | 167 | 168 | ### Return type 169 | 170 | [**[]ArticleMe**](ArticleMe.md) 171 | 172 | ### Authorization 173 | 174 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 175 | 176 | ### HTTP request headers 177 | 178 | - **Content-Type**: Not defined 179 | - **Accept**: application/json 180 | 181 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 182 | [[Back to Model list]](../README.md#documentation-for-models) 183 | [[Back to README]](../README.md) 184 | 185 | 186 | ## GetUserArticles 187 | 188 | > []ArticleMe GetUserArticles(ctx, optional) 189 | 190 | User's articles 191 | 192 | This endpoint allows the client to retrieve a list of published articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. Published articles will be in reverse chronological publication order. It will return published articles with pagination. By default a page will contain `30` articles. 193 | 194 | ### Required Parameters 195 | 196 | 197 | Name | Type | Description | Notes 198 | ------------- | ------------- | ------------- | ------------- 199 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 200 | **optional** | ***GetUserArticlesOpts** | optional parameters | nil if no parameters 201 | 202 | ### Optional Parameters 203 | 204 | Optional parameters are passed through a pointer to a GetUserArticlesOpts struct 205 | 206 | 207 | Name | Type | Description | Notes 208 | ------------- | ------------- | ------------- | ------------- 209 | **page** | **optional.Int32**| Pagination page. | 210 | **perPage** | **optional.Int32**| Page size (defaults to 30 with a maximum of 1000). | 211 | 212 | ### Return type 213 | 214 | [**[]ArticleMe**](ArticleMe.md) 215 | 216 | ### Authorization 217 | 218 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 219 | 220 | ### HTTP request headers 221 | 222 | - **Content-Type**: Not defined 223 | - **Accept**: application/json 224 | 225 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 226 | [[Back to Model list]](../README.md#documentation-for-models) 227 | [[Back to README]](../README.md) 228 | 229 | 230 | ## GetUserPublishedArticles 231 | 232 | > []ArticleMe GetUserPublishedArticles(ctx, optional) 233 | 234 | User's published articles 235 | 236 | This endpoint allows the client to retrieve a list of published articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. Published articles will be in reverse chronological publication order. It will return published articles with pagination. By default a page will contain `30` articles. 237 | 238 | ### Required Parameters 239 | 240 | 241 | Name | Type | Description | Notes 242 | ------------- | ------------- | ------------- | ------------- 243 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 244 | **optional** | ***GetUserPublishedArticlesOpts** | optional parameters | nil if no parameters 245 | 246 | ### Optional Parameters 247 | 248 | Optional parameters are passed through a pointer to a GetUserPublishedArticlesOpts struct 249 | 250 | 251 | Name | Type | Description | Notes 252 | ------------- | ------------- | ------------- | ------------- 253 | **page** | **optional.Int32**| Pagination page. | 254 | **perPage** | **optional.Int32**| Page size (defaults to 30 with a maximum of 1000). | 255 | 256 | ### Return type 257 | 258 | [**[]ArticleMe**](ArticleMe.md) 259 | 260 | ### Authorization 261 | 262 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 263 | 264 | ### HTTP request headers 265 | 266 | - **Content-Type**: Not defined 267 | - **Accept**: application/json 268 | 269 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 270 | [[Back to Model list]](../README.md#documentation-for-models) 271 | [[Back to README]](../README.md) 272 | 273 | 274 | ## GetUserUnpublishedArticles 275 | 276 | > []ArticleMe GetUserUnpublishedArticles(ctx, optional) 277 | 278 | User's unpublished articles 279 | 280 | This endpoint allows the client to retrieve a list of unpublished articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. Unpublished articles will be in reverse chronological creation order. It will return unpublished articles with pagination. By default a page will contain `30` articles. 281 | 282 | ### Required Parameters 283 | 284 | 285 | Name | Type | Description | Notes 286 | ------------- | ------------- | ------------- | ------------- 287 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 288 | **optional** | ***GetUserUnpublishedArticlesOpts** | optional parameters | nil if no parameters 289 | 290 | ### Optional Parameters 291 | 292 | Optional parameters are passed through a pointer to a GetUserUnpublishedArticlesOpts struct 293 | 294 | 295 | Name | Type | Description | Notes 296 | ------------- | ------------- | ------------- | ------------- 297 | **page** | **optional.Int32**| Pagination page. | 298 | **perPage** | **optional.Int32**| Page size (defaults to 30 with a maximum of 1000). | 299 | 300 | ### Return type 301 | 302 | [**[]ArticleMe**](ArticleMe.md) 303 | 304 | ### Authorization 305 | 306 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 307 | 308 | ### HTTP request headers 309 | 310 | - **Content-Type**: Not defined 311 | - **Accept**: application/json 312 | 313 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 314 | [[Back to Model list]](../README.md#documentation-for-models) 315 | [[Back to README]](../README.md) 316 | 317 | 318 | ## UpdateArticle 319 | 320 | > ArticleShow UpdateArticle(ctx, id, optional) 321 | 322 | Update an article 323 | 324 | This endpoint allows the client to updated an existing article. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. ### Rate limiting There are no limits on the amount of updates. ### Additional resources - [Rails tests for Articles API](https://github.com/thepracticaldev/dev.to/blob/master/spec/requests/api/v0/articles_spec.rb) 325 | 326 | ### Required Parameters 327 | 328 | 329 | Name | Type | Description | Notes 330 | ------------- | ------------- | ------------- | ------------- 331 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 332 | **id** | **int32**| Id of the article | 333 | **optional** | ***UpdateArticleOpts** | optional parameters | nil if no parameters 334 | 335 | ### Optional Parameters 336 | 337 | Optional parameters are passed through a pointer to a UpdateArticleOpts struct 338 | 339 | 340 | Name | Type | Description | Notes 341 | ------------- | ------------- | ------------- | ------------- 342 | 343 | **articleUpdate** | [**optional.Interface of ArticleUpdate**](ArticleUpdate.md)| Article params to update. *Note: if the article contains a front matter in its body, its front matter properties will still take precedence over any JSON equivalent params, which means that the full body_markdown with the modified front matter params needs to be provided for an update to be successful* | 344 | 345 | ### Return type 346 | 347 | [**ArticleShow**](ArticleShow.md) 348 | 349 | ### Authorization 350 | 351 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 352 | 353 | ### HTTP request headers 354 | 355 | - **Content-Type**: application/json 356 | - **Accept**: application/json 357 | 358 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 359 | [[Back to Model list]](../README.md#documentation-for-models) 360 | [[Back to README]](../README.md) 361 | 362 | -------------------------------------------------------------------------------- /pkg/devto/docs/UsersApi.md: -------------------------------------------------------------------------------- 1 | # \UsersApi 2 | 3 | All URIs are relative to *https://dev.to/api* 4 | 5 | Method | HTTP request | Description 6 | ------------- | ------------- | ------------- 7 | [**GetUserAllArticles**](UsersApi.md#GetUserAllArticles) | **Get** /articles/me/all | User's all articles 8 | [**GetUserArticles**](UsersApi.md#GetUserArticles) | **Get** /articles/me | User's articles 9 | [**GetUserPublishedArticles**](UsersApi.md#GetUserPublishedArticles) | **Get** /articles/me/published | User's published articles 10 | [**GetUserUnpublishedArticles**](UsersApi.md#GetUserUnpublishedArticles) | **Get** /articles/me/unpublished | User's unpublished articles 11 | 12 | 13 | 14 | ## GetUserAllArticles 15 | 16 | > []ArticleMe GetUserAllArticles(ctx, optional) 17 | 18 | User's all articles 19 | 20 | This endpoint allows the client to retrieve a list of all articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. It will return both published and unpublished articles with pagination. Unpublished articles will be at the top of the list in reverse chronological creation order. Published articles will follow in reverse chronological publication order. By default a page will contain `30` articles. 21 | 22 | ### Required Parameters 23 | 24 | 25 | Name | Type | Description | Notes 26 | ------------- | ------------- | ------------- | ------------- 27 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 28 | **optional** | ***GetUserAllArticlesOpts** | optional parameters | nil if no parameters 29 | 30 | ### Optional Parameters 31 | 32 | Optional parameters are passed through a pointer to a GetUserAllArticlesOpts struct 33 | 34 | 35 | Name | Type | Description | Notes 36 | ------------- | ------------- | ------------- | ------------- 37 | **page** | **optional.Int32**| Pagination page. | 38 | **perPage** | **optional.Int32**| Page size (defaults to 30 with a maximum of 1000). | 39 | 40 | ### Return type 41 | 42 | [**[]ArticleMe**](ArticleMe.md) 43 | 44 | ### Authorization 45 | 46 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 47 | 48 | ### HTTP request headers 49 | 50 | - **Content-Type**: Not defined 51 | - **Accept**: application/json 52 | 53 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 54 | [[Back to Model list]](../README.md#documentation-for-models) 55 | [[Back to README]](../README.md) 56 | 57 | 58 | ## GetUserArticles 59 | 60 | > []ArticleMe GetUserArticles(ctx, optional) 61 | 62 | User's articles 63 | 64 | This endpoint allows the client to retrieve a list of published articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. Published articles will be in reverse chronological publication order. It will return published articles with pagination. By default a page will contain `30` articles. 65 | 66 | ### Required Parameters 67 | 68 | 69 | Name | Type | Description | Notes 70 | ------------- | ------------- | ------------- | ------------- 71 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 72 | **optional** | ***GetUserArticlesOpts** | optional parameters | nil if no parameters 73 | 74 | ### Optional Parameters 75 | 76 | Optional parameters are passed through a pointer to a GetUserArticlesOpts struct 77 | 78 | 79 | Name | Type | Description | Notes 80 | ------------- | ------------- | ------------- | ------------- 81 | **page** | **optional.Int32**| Pagination page. | 82 | **perPage** | **optional.Int32**| Page size (defaults to 30 with a maximum of 1000). | 83 | 84 | ### Return type 85 | 86 | [**[]ArticleMe**](ArticleMe.md) 87 | 88 | ### Authorization 89 | 90 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 91 | 92 | ### HTTP request headers 93 | 94 | - **Content-Type**: Not defined 95 | - **Accept**: application/json 96 | 97 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 98 | [[Back to Model list]](../README.md#documentation-for-models) 99 | [[Back to README]](../README.md) 100 | 101 | 102 | ## GetUserPublishedArticles 103 | 104 | > []ArticleMe GetUserPublishedArticles(ctx, optional) 105 | 106 | User's published articles 107 | 108 | This endpoint allows the client to retrieve a list of published articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. Published articles will be in reverse chronological publication order. It will return published articles with pagination. By default a page will contain `30` articles. 109 | 110 | ### Required Parameters 111 | 112 | 113 | Name | Type | Description | Notes 114 | ------------- | ------------- | ------------- | ------------- 115 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 116 | **optional** | ***GetUserPublishedArticlesOpts** | optional parameters | nil if no parameters 117 | 118 | ### Optional Parameters 119 | 120 | Optional parameters are passed through a pointer to a GetUserPublishedArticlesOpts struct 121 | 122 | 123 | Name | Type | Description | Notes 124 | ------------- | ------------- | ------------- | ------------- 125 | **page** | **optional.Int32**| Pagination page. | 126 | **perPage** | **optional.Int32**| Page size (defaults to 30 with a maximum of 1000). | 127 | 128 | ### Return type 129 | 130 | [**[]ArticleMe**](ArticleMe.md) 131 | 132 | ### Authorization 133 | 134 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 135 | 136 | ### HTTP request headers 137 | 138 | - **Content-Type**: Not defined 139 | - **Accept**: application/json 140 | 141 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 142 | [[Back to Model list]](../README.md#documentation-for-models) 143 | [[Back to README]](../README.md) 144 | 145 | 146 | ## GetUserUnpublishedArticles 147 | 148 | > []ArticleMe GetUserUnpublishedArticles(ctx, optional) 149 | 150 | User's unpublished articles 151 | 152 | This endpoint allows the client to retrieve a list of unpublished articles on behalf of an authenticated user. \"Articles\" are all the posts that users create on DEV that typically show up in the feed. They can be a blog post, a discussion question, a help thread etc. but is referred to as article within the code. Unpublished articles will be in reverse chronological creation order. It will return unpublished articles with pagination. By default a page will contain `30` articles. 153 | 154 | ### Required Parameters 155 | 156 | 157 | Name | Type | Description | Notes 158 | ------------- | ------------- | ------------- | ------------- 159 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 160 | **optional** | ***GetUserUnpublishedArticlesOpts** | optional parameters | nil if no parameters 161 | 162 | ### Optional Parameters 163 | 164 | Optional parameters are passed through a pointer to a GetUserUnpublishedArticlesOpts struct 165 | 166 | 167 | Name | Type | Description | Notes 168 | ------------- | ------------- | ------------- | ------------- 169 | **page** | **optional.Int32**| Pagination page. | 170 | **perPage** | **optional.Int32**| Page size (defaults to 30 with a maximum of 1000). | 171 | 172 | ### Return type 173 | 174 | [**[]ArticleMe**](ArticleMe.md) 175 | 176 | ### Authorization 177 | 178 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 179 | 180 | ### HTTP request headers 181 | 182 | - **Content-Type**: Not defined 183 | - **Accept**: application/json 184 | 185 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 186 | [[Back to Model list]](../README.md#documentation-for-models) 187 | [[Back to README]](../README.md) 188 | 189 | -------------------------------------------------------------------------------- /pkg/devto/docs/WebhookCreate.md: -------------------------------------------------------------------------------- 1 | # WebhookCreate 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **WebhookEndpoint** | [**WebhookCreateWebhookEndpoint**](WebhookCreate_webhook_endpoint.md) | | [optional] 8 | 9 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 10 | 11 | 12 | -------------------------------------------------------------------------------- /pkg/devto/docs/WebhookCreateWebhookEndpoint.md: -------------------------------------------------------------------------------- 1 | # WebhookCreateWebhookEndpoint 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **Source** | **string** | The name of the requester, eg. \"DEV\" | 8 | **TargetUrl** | **string** | | 9 | **Events** | **[]string** | An array of events identifiers | 10 | 11 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 12 | 13 | 14 | -------------------------------------------------------------------------------- /pkg/devto/docs/WebhookIndex.md: -------------------------------------------------------------------------------- 1 | # WebhookIndex 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **TypeOf** | **string** | | [optional] 8 | **Id** | **int64** | | [optional] 9 | **Source** | **string** | The name of the requester, eg. \"DEV\" | [optional] 10 | **TargetUrl** | **string** | | [optional] 11 | **Events** | **[]string** | An array of events identifiers | [optional] 12 | **CreatedAt** | [**time.Time**](time.Time.md) | | [optional] 13 | 14 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 15 | 16 | 17 | -------------------------------------------------------------------------------- /pkg/devto/docs/WebhookShow.md: -------------------------------------------------------------------------------- 1 | # WebhookShow 2 | 3 | ## Properties 4 | 5 | Name | Type | Description | Notes 6 | ------------ | ------------- | ------------- | ------------- 7 | **TypeOf** | **string** | | [optional] 8 | **Id** | **int64** | | [optional] 9 | **Source** | **string** | The name of the requester, eg. \"DEV\" | [optional] 10 | **TargetUrl** | **string** | | [optional] 11 | **Events** | **[]string** | An array of events identifiers | [optional] 12 | **CreatedAt** | [**time.Time**](time.Time.md) | | [optional] 13 | **User** | [**ArticleUser**](ArticleUser.md) | | [optional] 14 | 15 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 16 | 17 | 18 | -------------------------------------------------------------------------------- /pkg/devto/docs/WebhooksApi.md: -------------------------------------------------------------------------------- 1 | # \WebhooksApi 2 | 3 | All URIs are relative to *https://dev.to/api* 4 | 5 | Method | HTTP request | Description 6 | ------------- | ------------- | ------------- 7 | [**CreateWebhook**](WebhooksApi.md#CreateWebhook) | **Post** /webhooks | Create a new webhook 8 | [**DeleteWebhook**](WebhooksApi.md#DeleteWebhook) | **Delete** /webhooks/{id} | A webhook endpoint 9 | [**GetWebhookById**](WebhooksApi.md#GetWebhookById) | **Get** /webhooks/{id} | A webhook endpoint 10 | [**GetWebhooks**](WebhooksApi.md#GetWebhooks) | **Get** /webhooks | Webhooks 11 | 12 | 13 | 14 | ## CreateWebhook 15 | 16 | > WebhookShow CreateWebhook(ctx, optional) 17 | 18 | Create a new webhook 19 | 20 | This endpoint allows the client to create a new webhook. \"Webhooks\" are used to register HTTP endpoints that will be called once a relevant event is triggered inside the web application, events like `article_created`, `article_updated`. 21 | 22 | ### Required Parameters 23 | 24 | 25 | Name | Type | Description | Notes 26 | ------------- | ------------- | ------------- | ------------- 27 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 28 | **optional** | ***CreateWebhookOpts** | optional parameters | nil if no parameters 29 | 30 | ### Optional Parameters 31 | 32 | Optional parameters are passed through a pointer to a CreateWebhookOpts struct 33 | 34 | 35 | Name | Type | Description | Notes 36 | ------------- | ------------- | ------------- | ------------- 37 | **webhookCreate** | [**optional.Interface of WebhookCreate**](WebhookCreate.md)| Webhook to create | 38 | 39 | ### Return type 40 | 41 | [**WebhookShow**](WebhookShow.md) 42 | 43 | ### Authorization 44 | 45 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 46 | 47 | ### HTTP request headers 48 | 49 | - **Content-Type**: application/json 50 | - **Accept**: application/json 51 | 52 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 53 | [[Back to Model list]](../README.md#documentation-for-models) 54 | [[Back to README]](../README.md) 55 | 56 | 57 | ## DeleteWebhook 58 | 59 | > DeleteWebhook(ctx, id) 60 | 61 | A webhook endpoint 62 | 63 | This endpoint allows the client to delete a single webhook given its `id`. 64 | 65 | ### Required Parameters 66 | 67 | 68 | Name | Type | Description | Notes 69 | ------------- | ------------- | ------------- | ------------- 70 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 71 | **id** | **int32**| Id of the webhook | 72 | 73 | ### Return type 74 | 75 | (empty response body) 76 | 77 | ### Authorization 78 | 79 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 80 | 81 | ### HTTP request headers 82 | 83 | - **Content-Type**: Not defined 84 | - **Accept**: application/json 85 | 86 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 87 | [[Back to Model list]](../README.md#documentation-for-models) 88 | [[Back to README]](../README.md) 89 | 90 | 91 | ## GetWebhookById 92 | 93 | > WebhookShow GetWebhookById(ctx, id) 94 | 95 | A webhook endpoint 96 | 97 | This endpoint allows the client to retrieve a single webhook given its `id`. 98 | 99 | ### Required Parameters 100 | 101 | 102 | Name | Type | Description | Notes 103 | ------------- | ------------- | ------------- | ------------- 104 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. 105 | **id** | **int32**| Id of the webhook | 106 | 107 | ### Return type 108 | 109 | [**WebhookShow**](WebhookShow.md) 110 | 111 | ### Authorization 112 | 113 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 114 | 115 | ### HTTP request headers 116 | 117 | - **Content-Type**: Not defined 118 | - **Accept**: application/json 119 | 120 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 121 | [[Back to Model list]](../README.md#documentation-for-models) 122 | [[Back to README]](../README.md) 123 | 124 | 125 | ## GetWebhooks 126 | 127 | > []WebhookIndex GetWebhooks(ctx, ) 128 | 129 | Webhooks 130 | 131 | This endpoint allows the client to retrieve a list of webhooks they have previously registered. \"Webhooks\" are used to register HTTP endpoints that will be called once a relevant event is triggered inside the web application, events like `article_created`, `article_updated`. It will return all webhooks, without pagination. 132 | 133 | ### Required Parameters 134 | 135 | This endpoint does not need any parameter. 136 | 137 | ### Return type 138 | 139 | [**[]WebhookIndex**](WebhookIndex.md) 140 | 141 | ### Authorization 142 | 143 | [api_key](../README.md#api_key), [oauth2](../README.md#oauth2) 144 | 145 | ### HTTP request headers 146 | 147 | - **Content-Type**: Not defined 148 | - **Accept**: application/json 149 | 150 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) 151 | [[Back to Model list]](../README.md#documentation-for-models) 152 | [[Back to README]](../README.md) 153 | 154 | -------------------------------------------------------------------------------- /pkg/devto/model_api_error.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | // ApiError struct for ApiError 14 | type ApiError struct { 15 | Error string `json:"error"` 16 | Status int32 `json:"status"` 17 | } 18 | -------------------------------------------------------------------------------- /pkg/devto/model_article_create.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | // ArticleCreate struct for ArticleCreate 14 | type ArticleCreate struct { 15 | Article ArticleCreateArticle `json:"article,omitempty"` 16 | } 17 | -------------------------------------------------------------------------------- /pkg/devto/model_article_create_article.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | // ArticleCreateArticle struct for ArticleCreateArticle 14 | type ArticleCreateArticle struct { 15 | Title string `json:"title"` 16 | // The body of the article. It can contain an optional front matter. For example ```markdown --- title: Hello, World! published: true tags: discuss, help date: 20190701T10:00Z series: Hello series canonical_url: https://example.com/blog/hello cover_image: article_published_cover_image --- ``` `date`, `series` and `canonical_url` are optional. `date` is the publication date-time `series` is the name of the series the article belongs to `canonical_url` is the canonical URL of the article `cover_image` is the main image of the article *If the markdown contains a front matter, it will take precedence on the equivalent params given in the JSON payload.* 17 | BodyMarkdown string `json:"body_markdown,omitempty"` 18 | // True to create a published article, false otherwise. Defaults to false 19 | Published bool `json:"published,omitempty"` 20 | // Article series name. All articles belonging to the same series need to have the same name in this parameter. 21 | Series string `json:"series,omitempty"` 22 | MainImage string `json:"main_image,omitempty"` 23 | CanonicalUrl string `json:"canonical_url,omitempty"` 24 | Description string `json:"description,omitempty"` 25 | Tags []string `json:"tags,omitempty"` 26 | // Only users belonging to an organization can assign the article to it 27 | OrganizationId int32 `json:"organization_id,omitempty"` 28 | } 29 | -------------------------------------------------------------------------------- /pkg/devto/model_article_flare_tag.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | // ArticleFlareTag Flare tag of the article 14 | type ArticleFlareTag struct { 15 | Name string `json:"name,omitempty"` 16 | // Background color (hexadecimal) 17 | BgColorHex string `json:"bg_color_hex,omitempty"` 18 | // Text color (hexadecimal) 19 | TextColorHex string `json:"text_color_hex,omitempty"` 20 | } 21 | -------------------------------------------------------------------------------- /pkg/devto/model_article_index.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | import ( 14 | "time" 15 | ) 16 | 17 | // ArticleIndex struct for ArticleIndex 18 | type ArticleIndex struct { 19 | TypeOf string `json:"type_of"` 20 | Id int32 `json:"id"` 21 | Title string `json:"title"` 22 | Description string `json:"description"` 23 | CoverImage string `json:"cover_image"` 24 | ReadablePublishDate string `json:"readable_publish_date"` 25 | SocialImage string `json:"social_image"` 26 | TagList []string `json:"tag_list"` 27 | Tags string `json:"tags"` 28 | Slug string `json:"slug"` 29 | Path string `json:"path"` 30 | Url string `json:"url"` 31 | CanonicalUrl string `json:"canonical_url"` 32 | CommentsCount int32 `json:"comments_count"` 33 | PositiveReactionsCount int32 `json:"positive_reactions_count"` 34 | CreatedAt time.Time `json:"created_at"` 35 | EditedAt time.Time `json:"edited_at"` 36 | CrosspostedAt time.Time `json:"crossposted_at"` 37 | PublishedAt time.Time `json:"published_at"` 38 | LastCommentAt time.Time `json:"last_comment_at"` 39 | // Crossposting or published date time 40 | PublishedTimestamp time.Time `json:"published_timestamp"` 41 | User ArticleUser `json:"user"` 42 | Organization ArticleOrganization `json:"organization,omitempty"` 43 | FlareTag ArticleFlareTag `json:"flare_tag,omitempty"` 44 | } 45 | -------------------------------------------------------------------------------- /pkg/devto/model_article_me.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | // ArticleMe struct for ArticleMe 14 | type ArticleMe struct { 15 | TypeOf string `json:"type_of"` 16 | Id int32 `json:"id"` 17 | Title string `json:"title"` 18 | Description string `json:"description"` 19 | CoverImage string `json:"cover_image"` 20 | Published bool `json:"published"` 21 | PublishedAt string `json:"published_at,omitempty"` 22 | TagList []string `json:"tag_list"` 23 | Slug string `json:"slug"` 24 | Path string `json:"path"` 25 | Url string `json:"url"` 26 | CanonicalUrl string `json:"canonical_url"` 27 | CommentsCount int32 `json:"comments_count"` 28 | PositiveReactionsCount int32 `json:"positive_reactions_count"` 29 | PageViewsCount int32 `json:"page_views_count"` 30 | // Crossposting or published date time 31 | PublishedTimestamp string `json:"published_timestamp,omitempty"` 32 | User ArticleUser `json:"user"` 33 | Organization ArticleOrganization `json:"organization,omitempty"` 34 | FlareTag ArticleFlareTag `json:"flare_tag,omitempty"` 35 | } 36 | -------------------------------------------------------------------------------- /pkg/devto/model_article_organization.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | // ArticleOrganization The organization the article belongs to 14 | type ArticleOrganization struct { 15 | Name string `json:"name,omitempty"` 16 | Username string `json:"username,omitempty"` 17 | Slug string `json:"slug,omitempty"` 18 | // Profile image (640x640) 19 | ProfileImage string `json:"profile_image,omitempty"` 20 | // Profile image (90x90) 21 | ProfileImage90 string `json:"profile_image_90,omitempty"` 22 | } 23 | -------------------------------------------------------------------------------- /pkg/devto/model_article_show.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | import ( 14 | "time" 15 | ) 16 | 17 | // ArticleShow struct for ArticleShow 18 | type ArticleShow struct { 19 | TypeOf string `json:"type_of"` 20 | Id int32 `json:"id"` 21 | Title string `json:"title"` 22 | Description string `json:"description"` 23 | CoverImage string `json:"cover_image"` 24 | ReadablePublishDate string `json:"readable_publish_date"` 25 | SocialImage string `json:"social_image"` 26 | TagList string `json:"tag_list"` 27 | Tags []string `json:"tags"` 28 | Slug string `json:"slug"` 29 | Path string `json:"path"` 30 | Url string `json:"url"` 31 | CanonicalUrl string `json:"canonical_url"` 32 | CommentsCount int32 `json:"comments_count"` 33 | PositiveReactionsCount int32 `json:"positive_reactions_count"` 34 | CreatedAt time.Time `json:"created_at"` 35 | EditedAt time.Time `json:"edited_at"` 36 | CrosspostedAt time.Time `json:"crossposted_at"` 37 | PublishedAt string `json:"published_at"` 38 | LastCommentAt time.Time `json:"last_comment_at"` 39 | // Crossposting or published date time 40 | PublishedTimestamp string `json:"published_timestamp"` 41 | BodyHtml string `json:"body_html"` 42 | BodyMarkdown string `json:"body_markdown"` 43 | User ArticleUser `json:"user"` 44 | Organization ArticleOrganization `json:"organization,omitempty"` 45 | FlareTag ArticleFlareTag `json:"flare_tag,omitempty"` 46 | } 47 | -------------------------------------------------------------------------------- /pkg/devto/model_article_update.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | // ArticleUpdate struct for ArticleUpdate 14 | type ArticleUpdate struct { 15 | Article ArticleUpdateArticle `json:"article,omitempty"` 16 | } 17 | -------------------------------------------------------------------------------- /pkg/devto/model_article_update_article.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | // ArticleUpdateArticle struct for ArticleUpdateArticle 14 | type ArticleUpdateArticle struct { 15 | Title string `json:"title,omitempty"` 16 | // The body of the article. It can contain an optional front matter. For example ```markdown --- title: Hello, World! published: true tags: discuss, help date: 20190701T10:00Z series: Hello series canonical_url: https://example.com/blog/hello cover_image: article_published_cover_image --- ``` `date`, `series` and `canonical_url` are optional. `date` is the publication date-time `series` is the name of the series the article belongs to `canonical_url` is the canonical URL of the article `cover_image` is the main image of the article *If the markdown contains a front matter, it will take precedence on the equivalent params given in the JSON payload.* 17 | BodyMarkdown string `json:"body_markdown,omitempty"` 18 | // True to create a published article, false otherwise. Defaults to false 19 | Published bool `json:"published,omitempty"` 20 | // Article series name. All articles belonging to the same series need to have the same name in this parameter. To remove an article from a series, the `null` value can be used. 21 | Series string `json:"series,omitempty"` 22 | MainImage string `json:"main_image,omitempty"` 23 | CanonicalUrl string `json:"canonical_url,omitempty"` 24 | Description string `json:"description,omitempty"` 25 | Tags []string `json:"tags,omitempty"` 26 | // Only users belonging to an organization can assign the article to it 27 | OrganizationId int32 `json:"organization_id,omitempty"` 28 | } 29 | -------------------------------------------------------------------------------- /pkg/devto/model_article_user.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | // ArticleUser The article's creator 14 | type ArticleUser struct { 15 | Name string `json:"name,omitempty"` 16 | Username string `json:"username,omitempty"` 17 | TwitterUsername string `json:"twitter_username,omitempty"` 18 | GithubUsername string `json:"github_username,omitempty"` 19 | WebsiteUrl string `json:"website_url,omitempty"` 20 | // Profile image (640x640) 21 | ProfileImage string `json:"profile_image,omitempty"` 22 | // Profile image (90x90) 23 | ProfileImage90 string `json:"profile_image_90,omitempty"` 24 | } 25 | -------------------------------------------------------------------------------- /pkg/devto/model_webhook_create.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | // WebhookCreate Webhook creation payload 14 | type WebhookCreate struct { 15 | WebhookEndpoint WebhookCreateWebhookEndpoint `json:"webhook_endpoint,omitempty"` 16 | } 17 | -------------------------------------------------------------------------------- /pkg/devto/model_webhook_create_webhook_endpoint.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | // WebhookCreateWebhookEndpoint struct for WebhookCreateWebhookEndpoint 14 | type WebhookCreateWebhookEndpoint struct { 15 | // The name of the requester, eg. \"DEV\" 16 | Source string `json:"source"` 17 | TargetUrl string `json:"target_url"` 18 | // An array of events identifiers 19 | Events []string `json:"events"` 20 | } 21 | -------------------------------------------------------------------------------- /pkg/devto/model_webhook_index.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | import ( 14 | "time" 15 | ) 16 | 17 | // WebhookIndex Webhook 18 | type WebhookIndex struct { 19 | TypeOf string `json:"type_of,omitempty"` 20 | Id int64 `json:"id,omitempty"` 21 | // The name of the requester, eg. \"DEV\" 22 | Source string `json:"source,omitempty"` 23 | TargetUrl string `json:"target_url,omitempty"` 24 | // An array of events identifiers 25 | Events []string `json:"events,omitempty"` 26 | CreatedAt time.Time `json:"created_at,omitempty"` 27 | } 28 | -------------------------------------------------------------------------------- /pkg/devto/model_webhook_show.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | import ( 14 | "time" 15 | ) 16 | 17 | // WebhookShow Webhook 18 | type WebhookShow struct { 19 | TypeOf string `json:"type_of,omitempty"` 20 | Id int64 `json:"id,omitempty"` 21 | // The name of the requester, eg. \"DEV\" 22 | Source string `json:"source,omitempty"` 23 | TargetUrl string `json:"target_url,omitempty"` 24 | // An array of events identifiers 25 | Events []string `json:"events,omitempty"` 26 | CreatedAt time.Time `json:"created_at,omitempty"` 27 | User ArticleUser `json:"user,omitempty"` 28 | } 29 | -------------------------------------------------------------------------------- /pkg/devto/response.go: -------------------------------------------------------------------------------- 1 | /* 2 | * DEV API (beta) 3 | * 4 | * Access DEV articles, comments and other resources via API 5 | * 6 | * API version: 0.5.9 7 | * Contact: yo@dev.to 8 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 9 | */ 10 | 11 | package devto 12 | 13 | import ( 14 | "net/http" 15 | ) 16 | 17 | // APIResponse stores the API response returned by the server. 18 | type APIResponse struct { 19 | *http.Response `json:"-"` 20 | Message string `json:"message,omitempty"` 21 | // Operation is the name of the OpenAPI operation. 22 | Operation string `json:"operation,omitempty"` 23 | // RequestURL is the request URL. This value is always available, even if the 24 | // embedded *http.Response is nil. 25 | RequestURL string `json:"url,omitempty"` 26 | // Method is the HTTP method used for the request. This value is always 27 | // available, even if the embedded *http.Response is nil. 28 | Method string `json:"method,omitempty"` 29 | // Payload holds the contents of the response body (which may be nil or empty). 30 | // This is provided here as the raw response.Body() reader will have already 31 | // been drained. 32 | Payload []byte `json:"-"` 33 | } 34 | 35 | // NewAPIResponse returns a new APIResonse object. 36 | func NewAPIResponse(r *http.Response) *APIResponse { 37 | 38 | response := &APIResponse{Response: r} 39 | return response 40 | } 41 | 42 | // NewAPIResponseWithError returns a new APIResponse object with the provided error message. 43 | func NewAPIResponseWithError(errorMessage string) *APIResponse { 44 | 45 | response := &APIResponse{Message: errorMessage} 46 | return response 47 | } 48 | -------------------------------------------------------------------------------- /pkg/links/links.go: -------------------------------------------------------------------------------- 1 | package links 2 | 3 | import ( 4 | "net/url" 5 | "path" 6 | ) 7 | 8 | type GitHub struct { 9 | Repo string 10 | Branch string 11 | } 12 | 13 | func NewGitHub(repo, branch string) *GitHub { 14 | return &GitHub{ 15 | Repo: repo, 16 | Branch: branch, 17 | } 18 | } 19 | 20 | const ( 21 | githubScheme = "https" 22 | githubHost = "github.com" 23 | ) 24 | 25 | func (g *GitHub) Create(relPath string) string { 26 | relPath = path.Join("/", relPath) 27 | p := path.Join(g.Repo, "raw", g.Branch, relPath) 28 | 29 | url := url.URL{ 30 | Scheme: githubScheme, 31 | Host: githubHost, 32 | Path: p, 33 | } 34 | 35 | return url.String() 36 | } 37 | -------------------------------------------------------------------------------- /pkg/links/links_test.go: -------------------------------------------------------------------------------- 1 | package links 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGitHub_Create(t *testing.T) { 10 | type fields struct { 11 | Repo string 12 | Branch string 13 | } 14 | 15 | type args struct { 16 | path string 17 | } 18 | 19 | tests := []struct { 20 | name string 21 | fields fields 22 | args args 23 | want string 24 | }{ 25 | { 26 | name: "normal", 27 | fields: fields{ 28 | Repo: "shihanng/devto", 29 | Branch: "test", 30 | }, 31 | args: args{ 32 | path: "test.png", 33 | }, 34 | want: "https://github.com/shihanng/devto/raw/test/test.png", 35 | }, 36 | { 37 | name: "ignore .. at beginning", 38 | fields: fields{ 39 | Repo: "shihanng/devto", 40 | Branch: "test", 41 | }, 42 | args: args{ 43 | path: "../test.png", 44 | }, 45 | want: "https://github.com/shihanng/devto/raw/test/test.png", 46 | }, 47 | { 48 | name: "can have .. inside", 49 | fields: fields{ 50 | Repo: "shihanng/devto", 51 | Branch: "test", 52 | }, 53 | args: args{ 54 | path: "some/tests/../test.png", 55 | }, 56 | want: "https://github.com/shihanng/devto/raw/test/some/test.png", 57 | }, 58 | { 59 | name: "can have .. inside but not exceed root", 60 | fields: fields{ 61 | Repo: "shihanng/devto", 62 | Branch: "test", 63 | }, 64 | args: args{ 65 | path: "tests/../../test.png", 66 | }, 67 | want: "https://github.com/shihanng/devto/raw/test/test.png", 68 | }, 69 | } 70 | 71 | for _, tt := range tests { 72 | t.Run(tt.name, func(t *testing.T) { 73 | g := NewGitHub(tt.fields.Repo, tt.fields.Branch) 74 | assert.Equal(t, tt.want, g.Create(tt.args.path)) 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pkg/links/root.go: -------------------------------------------------------------------------------- 1 | package links 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/cockroachdb/errors" 8 | "gopkg.in/src-d/go-git.v4" 9 | ) 10 | 11 | // Root is like git ls-files --full-name <filepath> 12 | func Root(path string) (string, error) { 13 | absPath, err := filepath.Abs(path) 14 | if err != nil { 15 | return "", errors.Wrap(err, "links: get absolute path") 16 | } 17 | 18 | repo, err := git.PlainOpenWithOptions(filepath.Dir(absPath), &git.PlainOpenOptions{DetectDotGit: true}) 19 | if err != nil { 20 | return "", errors.Wrap(err, "links: open git") 21 | } 22 | 23 | wt, err := repo.Worktree() 24 | 25 | var repoRootWorkDir string 26 | 27 | if err == nil { 28 | workDir := filepath.Dir(absPath) 29 | repoRootWorkDir = strings.TrimPrefix(workDir, wt.Filesystem.Root()) 30 | } 31 | 32 | return repoRootWorkDir, errors.Wrap(err, "links: get git worktree") 33 | } 34 | -------------------------------------------------------------------------------- /pkg/links/root_test.go: -------------------------------------------------------------------------------- 1 | package links 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestRoot(t *testing.T) { 13 | tmpfile, err := ioutil.TempFile("", "example") 14 | require.NoError(t, err) 15 | 16 | defer os.Remove(tmpfile.Name()) 17 | 18 | type args struct { 19 | path string 20 | } 21 | 22 | tests := []struct { 23 | name string 24 | args args 25 | want string 26 | assertion assert.ErrorAssertionFunc 27 | }{ 28 | { 29 | name: "normal", 30 | args: args{ 31 | path: "./root.go", 32 | }, 33 | want: "/pkg/links", 34 | assertion: assert.NoError, 35 | }, 36 | { 37 | name: "not git", 38 | args: args{ 39 | path: tmpfile.Name(), 40 | }, 41 | want: "", 42 | assertion: assert.Error, 43 | }, 44 | } 45 | 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | got, err := Root(tt.args.path) 49 | tt.assertion(t, err) 50 | assert.Equal(t, tt.want, got) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/spf13/cobra/cobra" 7 | _ "github.com/golang/mock/mockgen" 8 | ) 9 | --------------------------------------------------------------------------------