Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 993d64b9d1 | |||
| da7385572c | |||
| 966887f3ba | |||
| 5fd0a73741 | |||
| db30aeabf5 | |||
| 8b669910e4 | |||
| f01ac7b716 | |||
| 9edacef768 | |||
| b22e50d439 | |||
| 46c74e6b9b | |||
| 4711b2b160 | |||
| afa9507ee2 | |||
| 8a4f704788 | |||
| 7e2677c495 | |||
| c40ad29ae1 | |||
| 2511d0dcc7 | |||
| 5a7463d006 | |||
| 0d50afac8e | |||
| 833745395f | |||
| f8a84454d8 | |||
| fb99765a2a |
@@ -4,9 +4,9 @@
|
||||
|
||||
Go bindings to the [Lemmy](https://join-lemmy.org) API, automatically generated from Lemmy's source code using the generator in [cmd/gen](cmd/gen).
|
||||
|
||||
Examples:
|
||||
### Examples
|
||||
|
||||
- HTTP: [examples/http](examples/http)
|
||||
Examples can be found in the [examples](examples) directory.
|
||||
|
||||
### How to generate
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ type Route struct {
|
||||
Method string
|
||||
Path string
|
||||
ParamsName string
|
||||
ParamsID int64
|
||||
ReturnName string
|
||||
ReturnID int64
|
||||
}
|
||||
|
||||
type Struct struct {
|
||||
@@ -36,6 +34,7 @@ type Extractor struct {
|
||||
root gjson.Result
|
||||
}
|
||||
|
||||
// New parses the file at path and returns an extractor with its contents.
|
||||
func New(path string) (*Extractor, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -45,13 +44,20 @@ func New(path string) (*Extractor, error) {
|
||||
return &Extractor{gjson.ParseBytes(data)}, nil
|
||||
}
|
||||
|
||||
func (e *Extractor) Routes() []Route {
|
||||
// Extract reads the JSON document and extracts all the routes and structs from it.
|
||||
func (e *Extractor) Extract() ([]Route, []Struct) {
|
||||
structs := map[int64]Struct{}
|
||||
var out []Route
|
||||
|
||||
// Get all the routes in the JSON document
|
||||
routes := e.root.Get("children.#.children.#(kind==2048)#|@flatten")
|
||||
|
||||
for _, route := range routes.Array() {
|
||||
name := route.Get("name").String()
|
||||
signature := route.Get(`signatures.0`)
|
||||
|
||||
// Get the code part of the route's summary.
|
||||
// This will contain the HTTP method and path.
|
||||
httpInfo := signature.Get(`comment.summary.#(kind=="code").text`).String()
|
||||
if !strings.HasPrefix(httpInfo, "`HTTP") {
|
||||
continue
|
||||
@@ -63,37 +69,49 @@ func (e *Extractor) Routes() []Route {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the ID and name of the type this function accepts
|
||||
paramsID := signature.Get("parameters.0.type.target").Int()
|
||||
paramsName := signature.Get("parameters.0.type.name").String()
|
||||
|
||||
// Get the ID and name of the type this function returns
|
||||
returnID := signature.Get("type.typeArguments.0.target").Int()
|
||||
returnName := signature.Get("type.typeArguments.0.name").String()
|
||||
|
||||
// Get the referenced structs from the JSON document
|
||||
e.getStructs([]int64{paramsID, returnID}, structs)
|
||||
|
||||
// If the parameters struct contains no fields or union names
|
||||
if len(structs[paramsID].Fields) == 0 && len(structs[paramsID].UnionNames) == 0 {
|
||||
// Delete the params struct from the structs map
|
||||
// to make sure it doesn't get generated
|
||||
delete(structs, paramsID)
|
||||
|
||||
// Set paramsName to an empty string to signify that this route
|
||||
// has no input parameters.
|
||||
paramsName = ""
|
||||
}
|
||||
|
||||
// If the return struct contains no fields or union names
|
||||
if len(structs[returnID].Fields) == 0 && len(structs[returnID].UnionNames) == 0 {
|
||||
// Delete the return struct from the structs map
|
||||
// to make sure it doesn't get generated
|
||||
delete(structs, returnID)
|
||||
|
||||
// Set paramsName to an empty string to signify that this route
|
||||
// has no return value.
|
||||
returnName = ""
|
||||
}
|
||||
|
||||
out = append(out, Route{
|
||||
Name: name,
|
||||
Summary: summary,
|
||||
Method: method,
|
||||
Path: path,
|
||||
ParamsName: paramsName,
|
||||
ParamsID: paramsID,
|
||||
ReturnName: returnName,
|
||||
ReturnID: returnID,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (e *Extractor) Structs(routes []Route) []Struct {
|
||||
var ids []int64
|
||||
for _, route := range routes {
|
||||
ids = append(ids, route.ParamsID)
|
||||
if route.ReturnID != 0 {
|
||||
ids = append(ids, route.ReturnID)
|
||||
}
|
||||
}
|
||||
|
||||
structs := map[int64]Struct{}
|
||||
e.getStructs(ids, structs)
|
||||
return getKeys(structs)
|
||||
return out, getStructSlice(structs)
|
||||
}
|
||||
|
||||
func (e *Extractor) getStructs(ids []int64, structs map[int64]Struct) {
|
||||
@@ -102,6 +120,7 @@ func (e *Extractor) getStructs(ids []int64, structs map[int64]Struct) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the struct with the given ID from the JSON document
|
||||
jstruct := e.root.Get(fmt.Sprintf("children.#(id==%d)", id))
|
||||
if !jstruct.Exists() {
|
||||
continue
|
||||
@@ -122,12 +141,14 @@ func (e *Extractor) getStructs(ids []int64, structs map[int64]Struct) {
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
// Recursively get any structs referenced by this one
|
||||
e.getStructs(newIDs, structs)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// unionNames gets all the names of union type members
|
||||
func (e *Extractor) unionNames(jstruct gjson.Result) []string {
|
||||
jnames := jstruct.Get("type.types").Array()
|
||||
out := make([]string, len(jnames))
|
||||
@@ -137,6 +158,8 @@ func (e *Extractor) unionNames(jstruct gjson.Result) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// fields gets all the fields in a given struct from the JSON document.
|
||||
// It returns the fields and the IDs of any types they referenced.
|
||||
func (e *Extractor) fields(jstruct gjson.Result) ([]Field, []int64) {
|
||||
var fields []Field
|
||||
var ids []int64
|
||||
@@ -153,8 +176,11 @@ func (e *Extractor) fields(jstruct gjson.Result) ([]Field, []int64) {
|
||||
|
||||
switch jfield.Get("type.elementType.type").String() {
|
||||
case "reference":
|
||||
// If this field is referencing another type, add that type's id
|
||||
// to the ids slice.
|
||||
ids = append(ids, jfield.Get("type.elementType.target").Int())
|
||||
case "union":
|
||||
// Convert unions to strings
|
||||
field.Type = "string"
|
||||
}
|
||||
} else {
|
||||
@@ -162,8 +188,11 @@ func (e *Extractor) fields(jstruct gjson.Result) ([]Field, []int64) {
|
||||
|
||||
switch jfield.Get("type.type").String() {
|
||||
case "reference":
|
||||
// If this field is referencing another type, add that type's id
|
||||
// to the ids slice.
|
||||
ids = append(ids, jfield.Get("type.target").Int())
|
||||
case "union":
|
||||
// Convert unions to strings
|
||||
field.Type = "string"
|
||||
}
|
||||
}
|
||||
@@ -173,6 +202,8 @@ func (e *Extractor) fields(jstruct gjson.Result) ([]Field, []int64) {
|
||||
return fields, ids
|
||||
}
|
||||
|
||||
// parseHTTPInfo parses the string from a route's summary,
|
||||
// and returns the method and path it uses
|
||||
func parseHTTPInfo(httpInfo string) (method, path string) {
|
||||
httpInfo = strings.Trim(httpInfo, "`")
|
||||
method, path, _ = strings.Cut(httpInfo, " ")
|
||||
@@ -181,7 +212,8 @@ func parseHTTPInfo(httpInfo string) (method, path string) {
|
||||
return method, path
|
||||
}
|
||||
|
||||
func getKeys(m map[int64]Struct) []Struct {
|
||||
// getStructSlice returns all the structs in a map
|
||||
func getStructSlice(m map[int64]Struct) []Struct {
|
||||
out := make([]Struct, len(m))
|
||||
i := 0
|
||||
for _, s := range m {
|
||||
|
||||
@@ -22,55 +22,59 @@ func (r *RoutesGenerator) Generate(routes []extractor.Route) error {
|
||||
f.HeaderComment("Code generated by go.elara.ws/go-lemmy/cmd/gen (routes generator). DO NOT EDIT.")
|
||||
|
||||
for _, r := range routes {
|
||||
|
||||
f.Comment(r.Summary)
|
||||
f.Func().Params(
|
||||
jen.Id("c").Id("*Client"),
|
||||
).Id(transformName(r.Name)).Params(
|
||||
jen.Id("ctx").Qual("context", "Context"),
|
||||
jen.Id("data").Qual("go.elara.ws/go-lemmy/types", r.ParamsName),
|
||||
).ParamsFunc(func(g *jen.Group) {
|
||||
).Id(transformName(r.Name)).ParamsFunc(func(g *jen.Group) {
|
||||
g.Id("ctx").Qual("context", "Context")
|
||||
if r.ParamsName != "" {
|
||||
g.Id("data").Id(r.ParamsName)
|
||||
}
|
||||
}).ParamsFunc(func(g *jen.Group) {
|
||||
if r.ReturnName != "" {
|
||||
g.Op("*").Qual("go.elara.ws/go-lemmy/types", r.ReturnName)
|
||||
g.Op("*").Id(r.ReturnName)
|
||||
}
|
||||
g.Error()
|
||||
}).BlockFunc(func(g *jen.Group) {
|
||||
returnName := r.ReturnName
|
||||
if returnName == "" {
|
||||
returnName = "EmptyResponse"
|
||||
data := jen.Id("data")
|
||||
// If there are no parameters, set the data to nil
|
||||
if r.ParamsName == "" {
|
||||
data = jen.Nil()
|
||||
}
|
||||
|
||||
g.Id("resData").Op(":=").Op("&").Qual("go.elara.ws/go-lemmy/types", returnName).Block()
|
||||
returnName := r.ReturnName
|
||||
if returnName == "" {
|
||||
returnName = "emptyResponse"
|
||||
}
|
||||
|
||||
var funcName string
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
g.Id("resData").Op(":=").Op("&").Id(returnName).Block()
|
||||
|
||||
funcName := "req"
|
||||
if r.Method == "GET" {
|
||||
funcName = "getReq"
|
||||
default:
|
||||
funcName = "req"
|
||||
}
|
||||
|
||||
g.List(jen.Id("res"), jen.Err()).Op(":=").Id("c").Dot(funcName).Params(
|
||||
jen.Id("ctx"), jen.Lit(r.Method), jen.Lit(r.Path), jen.Id("data"), jen.Op("&").Id("resData"),
|
||||
jen.Id("ctx"), jen.Lit(r.Method), jen.Lit(r.Path), data, jen.Id("resData"),
|
||||
)
|
||||
g.If(jen.Err().Op("!=").Nil()).BlockFunc(func(g *jen.Group) {
|
||||
if returnName == "EmptyResponse" {
|
||||
if returnName == "emptyResponse" {
|
||||
g.Return(jen.Err())
|
||||
} else {
|
||||
g.Return(jen.Nil(), jen.Err())
|
||||
}
|
||||
})
|
||||
|
||||
g.Err().Op("=").Id("resError").Params(jen.Id("res"), jen.Id("resData").Dot("LemmyResponse"))
|
||||
g.Err().Op("=").Id("resError").Params(jen.Id("res"), jen.Id("resData").Dot("Error"))
|
||||
g.If(jen.Err().Op("!=").Nil()).BlockFunc(func(g *jen.Group) {
|
||||
if returnName == "EmptyResponse" {
|
||||
if returnName == "emptyResponse" {
|
||||
g.Return(jen.Err())
|
||||
} else {
|
||||
g.Return(jen.Nil(), jen.Err())
|
||||
}
|
||||
})
|
||||
|
||||
if returnName == "EmptyResponse" {
|
||||
if returnName == "emptyResponse" {
|
||||
g.Return(jen.Nil())
|
||||
} else {
|
||||
g.Return(jen.Id("resData"), jen.Nil())
|
||||
|
||||
@@ -35,14 +35,14 @@ func (s *StructGenerator) Generate(items []extractor.Struct) error {
|
||||
} else {
|
||||
f.Type().Id(item.Name).StructFunc(func(g *jen.Group) {
|
||||
for _, field := range item.Fields {
|
||||
g.Id(transformFieldName(field.Name)).Id(getType(field)).Tag(map[string]string{
|
||||
g.Id(transformFieldName(field.Name)).Add(getType(field)).Tag(map[string]string{
|
||||
"json": field.Name,
|
||||
"url": field.Name + ",omitempty",
|
||||
})
|
||||
}
|
||||
|
||||
if strings.HasSuffix(item.Name, "Response") {
|
||||
g.Id("LemmyResponse")
|
||||
g.Id("Error").Id("Optional").Types(jen.String()).Tag(map[string]string{"json": "error"})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -51,7 +51,19 @@ func (s *StructGenerator) Generate(items []extractor.Struct) error {
|
||||
return f.Render(s.w)
|
||||
}
|
||||
|
||||
func getType(f extractor.Field) string {
|
||||
func getType(f extractor.Field) jen.Code {
|
||||
// Some time fields are strings in the JS client,
|
||||
// use time.Time for those
|
||||
switch f.Name {
|
||||
case "published", "updated", "when_":
|
||||
return jen.Qual("time", "Time")
|
||||
}
|
||||
|
||||
// Rank types such as hot_rank and hot_rank_active may be floats.
|
||||
if strings.Contains(f.Name, "rank") {
|
||||
return jen.Float64()
|
||||
}
|
||||
|
||||
t := transformType(f.Name, f.Type)
|
||||
if f.IsArray {
|
||||
t = "[]" + t
|
||||
@@ -59,20 +71,13 @@ func getType(f extractor.Field) string {
|
||||
if f.IsOptional {
|
||||
t = "Optional[" + t + "]"
|
||||
}
|
||||
return t
|
||||
return jen.Id(t)
|
||||
}
|
||||
|
||||
func transformType(name, t string) string {
|
||||
// Some time fields are strings in the JS client,
|
||||
// use LemmyTime for those
|
||||
switch name {
|
||||
case "published", "updated":
|
||||
return "LemmyTime"
|
||||
}
|
||||
|
||||
switch t {
|
||||
case "number":
|
||||
return "float64"
|
||||
return "int64"
|
||||
case "boolean":
|
||||
return "bool"
|
||||
default:
|
||||
@@ -89,6 +94,11 @@ func transformFieldName(s string) string {
|
||||
"Jwt", "JWT",
|
||||
"Crud", "CRUD",
|
||||
"Pm", "PM",
|
||||
"Totp", "TOTP",
|
||||
"2fa", "2FA",
|
||||
"Png", "PNG",
|
||||
"Uuid", "UUID",
|
||||
"Wav", "WAV",
|
||||
).Replace(s)
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -25,21 +25,15 @@ func main() {
|
||||
log.Fatal("Error creating extractor").Err(err).Send()
|
||||
}
|
||||
|
||||
routes := e.Routes()
|
||||
structs := e.Structs(routes)
|
||||
routes, structs := e.Extract()
|
||||
|
||||
err = os.MkdirAll(filepath.Join(*outDir, "types"), 0o755)
|
||||
if err != nil {
|
||||
log.Fatal("Error creating types directory").Err(err).Send()
|
||||
}
|
||||
|
||||
otf, err := os.Create(filepath.Join(*outDir, "types/types.gen.go"))
|
||||
otf, err := os.Create(filepath.Join(*outDir, "types.gen.go"))
|
||||
if err != nil {
|
||||
log.Fatal("Error creating types output file").Err(err).Send()
|
||||
}
|
||||
defer otf.Close()
|
||||
|
||||
err = generator.NewStruct(otf, "types").Generate(structs)
|
||||
err = generator.NewStruct(otf, "lemmy").Generate(structs)
|
||||
if err != nil {
|
||||
log.Fatal("Error generating output routes file").Err(err).Send()
|
||||
}
|
||||
|
||||
@@ -4,18 +4,17 @@ import (
|
||||
"context"
|
||||
|
||||
"go.elara.ws/go-lemmy"
|
||||
"go.elara.ws/go-lemmy/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
c, err := lemmy.New("https://lemmygrad.ml")
|
||||
c, err := lemmy.New("https://lemmy.ml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = c.ClientLogin(ctx, types.Login{
|
||||
err = c.ClientLogin(ctx, lemmy.Login{
|
||||
UsernameOrEmail: "user@example.com",
|
||||
Password: `TestPwd`,
|
||||
})
|
||||
@@ -23,8 +22,8 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = c.SaveUserSettings(ctx, types.SaveUserSettings{
|
||||
BotAccount: types.NewOptional(true),
|
||||
_, err = c.SaveUserSettings(ctx, lemmy.SaveUserSettings{
|
||||
BotAccount: lemmy.NewOptional(true),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
48
examples/comments/main.go
Normal file
48
examples/comments/main.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"go.elara.ws/go-lemmy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
c, err := lemmy.New("https://lemmy.ml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = c.ClientLogin(ctx, lemmy.Login{
|
||||
UsernameOrEmail: "user@example.com",
|
||||
Password: `TestPwd`,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cr, err := c.CreateComment(ctx, lemmy.CreateComment{
|
||||
PostID: 2,
|
||||
Content: "Hello from go-lemmy!",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cr2, err := c.CreateComment(ctx, lemmy.CreateComment{
|
||||
PostID: 2,
|
||||
ParentID: lemmy.NewOptional(cr.CommentView.Comment.ID),
|
||||
Content: "Reply from go-lemmy",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"Created comment %d and replied to it with comment %d!\n",
|
||||
cr.CommentView.Comment.ID,
|
||||
cr2.CommentView.Comment.ID,
|
||||
)
|
||||
}
|
||||
43
examples/posts/main.go
Normal file
43
examples/posts/main.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"go.elara.ws/go-lemmy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
c, err := lemmy.New("https://lemmy.ml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Log in to lemmy.ml
|
||||
err = c.ClientLogin(ctx, lemmy.Login{
|
||||
UsernameOrEmail: "user@example.com",
|
||||
Password: `TestPwd`,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Get the linux community to get its ID.
|
||||
gcr, err := c.Community(ctx, lemmy.GetCommunity{
|
||||
Name: lemmy.NewOptional("linux"),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create a Hello World post in the linux community.
|
||||
pr, err := c.CreatePost(ctx, lemmy.CreatePost{
|
||||
CommunityID: gcr.CommunityView.Community.ID,
|
||||
Name: "Hello, World!",
|
||||
Body: lemmy.NewOptional("This is an example post"),
|
||||
})
|
||||
|
||||
log.Println("Created post:", pr.PostView.Post.ID)
|
||||
}
|
||||
115
lemmy.go
115
lemmy.go
@@ -4,15 +4,18 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
"go.elara.ws/go-lemmy/types"
|
||||
)
|
||||
|
||||
// ErrNoToken is an error returned by ClientLogin if the server sends a null or empty token
|
||||
var ErrNoToken = errors.New("the server didn't provide a token value in its response")
|
||||
|
||||
// Client is a client for Lemmy's HTTP API
|
||||
type Client struct {
|
||||
client *http.Client
|
||||
@@ -32,27 +35,30 @@ func NewWithClient(baseURL string, client *http.Client) (*Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
u = u.JoinPath("/api/v3")
|
||||
|
||||
return &Client{baseURL: u, client: client}, nil
|
||||
}
|
||||
|
||||
// ClientLogin logs in to Lemmy by sending an HTTP request to the
|
||||
// login endpoint. It stores the returned token in the client
|
||||
// for future use.
|
||||
func (c *Client) ClientLogin(ctx context.Context, l types.Login) error {
|
||||
lr, err := c.Login(ctx, l)
|
||||
// ClientLogin logs in to Lemmy by calling the login endpoint, and
|
||||
// stores the returned token in the Token field for use in future requests.
|
||||
//
|
||||
// The Token field can be set manually if you'd like to persist the
|
||||
// token somewhere.
|
||||
func (c *Client) ClientLogin(ctx context.Context, data Login) error {
|
||||
lr, err := c.Login(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Token = lr.JWT.MustValue()
|
||||
token, ok := lr.JWT.Value()
|
||||
if !ok || token == "" {
|
||||
return ErrNoToken
|
||||
}
|
||||
c.Token = token
|
||||
return nil
|
||||
}
|
||||
|
||||
// req makes a request to the server
|
||||
func (c *Client) req(ctx context.Context, method string, path string, data, resp any) (*http.Response, error) {
|
||||
data = c.setAuth(data)
|
||||
|
||||
var r io.Reader
|
||||
if data != nil {
|
||||
jsonData, err := json.Marshal(data)
|
||||
@@ -74,6 +80,10 @@ func (c *Client) req(ctx context.Context, method string, path string, data, resp
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
if c.Token != "" {
|
||||
req.Header.Add("Authorization", "Bearer "+c.Token)
|
||||
}
|
||||
|
||||
res, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -91,17 +101,17 @@ func (c *Client) req(ctx context.Context, method string, path string, data, resp
|
||||
}
|
||||
|
||||
// getReq makes a get request to the Lemmy server.
|
||||
// It is separate from req() because it uses query
|
||||
// It's separate from req() because it uses query
|
||||
// parameters rather than a JSON request body.
|
||||
func (c *Client) getReq(ctx context.Context, method string, path string, data, resp any) (*http.Response, error) {
|
||||
data = c.setAuth(data)
|
||||
|
||||
getURL := c.baseURL.JoinPath(path)
|
||||
vals, err := query.Values(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if data != nil {
|
||||
vals, err := query.Values(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getURL.RawQuery = vals.Encode()
|
||||
}
|
||||
getURL.RawQuery = vals.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
@@ -113,6 +123,10 @@ func (c *Client) getReq(ctx context.Context, method string, path string, data, r
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.Token != "" {
|
||||
req.Header.Add("Authorization", "Bearer "+c.Token)
|
||||
}
|
||||
|
||||
res, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -129,50 +143,39 @@ func (c *Client) getReq(ctx context.Context, method string, path string, data, r
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// resError returns an error if the given response is an error
|
||||
func resError(res *http.Response, lr types.LemmyResponse) error {
|
||||
if lr.Error.IsValid() {
|
||||
return types.LemmyError{
|
||||
// Error represents an error returned by the Lemmy API
|
||||
type Error struct {
|
||||
ErrStr string
|
||||
Code int
|
||||
}
|
||||
|
||||
func (le Error) Error() string {
|
||||
if le.ErrStr != "" {
|
||||
return fmt.Sprintf("%d %s: %s", le.Code, http.StatusText(le.Code), le.ErrStr)
|
||||
} else {
|
||||
return fmt.Sprintf("%d %s", le.Code, http.StatusText(le.Code))
|
||||
}
|
||||
}
|
||||
|
||||
// emptyResponse is a response without any fields.
|
||||
// It has an Error field to capture any errors.
|
||||
type emptyResponse struct {
|
||||
Error Optional[string] `json:"error"`
|
||||
}
|
||||
|
||||
// resError checks if the response contains an error, and if so, returns
|
||||
// a Go error representing it.
|
||||
func resError(res *http.Response, err Optional[string]) error {
|
||||
if errstr, ok := err.Value(); ok {
|
||||
return Error{
|
||||
Code: res.StatusCode,
|
||||
ErrStr: lr.Error.MustValue(),
|
||||
ErrStr: errstr,
|
||||
}
|
||||
} else if res.StatusCode != http.StatusOK {
|
||||
return types.HTTPError{
|
||||
return Error{
|
||||
Code: res.StatusCode,
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// setAuth uses reflection to automatically
|
||||
// set struct fields called Auth of type
|
||||
// string or types.Optional[string] to the
|
||||
// authentication token, then returns the
|
||||
// updated struct
|
||||
func (c *Client) setAuth(data any) any {
|
||||
if data == nil {
|
||||
return data
|
||||
}
|
||||
|
||||
val := reflect.New(reflect.TypeOf(data))
|
||||
val.Elem().Set(reflect.ValueOf(data))
|
||||
|
||||
authField := val.Elem().FieldByName("Auth")
|
||||
if !authField.IsValid() {
|
||||
return data
|
||||
}
|
||||
|
||||
switch authField.Type().String() {
|
||||
case "string":
|
||||
authField.SetString(c.Token)
|
||||
case "types.Optional[string]":
|
||||
setMtd := authField.MethodByName("Set")
|
||||
out := setMtd.Call([]reflect.Value{reflect.ValueOf(c.Token)})
|
||||
authField.Set(out[0])
|
||||
default:
|
||||
return data
|
||||
}
|
||||
|
||||
return val.Elem().Interface()
|
||||
}
|
||||
|
||||
@@ -1,64 +1,53 @@
|
||||
package types
|
||||
package lemmy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var ErrOptionalEmpty = errors.New("optional value is empty")
|
||||
|
||||
// Optional represents an optional value
|
||||
type Optional[T any] struct {
|
||||
value *T
|
||||
}
|
||||
|
||||
// NewOptional creates an optional with value v
|
||||
func NewOptional[T any](v T) Optional[T] {
|
||||
return Optional[T]{value: &v}
|
||||
}
|
||||
|
||||
func NewOptionalPtr[T any](v *T) Optional[T] {
|
||||
return Optional[T]{value: v}
|
||||
}
|
||||
|
||||
// NewOptionalNil creates a new nil optional value
|
||||
func NewOptionalNil[T any]() Optional[T] {
|
||||
return Optional[T]{}
|
||||
}
|
||||
|
||||
// Set sets the value of the optional
|
||||
func (o Optional[T]) Set(v T) Optional[T] {
|
||||
o.value = &v
|
||||
return o
|
||||
}
|
||||
|
||||
func (o Optional[T]) SetPtr(v *T) Optional[T] {
|
||||
o.value = v
|
||||
return o
|
||||
}
|
||||
|
||||
// SetNil sets the optional value to nil
|
||||
func (o Optional[T]) SetNil() Optional[T] {
|
||||
o.value = nil
|
||||
return o
|
||||
}
|
||||
|
||||
// IsValid returns true if the value of the optional is not nil
|
||||
func (o Optional[T]) IsValid() bool {
|
||||
return o.value != nil
|
||||
}
|
||||
|
||||
func (o Optional[T]) MustValue() T {
|
||||
if o.value == nil {
|
||||
panic("optional value is nil")
|
||||
}
|
||||
return *o.value
|
||||
}
|
||||
|
||||
func (o Optional[T]) Value() (T, error) {
|
||||
// Value returns the value in the optional.
|
||||
func (o Optional[T]) Value() (T, bool) {
|
||||
if o.value != nil {
|
||||
return *o.value, ErrOptionalEmpty
|
||||
return *o.value, true
|
||||
}
|
||||
return *new(T), nil
|
||||
return *new(T), false
|
||||
}
|
||||
|
||||
// ValueOr returns the value inside the optional if it exists, or else it returns fallback
|
||||
func (o Optional[T]) ValueOr(fallback T) T {
|
||||
if o.value != nil {
|
||||
return *o.value
|
||||
@@ -66,7 +55,8 @@ func (o Optional[T]) ValueOr(fallback T) T {
|
||||
return fallback
|
||||
}
|
||||
|
||||
func (o Optional[T]) ValueOrEmpty() T {
|
||||
// ValueOrZero returns the value inside the optional if it exists, or else it returns the zero value of T
|
||||
func (o Optional[T]) ValueOrZero() T {
|
||||
if o.value != nil {
|
||||
return *o.value
|
||||
}
|
||||
@@ -74,10 +64,12 @@ func (o Optional[T]) ValueOrEmpty() T {
|
||||
return value
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the optional value as JSON
|
||||
func (o Optional[T]) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(o.value)
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes JSON into the optional value
|
||||
func (o *Optional[T]) UnmarshalJSON(b []byte) error {
|
||||
if bytes.Equal(b, []byte("null")) {
|
||||
o.value = nil
|
||||
@@ -88,6 +80,7 @@ func (o *Optional[T]) UnmarshalJSON(b []byte) error {
|
||||
return json.Unmarshal(b, o.value)
|
||||
}
|
||||
|
||||
// EncodeValues encodes the optional as a URL query parameter
|
||||
func (o Optional[T]) EncodeValues(key string, v *url.Values) error {
|
||||
s := o.String()
|
||||
if s != "<nil>" {
|
||||
@@ -96,6 +89,7 @@ func (o Optional[T]) EncodeValues(key string, v *url.Values) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the string representation of the optional value
|
||||
func (o Optional[T]) String() string {
|
||||
if o.value == nil {
|
||||
return "<nil>"
|
||||
@@ -103,6 +97,7 @@ func (o Optional[T]) String() string {
|
||||
return fmt.Sprint(*o.value)
|
||||
}
|
||||
|
||||
// GoString returns the Go representation of the optional value
|
||||
func (o Optional[T]) GoString() string {
|
||||
if o.value == nil {
|
||||
return "nil"
|
||||
818
routes.gen.go
818
routes.gen.go
File diff suppressed because it is too large
Load Diff
1552
types.gen.go
Normal file
1552
types.gen.go
Normal file
File diff suppressed because it is too large
Load Diff
1596
types/types.gen.go
1596
types/types.gen.go
File diff suppressed because it is too large
Load Diff
@@ -1,67 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EmptyResponse struct {
|
||||
LemmyResponse
|
||||
}
|
||||
|
||||
type LemmyResponse struct {
|
||||
Error Optional[string] `json:"error" url:"error,omitempty"`
|
||||
}
|
||||
|
||||
type HTTPError struct {
|
||||
Code int
|
||||
}
|
||||
|
||||
func (he HTTPError) Error() string {
|
||||
return fmt.Sprintf("%d %s", he.Code, http.StatusText(he.Code))
|
||||
}
|
||||
|
||||
type LemmyError struct {
|
||||
ErrStr string
|
||||
Code int
|
||||
}
|
||||
|
||||
func (le LemmyError) Error() string {
|
||||
return fmt.Sprintf("%d %s: %s", le.Code, http.StatusText(le.Code), le.ErrStr)
|
||||
}
|
||||
|
||||
type LemmyTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
func (lt LemmyTime) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(lt.Time.Format("2006-01-02T15:04:05"))
|
||||
}
|
||||
|
||||
func (lt *LemmyTime) UnmarshalJSON(b []byte) error {
|
||||
var timeStr string
|
||||
err := json.Unmarshal(b, &timeStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if timeStr == "" {
|
||||
lt.Time = time.Unix(0, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
t, err := time.Parse("2006-01-02T15:04:05", timeStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lt.Time = t
|
||||
return nil
|
||||
}
|
||||
|
||||
type LemmyWebSocketMsg struct {
|
||||
Op string `json:"op"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
Reference in New Issue
Block a user