~glorifiedgluer/mataroa-cli

31704601faf1b70b83939da0df567caa002dc98e — Victor Freire 4 months ago master
wip
9 files changed, 753 insertions(+), 0 deletions(-)

A .gitignore
A client.go
A commands.go
A config.go
A flake.lock
A flake.nix
A go.mod
A go.sum
A main.go
A  => .gitignore +26 -0
@@ 1,26 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work

# direnv/devenv
.direnv
.devenv
.envrc

A  => client.go +316 -0
@@ 1,316 @@
package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"time"
)

var (
	ErrPostNotFound error = errors.New("not found")
)

type BaseResponse struct {
	OK      bool   `json:"ok,omitempty"`
	Message string `json:"message,omitempty"`
	Error   string `json:"error,omitempty"`
}

type Post struct {
	Title       string      `json:"title,omitempty"`
	Slug        string      `json:"slug,omitempty"`
	Body        string      `json:"body,omitempty"`
	PublishedAt interface{} `json:"published_at,omitempty"`
	Url         string      `json:"url,omitempty"`
}

type GetResponse struct {
	BaseResponse
	Post
}

type ListResponse struct {
	BaseResponse
	Posts []Post `json:"post_list,omitempty"`
}

type CreateRequest struct {
	Title       string `json:"title,omitempty"`
	Body        string `json:"body,omitempty"`
	PublishedAt string `json:"published_at,omitempty"`
}

type CreateResponse struct {
	BaseResponse `json:"base_response"`
	Slug         string `json:"slug,omitempty"`
	Url          string `json:"url,omitempty"`
}

type DeleteResponse struct {
	BaseResponse
}

type UpdateRequest struct {
	Title       string `json:"title,omitempty"`
	Slug        string `json:"slug,omitempty"`
	Body        string `json:"body,omitempty"`
	PublishedAt string `json:"published_at,omitempty"`
}

type UpdateResponse struct {
	BaseResponse
	Slug string `json:"slug,omitempty"`
	Url  string `json:"url,omitempty"`
}

type Mataroa interface {
	Create(context.Context, CreateRequest) (CreateResponse, error)
	Get(context.Context, string) (GetResponse, error)
	List(context.Context) (ListResponse, error)
	Delete(context.Context, string) (DeleteResponse, error)
	Update(context.Context, string, UpdateRequest) (UpdateResponse, error)
}

type Client struct {
	httpClient *http.Client
	baseURL    string
	token      string
}

var _ Mataroa = (*Client)(nil)

func NewClient(httpClient *http.Client, baseURL string, token string) (Client, error) {
	if httpClient == nil {
		httpClient = &http.Client{Timeout: time.Second * 10}
	}

	if baseURL == "" {
		return Client{}, fmt.Errorf("creating client: 'baseURL' cannot be empty")
	}
	baseURL = strings.TrimSuffix(baseURL, "/")

	if token == "" {
		return Client{}, fmt.Errorf("creating client: 'token' cannot be empty")
	}

	return Client{
		httpClient, baseURL, token,
	}, nil
}

func (c *Client) newRequest(ctx context.Context, method string, url string, body io.Reader) (*http.Request, error) {
	if !strings.HasSuffix(url, "/") {
		url += "/"
	}

	req, err := http.NewRequestWithContext(ctx, method, url, body)
	if err != nil {
		return req, err
	}

	req.Header.Add("Authorization", "Bearer "+c.token)
	req.Header.Set("Content-Type", "application/json")

	return req, nil
}

func (c *Client) Create(ctx context.Context, post CreateRequest) (CreateResponse, error) {
	urlPath := fmt.Sprintf("%s/api/posts", c.baseURL)

	if post.Title == "" {
		return CreateResponse{}, fmt.Errorf("create post: title cannot be empty")
	}

	b := new(bytes.Buffer)
	err := json.NewEncoder(b).Encode(&post)
	if err != nil {
		return CreateResponse{}, fmt.Errorf("create post: %w", err)
	}

	req, err := c.newRequest(ctx, http.MethodPost, urlPath, b)
	if err != nil {
		return CreateResponse{}, fmt.Errorf("create post: %w", err)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return CreateResponse{}, fmt.Errorf("create post: %w", err)
	}
	defer resp.Body.Close()

	var value CreateResponse
	decoder := json.NewDecoder(resp.Body)
	err = decoder.Decode(&value)
	if err != nil {
		return CreateResponse{}, fmt.Errorf("create post: %w", err)
	}

	if !value.OK {
		log.Println(value.OK)
		log.Println(value.Message)
		return value, fmt.Errorf("create post: %w", errors.New(value.Message))
	}

	return value, nil
}

func (c *Client) Delete(ctx context.Context, slug string) (DeleteResponse, error) {
	urlPath := fmt.Sprintf("%s/api/posts/%s/", c.baseURL, slug)

	req, err := c.newRequest(ctx, http.MethodDelete, urlPath, nil)
	if err != nil {
		return DeleteResponse{}, fmt.Errorf("delete post: %w", err)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return DeleteResponse{}, fmt.Errorf("delete post: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return DeleteResponse{}, fmt.Errorf("delete post: %w", ErrPostNotFound)
	}

	if resp.StatusCode != http.StatusOK {
		return DeleteResponse{}, fmt.Errorf("delete post: %w", errors.New(http.StatusText(resp.StatusCode)))
	}

	var value DeleteResponse
	decoder := json.NewDecoder(resp.Body)
	err = decoder.Decode(&value)
	if err != nil {
		return DeleteResponse{}, fmt.Errorf("delete post: %w", err)
	}

	if !value.OK {
		return value, fmt.Errorf("delete post: %w", errors.New(value.Message))
	}

	return value, nil
}

func (c *Client) Get(ctx context.Context, slug string) (GetResponse, error) {
	urlPath := fmt.Sprintf("%s/api/posts/%s", c.baseURL, slug)

	req, err := c.newRequest(ctx, http.MethodGet, urlPath, nil)
	if err != nil {
		return GetResponse{}, fmt.Errorf("get post: %w", err)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return GetResponse{}, fmt.Errorf("get post: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return GetResponse{}, fmt.Errorf("get post: %w", ErrPostNotFound)
	}

	if resp.StatusCode != http.StatusOK {
		return GetResponse{}, fmt.Errorf("get post: %w", errors.New(http.StatusText(resp.StatusCode)))
	}

	var value GetResponse
	decoder := json.NewDecoder(resp.Body)
	err = decoder.Decode(&value)
	if err != nil {
		return GetResponse{}, fmt.Errorf("error decoding post: %w", err)
	}

	if !value.OK {
		fmt.Println(value.Message)
		fmt.Println(value.Error)
		return value, fmt.Errorf("error getting post: %w", errors.New(value.Message))
	}

	return value, nil
}

func (c *Client) List(ctx context.Context) (ListResponse, error) {
	urlPath := fmt.Sprintf("%s/api/posts", c.baseURL)

	req, err := c.newRequest(ctx, http.MethodGet, urlPath, nil)
	if err != nil {
		return ListResponse{}, fmt.Errorf("list posts: %w", err)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return ListResponse{}, fmt.Errorf("list posts: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return ListResponse{}, fmt.Errorf("list posts: %w", ErrPostNotFound)
	}

	if resp.StatusCode != http.StatusOK {
		return ListResponse{}, fmt.Errorf("list post: %w", errors.New(http.StatusText(resp.StatusCode)))
	}

	var value ListResponse
	decoder := json.NewDecoder(resp.Body)
	err = decoder.Decode(&value)
	if err != nil {
		return ListResponse{}, fmt.Errorf("list posts: %w", err)
	}

	if !value.OK {
		return value, fmt.Errorf("list posts: %w", errors.New(value.Message))
	}

	return value, nil
}

func (c *Client) Update(ctx context.Context, slug string, post UpdateRequest) (UpdateResponse, error) {
	urlPath := fmt.Sprintf("%s/api/posts/%s", c.baseURL, slug)

	b := new(bytes.Buffer)
	err := json.NewEncoder(b).Encode(&post)
	if err != nil {
		return UpdateResponse{}, fmt.Errorf("update post: %w", err)
	}

	req, err := c.newRequest(ctx, http.MethodPatch, urlPath, b)
	if err != nil {
		return UpdateResponse{}, fmt.Errorf("update post: %w", err)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return UpdateResponse{}, fmt.Errorf("update post: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return UpdateResponse{}, fmt.Errorf("update post '%s': %w", slug, ErrPostNotFound)
	}

	if resp.StatusCode != http.StatusOK {
		return UpdateResponse{}, fmt.Errorf("update post: %w", errors.New(http.StatusText(resp.StatusCode)))
	}

	var value UpdateResponse
	decoder := json.NewDecoder(resp.Body)
	err = decoder.Decode(&value)
	if err != nil {
		return UpdateResponse{}, fmt.Errorf("update post: %w", err)
	}

	if !value.OK {
		log.Println(value.OK)
		log.Println(value.Message)
		return value, fmt.Errorf("update post: %w", errors.New(value.Message))
	}

	return value, nil
}

A  => commands.go +128 -0
@@ 1,128 @@
package main

import (
	"encoding/json"
	"fmt"
	"log"

	"github.com/spf13/cobra"
)

func CommandsRoot() *cobra.Command {
	cmd := &cobra.Command{
		Use:               "mata",
		Short:             "mata is a CLI tool for mataroa.blog",
		CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
		DisableAutoGenTag: true,
	}

	cmd.AddCommand(CommandsPosts())

	return cmd
}

func CommandsPosts() *cobra.Command {
	cmd := &cobra.Command{
		Use:     "posts",
		Aliases: []string{"p"},
		Short:   "Manage posts",
	}

	cmd.AddCommand(CommandsPostsList())
	cmd.AddCommand(CommandsPostsGet())
	cmd.AddCommand(CommandsPostsDelete())

	return cmd
}

func CommandsPostsList() *cobra.Command {
	run := func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()

		client, err := NewClient(nil, "https://mataroa.blog", "769fa349a28f63f93c8b6c25c07e6bf2")
		if err != nil {
			log.Fatal(err)
		}

		response, err := client.List(ctx)
		if err != nil {
			log.Fatal(err)
		}

		value, err := json.Marshal(&response)
		if err != nil {
			log.Fatal(err)
		}

		fmt.Println(string(value))
	}

	return &cobra.Command{
		Use:     "list",
		Aliases: []string{"l", "ls"},
		Short:   "List posts",
		Args:    cobra.ExactArgs(0),
		Run:     run,
	}
}

func CommandsPostsGet() *cobra.Command {
	run := func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()

		slug := args[0]

		client, err := NewClient(nil, "https://mataroa.blog", "769fa349a28f63f93c8b6c25c07e6bf2")
		if err != nil {
			log.Fatal(err)
		}

		response, err := client.Get(ctx, slug)
		if err != nil {
			log.Fatal(err)
		}

		value, err := json.Marshal(&response)
		if err != nil {
			log.Fatal(err)
		}

		fmt.Println(string(value))
	}

	return &cobra.Command{
		Use:     "get",
		Aliases: []string{"g"},
		Short:   "Get a post",
		Args:    cobra.ExactArgs(1),
		Run:     run,
	}
}

func CommandsPostsDelete() *cobra.Command {
	run := func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()

		slug := args[0]

		client, err := NewClient(nil, "https://mataroa.blog", "769fa349a28f63f93c8b6c25c07e6bf2")
		if err != nil {
			log.Fatal(err)
		}

		_, err = client.Delete(ctx, slug)
		if err != nil {
			log.Fatal(err)
		}

		fmt.Println("deleted post successfully")
	}

	return &cobra.Command{
		Use:     "delete",
		Aliases: []string{"rm", "d"},
		Short:   "Delete a post",
		Args:    cobra.ExactArgs(1),
		Run:     run,
	}
}

A  => config.go +6 -0
@@ 1,6 @@
package main

type Config struct {
	BaseURL string
	Token   string
}

A  => flake.lock +218 -0
@@ 1,218 @@
{
  "nodes": {
    "devenv": {
      "inputs": {
        "flake-compat": "flake-compat",
        "nix": "nix",
        "nixpkgs": "nixpkgs",
        "pre-commit-hooks": "pre-commit-hooks"
      },
      "locked": {
        "lastModified": 1684251214,
        "narHash": "sha256-c+hiXMdeBXKWGZqU/3RPz3Wymg0UcgOnbcW1eJip+88=",
        "owner": "cachix",
        "repo": "devenv",
        "rev": "957e4eb5ce74541f63af5e221a7917107c0c1d22",
        "type": "github"
      },
      "original": {
        "owner": "cachix",
        "repo": "devenv",
        "type": "github"
      }
    },
    "flake-compat": {
      "flake": false,
      "locked": {
        "lastModified": 1673956053,
        "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
        "owner": "edolstra",
        "repo": "flake-compat",
        "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
        "type": "github"
      },
      "original": {
        "owner": "edolstra",
        "repo": "flake-compat",
        "type": "github"
      }
    },
    "flake-utils": {
      "locked": {
        "lastModified": 1667395993,
        "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
        "owner": "numtide",
        "repo": "flake-utils",
        "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
        "type": "github"
      },
      "original": {
        "owner": "numtide",
        "repo": "flake-utils",
        "type": "github"
      }
    },
    "gitignore": {
      "inputs": {
        "nixpkgs": [
          "devenv",
          "pre-commit-hooks",
          "nixpkgs"
        ]
      },
      "locked": {
        "lastModified": 1660459072,
        "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
        "owner": "hercules-ci",
        "repo": "gitignore.nix",
        "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
        "type": "github"
      },
      "original": {
        "owner": "hercules-ci",
        "repo": "gitignore.nix",
        "type": "github"
      }
    },
    "lowdown-src": {
      "flake": false,
      "locked": {
        "lastModified": 1633514407,
        "narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
        "owner": "kristapsdz",
        "repo": "lowdown",
        "rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
        "type": "github"
      },
      "original": {
        "owner": "kristapsdz",
        "repo": "lowdown",
        "type": "github"
      }
    },
    "nix": {
      "inputs": {
        "lowdown-src": "lowdown-src",
        "nixpkgs": [
          "devenv",
          "nixpkgs"
        ],
        "nixpkgs-regression": "nixpkgs-regression"
      },
      "locked": {
        "lastModified": 1676545802,
        "narHash": "sha256-EK4rZ+Hd5hsvXnzSzk2ikhStJnD63odF7SzsQ8CuSPU=",
        "owner": "domenkozar",
        "repo": "nix",
        "rev": "7c91803598ffbcfe4a55c44ac6d49b2cf07a527f",
        "type": "github"
      },
      "original": {
        "owner": "domenkozar",
        "ref": "relaxed-flakes",
        "repo": "nix",
        "type": "github"
      }
    },
    "nixpkgs": {
      "locked": {
        "lastModified": 1678875422,
        "narHash": "sha256-T3o6NcQPwXjxJMn2shz86Chch4ljXgZn746c2caGxd8=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "126f49a01de5b7e35a43fd43f891ecf6d3a51459",
        "type": "github"
      },
      "original": {
        "owner": "NixOS",
        "ref": "nixpkgs-unstable",
        "repo": "nixpkgs",
        "type": "github"
      }
    },
    "nixpkgs-regression": {
      "locked": {
        "lastModified": 1643052045,
        "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
        "type": "github"
      },
      "original": {
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
        "type": "github"
      }
    },
    "nixpkgs-stable": {
      "locked": {
        "lastModified": 1678872516,
        "narHash": "sha256-/E1YwtMtFAu2KUQKV/1+KFuReYPANM2Rzehk84VxVoc=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "9b8e5abb18324c7fe9f07cb100c3cd4a29cda8b8",
        "type": "github"
      },
      "original": {
        "owner": "NixOS",
        "ref": "nixos-22.11",
        "repo": "nixpkgs",
        "type": "github"
      }
    },
    "nixpkgs_2": {
      "locked": {
        "lastModified": 1684171562,
        "narHash": "sha256-BMUWjVWAUdyMWKk0ATMC9H0Bv4qAV/TXwwPUvTiC5IQ=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "55af203d468a6f5032a519cba4f41acf5a74b638",
        "type": "github"
      },
      "original": {
        "owner": "NixOS",
        "ref": "nixos-22.11",
        "repo": "nixpkgs",
        "type": "github"
      }
    },
    "pre-commit-hooks": {
      "inputs": {
        "flake-compat": [
          "devenv",
          "flake-compat"
        ],
        "flake-utils": "flake-utils",
        "gitignore": "gitignore",
        "nixpkgs": [
          "devenv",
          "nixpkgs"
        ],
        "nixpkgs-stable": "nixpkgs-stable"
      },
      "locked": {
        "lastModified": 1682596858,
        "narHash": "sha256-Hf9XVpqaGqe/4oDGr30W8HlsWvJXtMsEPHDqHZA6dDg=",
        "owner": "cachix",
        "repo": "pre-commit-hooks.nix",
        "rev": "fb58866e20af98779017134319b5663b8215d912",
        "type": "github"
      },
      "original": {
        "owner": "cachix",
        "repo": "pre-commit-hooks.nix",
        "type": "github"
      }
    },
    "root": {
      "inputs": {
        "devenv": "devenv",
        "nixpkgs": "nixpkgs_2"
      }
    }
  },
  "root": "root",
  "version": 7
}

A  => flake.nix +29 -0
@@ 1,29 @@
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11";
    devenv.url = "github:cachix/devenv";
  };

  outputs = { self, nixpkgs, devenv, ... } @ inputs:
    let
      forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
    in
    {
      devShells = forAllSystems (system:
        let
          pkgs = nixpkgs.legacyPackages.${system};
        in
        {
          default = devenv.lib.mkShell {
            inherit inputs pkgs;
            modules = [
              ({ pkgs, ... }: {
                languages.go.enable = true;
                packages = with pkgs; [ gcc ];
              })
            ];
          };
        });
    };
}


A  => go.mod +9 -0
@@ 1,9 @@
module github.com/mataroa/mataroa-cli

go 1.19

require (
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/spf13/cobra v1.7.0 // indirect
	github.com/spf13/pflag v1.0.5 // indirect
)

A  => go.sum +10 -0
@@ 1,10 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

A  => main.go +11 -0
@@ 1,11 @@
package main

import (
	"log"
)

func main() {
	if err := CommandsRoot().Execute(); err != nil {
		log.Fatalf("error running mata: %s", err)
	}
}