Compare commits

...

23 Commits

Author SHA1 Message Date
Elara 7c464b1640 Add FUNDING.yml 2023-12-15 16:08:14 -08:00
Elara 27fc1cd810 Fix ExportSettings endpoint 2023-12-15 09:25:29 -08:00
Elara 993d64b9d1 Update for Lemmy 0.19.0 2023-12-15 08:53:22 -08:00
Elara da7385572c Update examples section in README 2023-10-09 19:51:41 +00:00
Elara 966887f3ba Fix comments example printf 2023-10-06 07:12:54 -07:00
Elara 5fd0a73741 Remove unnecessary pointer 2023-10-05 13:44:29 -07:00
Elara db30aeabf5 Add more examples 2023-10-05 13:20:12 -07:00
Elara 8b669910e4 Clarify comments 2023-10-04 21:13:20 -07:00
Elara f01ac7b716 Merge HTTPError into Error 2023-10-04 21:08:55 -07:00
Elara 9edacef768 Rename LemmyError to Error to remove stutter 2023-10-04 20:54:36 -07:00
Elara b22e50d439 Remove MustValue from optional type 2023-10-04 20:47:30 -07:00
Elara 46c74e6b9b Remove lemmyResponse type 2023-10-04 20:45:36 -07:00
Elara 4711b2b160 Add comment for ErrNoToken 2023-10-04 17:14:28 -07:00
Elara afa9507ee2 Remove panic from ClientLogin 2023-10-04 17:13:27 -07:00
Elara 8a4f704788 Return bool from Optional[T].Value() instead of an error 2023-10-04 16:44:07 -07:00
Elara 7e2677c495 Unexport types that no longer need to be exported 2023-10-04 16:28:52 -07:00
Elara c40ad29ae1 Merge types package into root lemmy package 2023-10-04 16:23:31 -07:00
Elara 2511d0dcc7 Update for 0.19.0-rc.1 2023-10-04 16:16:36 -07:00
Elara 5a7463d006 Add some comments and separate stuff into different files 2023-09-25 09:55:08 -07:00
Elara 0d50afac8e Handle lack of parameters 2023-09-24 21:48:00 -07:00
Elara 833745395f Fix some more capitalization and remove WebsocketMsg 2023-09-24 20:51:23 -07:00
Elara f8a84454d8 Handle when_ fields 2023-09-24 20:40:00 -07:00
Elara fb99765a2a Use int64 instead of float64 and fix some capitalization 2023-09-24 20:16:12 -07:00
15 changed files with 2312 additions and 2180 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
liberapay: Elara6331

View File

@ -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

View File

@ -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,58 @@ 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()
anyType := false
if returnName == "any" {
anyType = true
}
// 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 = ""
}
if anyType {
returnName = "map[string]any"
}
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 +129,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 +150,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 +167,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 +185,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 +197,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 +211,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 +221,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 {

View File

@ -22,55 +22,80 @@ 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) {
if r.ReturnName != "" {
g.Op("*").Qual("go.elara.ws/go-lemmy/types", r.ReturnName)
).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 == "map[string]any" {
g.Map(jen.String()).Any()
} else if r.ReturnName != "" {
g.Op("*").Id(r.ReturnName)
}
g.Error()
}).BlockFunc(func(g *jen.Group) {
data := jen.Id("data")
// If there are no parameters, set the data to nil
if r.ParamsName == "" {
data = jen.Nil()
}
returnName := r.ReturnName
if returnName == "" {
returnName = "EmptyResponse"
returnName = "emptyResponse"
}
g.Id("resData").Op(":=").Op("&").Qual("go.elara.ws/go-lemmy/types", returnName).Block()
if returnName == "map[string]any" {
g.Id("resData").Op(":=").Map(jen.String()).Any().Block()
} else {
g.Id("resData").Op(":=").Op("&").Id(returnName).Block()
}
var funcName string
switch r.Method {
case "GET":
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"),
)
g.List(jen.Id("res"), jen.Err()).Op(":=").Id("c").Dot(funcName).ParamsFunc(func(g *jen.Group) {
g.Id("ctx")
g.Lit(r.Method)
g.Lit(r.Path)
g.Add(data)
if returnName == "map[string]any" {
g.Op("&").Id("resData")
} else {
g.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"))
if r.ReturnName == "map[string]any" {
g.Err().Op("=").Id("resError").Params(jen.Id("res"), jen.Id("NewOptionalNil[string]").Params())
} else {
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())

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
View 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
View 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
View File

@ -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()
}

View File

@ -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"

File diff suppressed because it is too large Load Diff

1552
types.gen.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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"`
}