api: implemented resolve subreddit name api
This commit is contained in:
parent
b220cf1b2c
commit
af2fd593b4
1
go.mod
1
go.mod
|
@ -52,6 +52,7 @@ require (
|
||||||
github.com/XSAM/otelsql v0.32.0 // indirect
|
github.com/XSAM/otelsql v0.32.0 // indirect
|
||||||
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
|
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
|
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -28,6 +28,8 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||||
github.com/bufbuild/protovalidate-go v0.6.3 h1:wxQyzW035zM16Binbaz/nWAzS12dRIXhZdSUWRY7Fv0=
|
github.com/bufbuild/protovalidate-go v0.6.3 h1:wxQyzW035zM16Binbaz/nWAzS12dRIXhZdSUWRY7Fv0=
|
||||||
github.com/bufbuild/protovalidate-go v0.6.3/go.mod h1:J4PtwP9Z2YAGgB0+o+tTWEDtLtXvz/gfhFZD8pbzM/U=
|
github.com/bufbuild/protovalidate-go v0.6.3/go.mod h1:J4PtwP9Z2YAGgB0+o+tTWEDtLtXvz/gfhFZD8pbzM/U=
|
||||||
|
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXyeZBvSYvQf8u86jbKehZPVDDlkgDl4=
|
||||||
|
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
|
|
@ -5,12 +5,14 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/stephenafamo/bob"
|
"github.com/stephenafamo/bob"
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/gen/reddit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
Executor bob.Executor
|
Executor bob.Executor
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
|
Reddit *reddit.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) lockf(f func()) {
|
func (api *API) lockf(f func()) {
|
||||||
|
|
|
@ -8,19 +8,21 @@ import (
|
||||||
"github.com/mattn/go-sqlite3"
|
"github.com/mattn/go-sqlite3"
|
||||||
"github.com/tigorlazuardi/bluemage/go/gen/models"
|
"github.com/tigorlazuardi/bluemage/go/gen/models"
|
||||||
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
|
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/pkg/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (api *API) SubredditCreate(ctx context.Context, subreddit *models.SubredditSetter) (err error) {
|
func (api *API) SubredditCreate(ctx context.Context, request *models.SubredditSetter) (err error) {
|
||||||
// TODO: add check to Reddit API to see if subreddit exists.
|
ctx, span := tracer.Start(ctx, "SubredditCreate")
|
||||||
|
defer func() { telemetry.EndWithStatus(span, err) }()
|
||||||
|
|
||||||
api.lockf(func() {
|
api.lockf(func() {
|
||||||
_, err = models.Subreddits.Insert(ctx, api.Executor, subreddit)
|
_, err = models.Subreddits.Insert(ctx, api.Executor, request)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if sqlite3err := new(sqlite3.Error); errors.As(err, &sqlite3err) {
|
if sqlite3err := new(sqlite3.Error); errors.As(err, &sqlite3err) {
|
||||||
if sqlite3err.Code == sqlite3.ErrConstraint {
|
if sqlite3err.Code == sqlite3.ErrConstraint {
|
||||||
return errs.
|
return errs.
|
||||||
Wrapw(err, "subreddit already exists", "input", subreddit).
|
Wrapw(err, "subreddit already exists", "input", request).
|
||||||
Code(connect.CodeAlreadyExists)
|
Code(connect.CodeAlreadyExists)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
76
go/api/subreddits_resolve.go
Normal file
76
go/api/subreddits_resolve.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"connectrpc.com/connect"
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/gen/reddit"
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/pkg/log"
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/pkg/telemetry"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubredditResolveNameRequest struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) SubredditResolveName(ctx context.Context, request SubredditResolveNameRequest) (resolved string, err error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "SubredditResolveName")
|
||||||
|
defer func() { telemetry.EndWithStatus(span, err) }()
|
||||||
|
|
||||||
|
ctx, httplog := log.ContextWithRoundTripCollector(ctx)
|
||||||
|
typ := reddit.GetListingTypeR
|
||||||
|
if request.Type == "user" {
|
||||||
|
typ = reddit.GetListingTypeUser
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := api.Reddit.GetListing(ctx, reddit.GetListingParams{
|
||||||
|
Type: typ,
|
||||||
|
Name: request.Name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
err = errs.Wrapw(err, "failed to get listing from reddit", "round_trip", httplog)
|
||||||
|
return resolved, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resp := resp.(type) {
|
||||||
|
case *reddit.GetListingForbidden:
|
||||||
|
err = errs.
|
||||||
|
Failw(
|
||||||
|
"subreddit is private",
|
||||||
|
"round_trip", httplog,
|
||||||
|
).
|
||||||
|
Code(connect.CodePermissionDenied)
|
||||||
|
return resolved, err
|
||||||
|
case *reddit.GetListingTooManyRequests:
|
||||||
|
err = errs.
|
||||||
|
Failw(
|
||||||
|
"too many requests error response from reddit",
|
||||||
|
"round_trip", httplog,
|
||||||
|
).
|
||||||
|
Code(connect.CodeResourceExhausted)
|
||||||
|
return resolved, err
|
||||||
|
case *reddit.ListingResponse:
|
||||||
|
if !isValidSubreddit(resp) {
|
||||||
|
err = errs.
|
||||||
|
Failf("subreddit '%s' of type '%s' seems to be empty or not valid", request.Name, request.Type).
|
||||||
|
Details("round_trip", httplog).
|
||||||
|
Code(connect.CodeNotFound)
|
||||||
|
return resolved, err
|
||||||
|
}
|
||||||
|
data := resp.Data.Children[0].Data
|
||||||
|
if request.Type == "user" {
|
||||||
|
return data.Author, nil
|
||||||
|
}
|
||||||
|
return data.Subreddit, nil
|
||||||
|
default:
|
||||||
|
err = errs.
|
||||||
|
Failw("unexpected response from reddit", "round_trip", httplog)
|
||||||
|
return resolved, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidSubreddit(list *reddit.ListingResponse) bool {
|
||||||
|
return list.Data.After.Null && len(list.Data.Children) == 0
|
||||||
|
}
|
|
@ -20,7 +20,9 @@ import (
|
||||||
"github.com/stephenafamo/bob"
|
"github.com/stephenafamo/bob"
|
||||||
"github.com/tigorlazuardi/bluemage/go/api"
|
"github.com/tigorlazuardi/bluemage/go/api"
|
||||||
"github.com/tigorlazuardi/bluemage/go/config"
|
"github.com/tigorlazuardi/bluemage/go/config"
|
||||||
"github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1/v1connect"
|
v1DeviceConnect "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1/v1connect"
|
||||||
|
v1SubredditsConnect "github.com/tigorlazuardi/bluemage/go/gen/proto/subreddits/v1/v1connect"
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/gen/reddit"
|
||||||
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
|
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
|
||||||
"github.com/tigorlazuardi/bluemage/go/pkg/log"
|
"github.com/tigorlazuardi/bluemage/go/pkg/log"
|
||||||
"github.com/tigorlazuardi/bluemage/go/pkg/telemetry"
|
"github.com/tigorlazuardi/bluemage/go/pkg/telemetry"
|
||||||
|
@ -91,15 +93,17 @@ var Cmd = &cobra.Command{
|
||||||
)
|
)
|
||||||
db := bob.New(sqldb)
|
db := bob.New(sqldb)
|
||||||
|
|
||||||
|
client, err := reddit.NewClient("https://reddit.com", reddit.WithClient(&http.Client{
|
||||||
|
Transport: log.NewRoundTripper(http.DefaultTransport),
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
api := &api.API{
|
api := &api.API{
|
||||||
Executor: db,
|
Executor: db,
|
||||||
DB: sqldb,
|
DB: sqldb,
|
||||||
}
|
Reddit: client,
|
||||||
|
|
||||||
handler := &server.Server{
|
|
||||||
DeviceHandler: server.DeviceHandler{
|
|
||||||
API: api,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validationInterceptor, err := validate.NewInterceptor()
|
validationInterceptor, err := validate.NewInterceptor()
|
||||||
|
@ -112,13 +116,20 @@ var Cmd = &cobra.Command{
|
||||||
return errs.Wrap(err, "failed to create otel interceptor")
|
return errs.Wrap(err, "failed to create otel interceptor")
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
interceptors := []connect.Interceptor{
|
||||||
mux.Handle(v1connect.NewDeviceServiceHandler(handler, connect.WithInterceptors(
|
|
||||||
validationInterceptor,
|
validationInterceptor,
|
||||||
otelInterceptor,
|
otelInterceptor,
|
||||||
server.ErrorMessageInterceptor(),
|
server.ErrorMessageInterceptor(),
|
||||||
server.LogInterceptor(),
|
server.LogInterceptor(),
|
||||||
)))
|
}
|
||||||
|
|
||||||
|
handlerOpts := []connect.HandlerOption{
|
||||||
|
connect.WithInterceptors(interceptors...),
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle(v1DeviceConnect.NewDeviceServiceHandler(&server.DeviceHandler{API: api}, handlerOpts...))
|
||||||
|
mux.Handle(v1SubredditsConnect.NewSubredditsServiceHandler(&server.SubredditHandler{API: api}, handlerOpts...))
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: fmt.Sprintf("%s:%s", cfg.String("http.host"), cfg.String("http.port")),
|
Addr: fmt.Sprintf("%s:%s", cfg.String("http.host"), cfg.String("http.port")),
|
||||||
|
|
|
@ -51,6 +51,12 @@ type SubredditConverter interface {
|
||||||
|
|
||||||
// goverter:ignore Name Type CoverImageID CreatedAt UpdatedAt
|
// goverter:ignore Name Type CoverImageID CreatedAt UpdatedAt
|
||||||
ProtoSubredditSetterToBobSubredditSetter(*subreddits.SubredditSetter) *models.SubredditSetter
|
ProtoSubredditSetterToBobSubredditSetter(*subreddits.SubredditSetter) *models.SubredditSetter
|
||||||
|
|
||||||
|
// goverter:useZeroValueOnPointerInconsistency
|
||||||
|
ProtoCreateSubredditRequestToAPISubredditResolveNameRequest(*subreddits.CreateSubredditRequest) api.SubredditResolveNameRequest
|
||||||
|
|
||||||
|
// goverter:useZeroValueOnPointerInconsistency
|
||||||
|
ProtoResolveSubredditNameRequestToAPISubredditResolveName(*subreddits.ResolveSubredditNameRequest) api.SubredditResolveNameRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubredditTypeToString(subType subreddits.SubredditType) string {
|
func SubredditTypeToString(subType subreddits.SubredditType) string {
|
||||||
|
|
|
@ -220,6 +220,15 @@ func Failf(message string, args ...any) Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Failw(message string, details ...any) Error {
|
||||||
|
return &Err{
|
||||||
|
origin: errors.New(message),
|
||||||
|
caller: caller.New(3),
|
||||||
|
code: connect.CodeInternal,
|
||||||
|
details: details,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func IntoConnectError(err error) error {
|
func IntoConnectError(err error) error {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
141
go/pkg/log/http_transport.go
Normal file
141
go/pkg/log/http_transport.go
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/c2h5oh/datasize"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RoundTripCollector struct {
|
||||||
|
Request *http.Request
|
||||||
|
// HTTP Response. May be nil.
|
||||||
|
Response *http.Response
|
||||||
|
Start time.Time
|
||||||
|
End time.Time
|
||||||
|
RequestBody *bytes.Buffer
|
||||||
|
ResponseBody *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ht RoundTripCollector) LogValue() slog.Value {
|
||||||
|
values := make([]slog.Attr, 0, 4)
|
||||||
|
if !ht.Start.IsZero() {
|
||||||
|
values = append(values, slog.Time("start", ht.Start))
|
||||||
|
}
|
||||||
|
if !ht.End.IsZero() {
|
||||||
|
values = append(values, slog.Time("end", ht.End))
|
||||||
|
}
|
||||||
|
if !ht.Start.IsZero() && !ht.End.IsZero() {
|
||||||
|
values = append(values, slog.Duration("duration", ht.End.Sub(ht.Start)))
|
||||||
|
}
|
||||||
|
if ht.Request != nil {
|
||||||
|
vals := make([]slog.Attr, 0, 5)
|
||||||
|
vals = append(vals, slog.String("url", ht.Request.URL.String()))
|
||||||
|
vals = append(vals, slog.String("method", ht.Request.Method))
|
||||||
|
headers := []slog.Attr{}
|
||||||
|
for k := range ht.Request.Header {
|
||||||
|
headers = append(headers, slog.String(k, ht.Request.Header.Get(k)))
|
||||||
|
}
|
||||||
|
if len(headers) > 0 {
|
||||||
|
vals = append(vals, slog.Attr{Key: "headers", Value: slog.GroupValue(headers...)})
|
||||||
|
}
|
||||||
|
if ht.RequestBody.Len() > 0 {
|
||||||
|
cl := datasize.ByteSize(ht.RequestBody.Len())
|
||||||
|
vals = append(vals, slog.String("content_length", cl.HumanReadable()))
|
||||||
|
if ht.Request.Header.Get("Content-Type") == "application/json" {
|
||||||
|
vals = append(vals, slog.Any("body", json.RawMessage(ht.RequestBody.Bytes())))
|
||||||
|
} else {
|
||||||
|
vals = append(vals, slog.String("body", ht.RequestBody.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values = append(values, slog.Attr{Key: "request", Value: slog.GroupValue(vals...)})
|
||||||
|
}
|
||||||
|
if ht.Response != nil {
|
||||||
|
vals := make([]slog.Attr, 0, 4)
|
||||||
|
vals = append(vals, slog.Int("code", ht.Response.StatusCode))
|
||||||
|
cl := datasize.ByteSize(ht.Response.ContentLength)
|
||||||
|
vals = append(vals, slog.String("content_length", cl.HumanReadable()))
|
||||||
|
headers := []slog.Attr{}
|
||||||
|
for k := range ht.Response.Header {
|
||||||
|
headers = append(headers, slog.String(k, ht.Response.Header.Get(k)))
|
||||||
|
}
|
||||||
|
if ht.ResponseBody.Len() > 0 {
|
||||||
|
if ht.Response.Header.Get("Content-Type") == "application/json" {
|
||||||
|
vals = append(vals, slog.Any("body", json.RawMessage(ht.ResponseBody.Bytes())))
|
||||||
|
} else {
|
||||||
|
vals = append(vals, slog.String("body", ht.ResponseBody.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values = append(values, slog.Attr{Key: "response", Value: slog.GroupValue(vals...)})
|
||||||
|
}
|
||||||
|
return slog.GroupValue(values...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpLogCollectorKey struct{}
|
||||||
|
|
||||||
|
// ContextWithRoundTripCollector injects an *HTTPLogCollector into given context.
|
||||||
|
func ContextWithRoundTripCollector(ctx context.Context) (context.Context, *RoundTripCollector) {
|
||||||
|
coll := &RoundTripCollector{}
|
||||||
|
return context.WithValue(ctx, httpLogCollectorKey{}, coll), coll
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTripCollectorFromContext gets an *HTTPLogCollector instance.
|
||||||
|
//
|
||||||
|
// Returns nil if not found.
|
||||||
|
func RoundTripCollectorFromContext(ctx context.Context) *RoundTripCollector {
|
||||||
|
coll, _ := ctx.Value(httpLogCollectorKey{}).(*RoundTripCollector)
|
||||||
|
return coll
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoundTripper struct {
|
||||||
|
Next http.RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoundTripper creates a new http log collector round tripper.
|
||||||
|
//
|
||||||
|
// If next is nil, uses http.DefaultTransport instead.
|
||||||
|
func NewRoundTripper(next http.RoundTripper) *RoundTripper {
|
||||||
|
if next == nil {
|
||||||
|
next = http.DefaultTransport
|
||||||
|
}
|
||||||
|
return &RoundTripper{next}
|
||||||
|
}
|
||||||
|
|
||||||
|
type bodyCloser struct {
|
||||||
|
io.Reader
|
||||||
|
close func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b bodyCloser) Close() error {
|
||||||
|
return b.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTrip implements http.RoundTripper
|
||||||
|
func (ht *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
coll := RoundTripCollectorFromContext(req.Context())
|
||||||
|
if coll == nil {
|
||||||
|
return ht.Next.RoundTrip(req)
|
||||||
|
}
|
||||||
|
coll.RequestBody = new(bytes.Buffer)
|
||||||
|
coll.ResponseBody = new(bytes.Buffer)
|
||||||
|
coll.Request = req
|
||||||
|
if coll.Request.Body != nil {
|
||||||
|
tee := io.TeeReader(coll.Request.Body, coll.RequestBody)
|
||||||
|
req.Body = bodyCloser{tee, req.Body.Close}
|
||||||
|
}
|
||||||
|
coll.Start = time.Now()
|
||||||
|
resp, err := ht.Next.RoundTrip(req)
|
||||||
|
coll.End = time.Now()
|
||||||
|
coll.Response = resp
|
||||||
|
if resp != nil {
|
||||||
|
coll.Request = resp.Request
|
||||||
|
tee := io.TeeReader(resp.Body, coll.ResponseBody)
|
||||||
|
resp.Body = bodyCloser{tee, resp.Body.Close}
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
|
@ -41,13 +41,20 @@ func (su *SubredditHandler) CountSubreddits(ctx context.Context, request *connec
|
||||||
// - connect.CodeInvalidArgument if validation failed, e.g. Invalid schedule cron format.
|
// - connect.CodeInvalidArgument if validation failed, e.g. Invalid schedule cron format.
|
||||||
// - connect.CodeNotFound if the subreddit does not exist.
|
// - connect.CodeNotFound if the subreddit does not exist.
|
||||||
func (su *SubredditHandler) CreateSubreddit(ctx context.Context, request *connect.Request[subreddits.CreateSubredditRequest]) (*connect.Response[subreddits.CreateSubredditResponse], error) {
|
func (su *SubredditHandler) CreateSubreddit(ctx context.Context, request *connect.Request[subreddits.CreateSubredditRequest]) (*connect.Response[subreddits.CreateSubredditResponse], error) {
|
||||||
|
resolveRequest := subredditConverter.ProtoCreateSubredditRequestToAPISubredditResolveNameRequest(request.Msg)
|
||||||
|
resolved, err := su.API.SubredditResolveName(ctx, resolveRequest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.IntoConnectError(err)
|
||||||
|
}
|
||||||
|
request.Msg.Name = resolved
|
||||||
|
|
||||||
data := subredditConverter.CreateSubredditRequestToModelsSubredditSetter(request.Msg)
|
data := subredditConverter.CreateSubredditRequestToModelsSubredditSetter(request.Msg)
|
||||||
if err := su.API.SubredditCreate(ctx, data); err != nil {
|
if err := su.API.SubredditCreate(ctx, data); err != nil {
|
||||||
return nil, errs.IntoConnectError(err)
|
return nil, errs.IntoConnectError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := &subreddits.CreateSubredditResponse{
|
resp := &subreddits.CreateSubredditResponse{
|
||||||
Name: data.Name.GetOrZero(),
|
Name: resolved,
|
||||||
}
|
}
|
||||||
|
|
||||||
return connect.NewResponse(resp), nil
|
return connect.NewResponse(resp), nil
|
||||||
|
@ -101,7 +108,7 @@ func (su *SubredditHandler) UpdateSubreddit(ctx context.Context, request *connec
|
||||||
// DeleteSubreddit deletes a subreddit.
|
// DeleteSubreddit deletes a subreddit.
|
||||||
//
|
//
|
||||||
// Returns error with connect.CodeNotFound if subreddit does not exist.
|
// Returns error with connect.CodeNotFound if subreddit does not exist.
|
||||||
func (su *SubredditHandler) DeleteSubreddit(_ context.Context, _ *connect.Request[subreddits.DeleteSubredditRequest]) (*connect.Response[subreddits.DeleteSubredditResponse], error) {
|
func (su *SubredditHandler) DeleteSubreddit(ctx context.Context, request *connect.Request[subreddits.DeleteSubredditRequest]) (*connect.Response[subreddits.DeleteSubredditResponse], error) {
|
||||||
panic("not implemented") // TODO: Implement
|
panic("not implemented") // TODO: Implement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +119,14 @@ func (su *SubredditHandler) DeleteSubreddit(_ context.Context, _ *connect.Reques
|
||||||
//
|
//
|
||||||
// Returns error with connect.CodeNotFound if subreddit does not exist.
|
// Returns error with connect.CodeNotFound if subreddit does not exist.
|
||||||
// So this rpc endpoint also acts to check subreddit's existence.
|
// So this rpc endpoint also acts to check subreddit's existence.
|
||||||
func (su *SubredditHandler) ResolveSubredditName(_ context.Context, _ *connect.Request[subreddits.ResolveSubredditNameRequest]) (*connect.Response[subreddits.ResolveSubredditNameResponse], error) {
|
func (su *SubredditHandler) ResolveSubredditName(ctx context.Context, request *connect.Request[subreddits.ResolveSubredditNameRequest]) (*connect.Response[subreddits.ResolveSubredditNameResponse], error) {
|
||||||
panic("not implemented") // TODO: Implement
|
req := subredditConverter.ProtoResolveSubredditNameRequestToAPISubredditResolveName(request.Msg)
|
||||||
|
resolved, err := su.API.SubredditResolveName(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.IntoConnectError(err)
|
||||||
|
}
|
||||||
|
resp := &subreddits.ResolveSubredditNameResponse{
|
||||||
|
Resolved: resolved,
|
||||||
|
}
|
||||||
|
return connect.NewResponse(resp), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,11 +86,9 @@ paths:
|
||||||
description: |-
|
description: |-
|
||||||
`after` can be filled with post `name`.
|
`after` can be filled with post `name`.
|
||||||
|
|
||||||
Easiest to find this value is in the `response body` on
|
Easiest to find this value is in the `response body` on `data.after`.
|
||||||
`data.after`.
|
|
||||||
|
|
||||||
`after` tells Reddit to look up posts after
|
`after` tells Reddit to look up posts after this value.
|
||||||
this value.
|
|
||||||
|
|
||||||
`after` cannot be used together with `before`.
|
`after` cannot be used together with `before`.
|
||||||
- in: query
|
- in: query
|
||||||
|
@ -114,11 +112,6 @@ paths:
|
||||||
|
|
||||||
Maximum value to fetch is `100`.
|
Maximum value to fetch is `100`.
|
||||||
example: 25
|
example: 25
|
||||||
- in: header
|
|
||||||
name: User-Agent
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: bluemage/v1
|
|
||||||
operationId: getListing
|
operationId: getListing
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
|
@ -133,6 +126,12 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
|
@ -185,44 +184,25 @@ components:
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/ListingChildData"
|
$ref: "#/components/schemas/ListingChildData"
|
||||||
ListingChildData:
|
ListingChildData:
|
||||||
oneOf:
|
|
||||||
- $ref: "#/components/schemas/T1"
|
|
||||||
- $ref: "#/components/schemas/T3"
|
|
||||||
T1:
|
|
||||||
type: object
|
type: object
|
||||||
description: |-
|
required:
|
||||||
This is listed because this item type is a possibility.
|
- kind
|
||||||
|
- data
|
||||||
`t1` item type is unwanted for fetching image posts since
|
|
||||||
it only contains comment data.
|
|
||||||
properties:
|
properties:
|
||||||
kind:
|
kind:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- t1
|
- t1
|
||||||
T3:
|
|
||||||
type: object
|
|
||||||
description: |-
|
|
||||||
`t3` item type is a post (link) data.
|
|
||||||
|
|
||||||
This is the item type to look for image posts.
|
|
||||||
|
|
||||||
Not all fields are listed here, only fields that are relevant are
|
|
||||||
listed to reduce deserializing errors.
|
|
||||||
properties:
|
|
||||||
kind:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- t3
|
- t3
|
||||||
data:
|
data:
|
||||||
$ref: "#/components/schemas/T3Data"
|
$ref: "#/components/schemas/ChildData"
|
||||||
|
ChildData:
|
||||||
T3Data:
|
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- subreddit
|
- subreddit
|
||||||
- title
|
- title
|
||||||
- name
|
- name
|
||||||
|
- author
|
||||||
properties:
|
properties:
|
||||||
subreddit:
|
subreddit:
|
||||||
type: string
|
type: string
|
||||||
|
|
16
schemas/proto/subreddits/v1/exist.proto
Normal file
16
schemas/proto/subreddits/v1/exist.proto
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package subreddits.v1;
|
||||||
|
|
||||||
|
import "buf/validate/validate.proto";
|
||||||
|
|
||||||
|
option go_package = "github.com/tigorlazuardi/bluemage/go/gen/proto/subreddits/v1";
|
||||||
|
|
||||||
|
message SubredditExistsRequest {
|
||||||
|
// name of the subreddit. Case insensitive.
|
||||||
|
string name = 1 [(buf.validate.field).string.min_len = 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubredditExistsResponse {
|
||||||
|
bool exist = 1;
|
||||||
|
}
|
|
@ -3,12 +3,14 @@ syntax = "proto3";
|
||||||
package subreddits.v1;
|
package subreddits.v1;
|
||||||
|
|
||||||
import "buf/validate/validate.proto";
|
import "buf/validate/validate.proto";
|
||||||
|
import "subreddits/v1/types.proto";
|
||||||
|
|
||||||
option go_package = "github.com/tigorlazuardi/bluemage/go/gen/proto/subreddits/v1";
|
option go_package = "github.com/tigorlazuardi/bluemage/go/gen/proto/subreddits/v1";
|
||||||
|
|
||||||
message ResolveSubredditNameRequest {
|
message ResolveSubredditNameRequest {
|
||||||
// name of the subreddit to resolve (check existence and casing).
|
// name of the subreddit to resolve (check existence and casing).
|
||||||
string name = 1 [(buf.validate.field).string.min_len = 1];
|
string name = 1 [(buf.validate.field).string.min_len = 1];
|
||||||
|
SubredditType type = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ResolveSubredditNameResponse {
|
message ResolveSubredditNameResponse {
|
||||||
|
|
|
@ -5,6 +5,7 @@ package subreddits.v1;
|
||||||
import "subreddits/v1/count.proto";
|
import "subreddits/v1/count.proto";
|
||||||
import "subreddits/v1/create.proto";
|
import "subreddits/v1/create.proto";
|
||||||
import "subreddits/v1/delete.proto";
|
import "subreddits/v1/delete.proto";
|
||||||
|
import "subreddits/v1/exist.proto";
|
||||||
import "subreddits/v1/get.proto";
|
import "subreddits/v1/get.proto";
|
||||||
import "subreddits/v1/list.proto";
|
import "subreddits/v1/list.proto";
|
||||||
import "subreddits/v1/resolve.proto";
|
import "subreddits/v1/resolve.proto";
|
||||||
|
@ -56,4 +57,11 @@ service SubredditsService {
|
||||||
//
|
//
|
||||||
// Default values count all.
|
// Default values count all.
|
||||||
rpc CountSubreddits(CountSubredditsRequest) returns (CountSubredditsResponse);
|
rpc CountSubreddits(CountSubredditsRequest) returns (CountSubredditsResponse);
|
||||||
|
|
||||||
|
// SubredditExists checks if the subreddits already handled in database.
|
||||||
|
//
|
||||||
|
// THIS ENDPOINT DOES NOT CALL REDDIT API, ONLY DATABASE.
|
||||||
|
//
|
||||||
|
// Use ResolveSubredditName to check if subreddit actually exists in Reddit.
|
||||||
|
rpc SubredditExists(SubredditExistsRequest) returns (SubredditExistsResponse);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue