Compare commits

...

22 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
14 changed files with 2114 additions and 1984 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,17 +71,10 @@ 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 "int64"
@ -91,6 +96,9 @@ func transformFieldName(s string) string {
"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

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