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).
|
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
|
### How to generate
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ type Route struct {
|
|||||||
Method string
|
Method string
|
||||||
Path string
|
Path string
|
||||||
ParamsName string
|
ParamsName string
|
||||||
ParamsID int64
|
|
||||||
ReturnName string
|
ReturnName string
|
||||||
ReturnID int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Struct struct {
|
type Struct struct {
|
||||||
@@ -36,6 +34,7 @@ type Extractor struct {
|
|||||||
root gjson.Result
|
root gjson.Result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New parses the file at path and returns an extractor with its contents.
|
||||||
func New(path string) (*Extractor, error) {
|
func New(path string) (*Extractor, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -45,13 +44,20 @@ func New(path string) (*Extractor, error) {
|
|||||||
return &Extractor{gjson.ParseBytes(data)}, nil
|
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
|
var out []Route
|
||||||
|
|
||||||
|
// Get all the routes in the JSON document
|
||||||
routes := e.root.Get("children.#.children.#(kind==2048)#|@flatten")
|
routes := e.root.Get("children.#.children.#(kind==2048)#|@flatten")
|
||||||
|
|
||||||
for _, route := range routes.Array() {
|
for _, route := range routes.Array() {
|
||||||
name := route.Get("name").String()
|
name := route.Get("name").String()
|
||||||
signature := route.Get(`signatures.0`)
|
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()
|
httpInfo := signature.Get(`comment.summary.#(kind=="code").text`).String()
|
||||||
if !strings.HasPrefix(httpInfo, "`HTTP") {
|
if !strings.HasPrefix(httpInfo, "`HTTP") {
|
||||||
continue
|
continue
|
||||||
@@ -63,37 +69,49 @@ func (e *Extractor) Routes() []Route {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the ID and name of the type this function accepts
|
||||||
paramsID := signature.Get("parameters.0.type.target").Int()
|
paramsID := signature.Get("parameters.0.type.target").Int()
|
||||||
paramsName := signature.Get("parameters.0.type.name").String()
|
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()
|
returnID := signature.Get("type.typeArguments.0.target").Int()
|
||||||
returnName := signature.Get("type.typeArguments.0.name").String()
|
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{
|
out = append(out, Route{
|
||||||
Name: name,
|
Name: name,
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
Method: method,
|
Method: method,
|
||||||
Path: path,
|
Path: path,
|
||||||
ParamsName: paramsName,
|
ParamsName: paramsName,
|
||||||
ParamsID: paramsID,
|
|
||||||
ReturnName: returnName,
|
ReturnName: returnName,
|
||||||
ReturnID: returnID,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return out
|
return out, getStructSlice(structs)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Extractor) getStructs(ids []int64, structs map[int64]Struct) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the struct with the given ID from the JSON document
|
||||||
jstruct := e.root.Get(fmt.Sprintf("children.#(id==%d)", id))
|
jstruct := e.root.Get(fmt.Sprintf("children.#(id==%d)", id))
|
||||||
if !jstruct.Exists() {
|
if !jstruct.Exists() {
|
||||||
continue
|
continue
|
||||||
@@ -122,12 +141,14 @@ func (e *Extractor) getStructs(ids []int64, structs map[int64]Struct) {
|
|||||||
Fields: fields,
|
Fields: fields,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursively get any structs referenced by this one
|
||||||
e.getStructs(newIDs, structs)
|
e.getStructs(newIDs, structs)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unionNames gets all the names of union type members
|
||||||
func (e *Extractor) unionNames(jstruct gjson.Result) []string {
|
func (e *Extractor) unionNames(jstruct gjson.Result) []string {
|
||||||
jnames := jstruct.Get("type.types").Array()
|
jnames := jstruct.Get("type.types").Array()
|
||||||
out := make([]string, len(jnames))
|
out := make([]string, len(jnames))
|
||||||
@@ -137,6 +158,8 @@ func (e *Extractor) unionNames(jstruct gjson.Result) []string {
|
|||||||
return out
|
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) {
|
func (e *Extractor) fields(jstruct gjson.Result) ([]Field, []int64) {
|
||||||
var fields []Field
|
var fields []Field
|
||||||
var ids []int64
|
var ids []int64
|
||||||
@@ -153,8 +176,11 @@ func (e *Extractor) fields(jstruct gjson.Result) ([]Field, []int64) {
|
|||||||
|
|
||||||
switch jfield.Get("type.elementType.type").String() {
|
switch jfield.Get("type.elementType.type").String() {
|
||||||
case "reference":
|
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())
|
ids = append(ids, jfield.Get("type.elementType.target").Int())
|
||||||
case "union":
|
case "union":
|
||||||
|
// Convert unions to strings
|
||||||
field.Type = "string"
|
field.Type = "string"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -162,8 +188,11 @@ func (e *Extractor) fields(jstruct gjson.Result) ([]Field, []int64) {
|
|||||||
|
|
||||||
switch jfield.Get("type.type").String() {
|
switch jfield.Get("type.type").String() {
|
||||||
case "reference":
|
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())
|
ids = append(ids, jfield.Get("type.target").Int())
|
||||||
case "union":
|
case "union":
|
||||||
|
// Convert unions to strings
|
||||||
field.Type = "string"
|
field.Type = "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,6 +202,8 @@ func (e *Extractor) fields(jstruct gjson.Result) ([]Field, []int64) {
|
|||||||
return fields, ids
|
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) {
|
func parseHTTPInfo(httpInfo string) (method, path string) {
|
||||||
httpInfo = strings.Trim(httpInfo, "`")
|
httpInfo = strings.Trim(httpInfo, "`")
|
||||||
method, path, _ = strings.Cut(httpInfo, " ")
|
method, path, _ = strings.Cut(httpInfo, " ")
|
||||||
@@ -181,7 +212,8 @@ func parseHTTPInfo(httpInfo string) (method, path string) {
|
|||||||
return method, path
|
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))
|
out := make([]Struct, len(m))
|
||||||
i := 0
|
i := 0
|
||||||
for _, s := range m {
|
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.")
|
f.HeaderComment("Code generated by go.elara.ws/go-lemmy/cmd/gen (routes generator). DO NOT EDIT.")
|
||||||
|
|
||||||
for _, r := range routes {
|
for _, r := range routes {
|
||||||
|
|
||||||
f.Comment(r.Summary)
|
f.Comment(r.Summary)
|
||||||
f.Func().Params(
|
f.Func().Params(
|
||||||
jen.Id("c").Id("*Client"),
|
jen.Id("c").Id("*Client"),
|
||||||
).Id(transformName(r.Name)).Params(
|
).Id(transformName(r.Name)).ParamsFunc(func(g *jen.Group) {
|
||||||
jen.Id("ctx").Qual("context", "Context"),
|
g.Id("ctx").Qual("context", "Context")
|
||||||
jen.Id("data").Qual("go.elara.ws/go-lemmy/types", r.ParamsName),
|
if r.ParamsName != "" {
|
||||||
).ParamsFunc(func(g *jen.Group) {
|
g.Id("data").Id(r.ParamsName)
|
||||||
|
}
|
||||||
|
}).ParamsFunc(func(g *jen.Group) {
|
||||||
if r.ReturnName != "" {
|
if r.ReturnName != "" {
|
||||||
g.Op("*").Qual("go.elara.ws/go-lemmy/types", r.ReturnName)
|
g.Op("*").Id(r.ReturnName)
|
||||||
}
|
}
|
||||||
g.Error()
|
g.Error()
|
||||||
}).BlockFunc(func(g *jen.Group) {
|
}).BlockFunc(func(g *jen.Group) {
|
||||||
returnName := r.ReturnName
|
data := jen.Id("data")
|
||||||
if returnName == "" {
|
// If there are no parameters, set the data to nil
|
||||||
returnName = "EmptyResponse"
|
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
|
g.Id("resData").Op(":=").Op("&").Id(returnName).Block()
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
funcName := "req"
|
||||||
|
if r.Method == "GET" {
|
||||||
funcName = "getReq"
|
funcName = "getReq"
|
||||||
default:
|
|
||||||
funcName = "req"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g.List(jen.Id("res"), jen.Err()).Op(":=").Id("c").Dot(funcName).Params(
|
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) {
|
g.If(jen.Err().Op("!=").Nil()).BlockFunc(func(g *jen.Group) {
|
||||||
if returnName == "EmptyResponse" {
|
if returnName == "emptyResponse" {
|
||||||
g.Return(jen.Err())
|
g.Return(jen.Err())
|
||||||
} else {
|
} else {
|
||||||
g.Return(jen.Nil(), jen.Err())
|
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) {
|
g.If(jen.Err().Op("!=").Nil()).BlockFunc(func(g *jen.Group) {
|
||||||
if returnName == "EmptyResponse" {
|
if returnName == "emptyResponse" {
|
||||||
g.Return(jen.Err())
|
g.Return(jen.Err())
|
||||||
} else {
|
} else {
|
||||||
g.Return(jen.Nil(), jen.Err())
|
g.Return(jen.Nil(), jen.Err())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if returnName == "EmptyResponse" {
|
if returnName == "emptyResponse" {
|
||||||
g.Return(jen.Nil())
|
g.Return(jen.Nil())
|
||||||
} else {
|
} else {
|
||||||
g.Return(jen.Id("resData"), jen.Nil())
|
g.Return(jen.Id("resData"), jen.Nil())
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ func (s *StructGenerator) Generate(items []extractor.Struct) error {
|
|||||||
} else {
|
} else {
|
||||||
f.Type().Id(item.Name).StructFunc(func(g *jen.Group) {
|
f.Type().Id(item.Name).StructFunc(func(g *jen.Group) {
|
||||||
for _, field := range item.Fields {
|
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,
|
"json": field.Name,
|
||||||
"url": field.Name + ",omitempty",
|
"url": field.Name + ",omitempty",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasSuffix(item.Name, "Response") {
|
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)
|
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)
|
t := transformType(f.Name, f.Type)
|
||||||
if f.IsArray {
|
if f.IsArray {
|
||||||
t = "[]" + t
|
t = "[]" + t
|
||||||
@@ -59,20 +71,13 @@ func getType(f extractor.Field) string {
|
|||||||
if f.IsOptional {
|
if f.IsOptional {
|
||||||
t = "Optional[" + t + "]"
|
t = "Optional[" + t + "]"
|
||||||
}
|
}
|
||||||
return t
|
return jen.Id(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformType(name, t string) string {
|
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 {
|
switch t {
|
||||||
case "number":
|
case "number":
|
||||||
return "float64"
|
return "int64"
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return "bool"
|
return "bool"
|
||||||
default:
|
default:
|
||||||
@@ -89,6 +94,11 @@ func transformFieldName(s string) string {
|
|||||||
"Jwt", "JWT",
|
"Jwt", "JWT",
|
||||||
"Crud", "CRUD",
|
"Crud", "CRUD",
|
||||||
"Pm", "PM",
|
"Pm", "PM",
|
||||||
|
"Totp", "TOTP",
|
||||||
|
"2fa", "2FA",
|
||||||
|
"Png", "PNG",
|
||||||
|
"Uuid", "UUID",
|
||||||
|
"Wav", "WAV",
|
||||||
).Replace(s)
|
).Replace(s)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,21 +25,15 @@ func main() {
|
|||||||
log.Fatal("Error creating extractor").Err(err).Send()
|
log.Fatal("Error creating extractor").Err(err).Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
routes := e.Routes()
|
routes, structs := e.Extract()
|
||||||
structs := e.Structs(routes)
|
|
||||||
|
|
||||||
err = os.MkdirAll(filepath.Join(*outDir, "types"), 0o755)
|
otf, err := os.Create(filepath.Join(*outDir, "types.gen.go"))
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Error creating types directory").Err(err).Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
otf, err := os.Create(filepath.Join(*outDir, "types/types.gen.go"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error creating types output file").Err(err).Send()
|
log.Fatal("Error creating types output file").Err(err).Send()
|
||||||
}
|
}
|
||||||
defer otf.Close()
|
defer otf.Close()
|
||||||
|
|
||||||
err = generator.NewStruct(otf, "types").Generate(structs)
|
err = generator.NewStruct(otf, "lemmy").Generate(structs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error generating output routes file").Err(err).Send()
|
log.Fatal("Error generating output routes file").Err(err).Send()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.elara.ws/go-lemmy"
|
"go.elara.ws/go-lemmy"
|
||||||
"go.elara.ws/go-lemmy/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
c, err := lemmy.New("https://lemmygrad.ml")
|
c, err := lemmy.New("https://lemmy.ml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.ClientLogin(ctx, types.Login{
|
err = c.ClientLogin(ctx, lemmy.Login{
|
||||||
UsernameOrEmail: "user@example.com",
|
UsernameOrEmail: "user@example.com",
|
||||||
Password: `TestPwd`,
|
Password: `TestPwd`,
|
||||||
})
|
})
|
||||||
@@ -23,8 +22,8 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.SaveUserSettings(ctx, types.SaveUserSettings{
|
_, err = c.SaveUserSettings(ctx, lemmy.SaveUserSettings{
|
||||||
BotAccount: types.NewOptional(true),
|
BotAccount: lemmy.NewOptional(true),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/google/go-querystring/query"
|
"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
|
// Client is a client for Lemmy's HTTP API
|
||||||
type Client struct {
|
type Client struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
@@ -32,27 +35,30 @@ func NewWithClient(baseURL string, client *http.Client) (*Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
u = u.JoinPath("/api/v3")
|
u = u.JoinPath("/api/v3")
|
||||||
|
|
||||||
return &Client{baseURL: u, client: client}, nil
|
return &Client{baseURL: u, client: client}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientLogin logs in to Lemmy by sending an HTTP request to the
|
// ClientLogin logs in to Lemmy by calling the login endpoint, and
|
||||||
// login endpoint. It stores the returned token in the client
|
// stores the returned token in the Token field for use in future requests.
|
||||||
// for future use.
|
//
|
||||||
func (c *Client) ClientLogin(ctx context.Context, l types.Login) error {
|
// The Token field can be set manually if you'd like to persist the
|
||||||
lr, err := c.Login(ctx, l)
|
// token somewhere.
|
||||||
|
func (c *Client) ClientLogin(ctx context.Context, data Login) error {
|
||||||
|
lr, err := c.Login(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Token = lr.JWT.MustValue()
|
token, ok := lr.JWT.Value()
|
||||||
|
if !ok || token == "" {
|
||||||
|
return ErrNoToken
|
||||||
|
}
|
||||||
|
c.Token = token
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// req makes a request to the server
|
// req makes a request to the server
|
||||||
func (c *Client) req(ctx context.Context, method string, path string, data, resp any) (*http.Response, error) {
|
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
|
var r io.Reader
|
||||||
if data != nil {
|
if data != nil {
|
||||||
jsonData, err := json.Marshal(data)
|
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")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if c.Token != "" {
|
||||||
|
req.Header.Add("Authorization", "Bearer "+c.Token)
|
||||||
|
}
|
||||||
|
|
||||||
res, err := c.client.Do(req)
|
res, err := c.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// 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.
|
// parameters rather than a JSON request body.
|
||||||
func (c *Client) getReq(ctx context.Context, method string, path string, data, resp any) (*http.Response, error) {
|
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)
|
getURL := c.baseURL.JoinPath(path)
|
||||||
vals, err := query.Values(data)
|
if data != nil {
|
||||||
if err != nil {
|
vals, err := query.Values(data)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
getURL.RawQuery = vals.Encode()
|
||||||
}
|
}
|
||||||
getURL.RawQuery = vals.Encode()
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(
|
req, err := http.NewRequestWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -113,6 +123,10 @@ func (c *Client) getReq(ctx context.Context, method string, path string, data, r
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Token != "" {
|
||||||
|
req.Header.Add("Authorization", "Bearer "+c.Token)
|
||||||
|
}
|
||||||
|
|
||||||
res, err := c.client.Do(req)
|
res, err := c.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -129,50 +143,39 @@ func (c *Client) getReq(ctx context.Context, method string, path string, data, r
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resError returns an error if the given response is an error
|
// Error represents an error returned by the Lemmy API
|
||||||
func resError(res *http.Response, lr types.LemmyResponse) error {
|
type Error struct {
|
||||||
if lr.Error.IsValid() {
|
ErrStr string
|
||||||
return types.LemmyError{
|
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,
|
Code: res.StatusCode,
|
||||||
ErrStr: lr.Error.MustValue(),
|
ErrStr: errstr,
|
||||||
}
|
}
|
||||||
} else if res.StatusCode != http.StatusOK {
|
} else if res.StatusCode != http.StatusOK {
|
||||||
return types.HTTPError{
|
return Error{
|
||||||
Code: res.StatusCode,
|
Code: res.StatusCode,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrOptionalEmpty = errors.New("optional value is empty")
|
// Optional represents an optional value
|
||||||
|
|
||||||
type Optional[T any] struct {
|
type Optional[T any] struct {
|
||||||
value *T
|
value *T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewOptional creates an optional with value v
|
||||||
func NewOptional[T any](v T) Optional[T] {
|
func NewOptional[T any](v T) Optional[T] {
|
||||||
return Optional[T]{value: &v}
|
return Optional[T]{value: &v}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOptionalPtr[T any](v *T) Optional[T] {
|
// NewOptionalNil creates a new nil optional value
|
||||||
return Optional[T]{value: v}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOptionalNil[T any]() Optional[T] {
|
func NewOptionalNil[T any]() Optional[T] {
|
||||||
return Optional[T]{}
|
return Optional[T]{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set sets the value of the optional
|
||||||
func (o Optional[T]) Set(v T) Optional[T] {
|
func (o Optional[T]) Set(v T) Optional[T] {
|
||||||
o.value = &v
|
o.value = &v
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o Optional[T]) SetPtr(v *T) Optional[T] {
|
// SetNil sets the optional value to nil
|
||||||
o.value = v
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Optional[T]) SetNil() Optional[T] {
|
func (o Optional[T]) SetNil() Optional[T] {
|
||||||
o.value = nil
|
o.value = nil
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValid returns true if the value of the optional is not nil
|
||||||
func (o Optional[T]) IsValid() bool {
|
func (o Optional[T]) IsValid() bool {
|
||||||
return o.value != nil
|
return o.value != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o Optional[T]) MustValue() T {
|
// Value returns the value in the optional.
|
||||||
if o.value == nil {
|
func (o Optional[T]) Value() (T, bool) {
|
||||||
panic("optional value is nil")
|
|
||||||
}
|
|
||||||
return *o.value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Optional[T]) Value() (T, error) {
|
|
||||||
if o.value != nil {
|
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 {
|
func (o Optional[T]) ValueOr(fallback T) T {
|
||||||
if o.value != nil {
|
if o.value != nil {
|
||||||
return *o.value
|
return *o.value
|
||||||
@@ -66,7 +55,8 @@ func (o Optional[T]) ValueOr(fallback T) T {
|
|||||||
return fallback
|
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 {
|
if o.value != nil {
|
||||||
return *o.value
|
return *o.value
|
||||||
}
|
}
|
||||||
@@ -74,10 +64,12 @@ func (o Optional[T]) ValueOrEmpty() T {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON encodes the optional value as JSON
|
||||||
func (o Optional[T]) MarshalJSON() ([]byte, error) {
|
func (o Optional[T]) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(o.value)
|
return json.Marshal(o.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON decodes JSON into the optional value
|
||||||
func (o *Optional[T]) UnmarshalJSON(b []byte) error {
|
func (o *Optional[T]) UnmarshalJSON(b []byte) error {
|
||||||
if bytes.Equal(b, []byte("null")) {
|
if bytes.Equal(b, []byte("null")) {
|
||||||
o.value = nil
|
o.value = nil
|
||||||
@@ -88,6 +80,7 @@ func (o *Optional[T]) UnmarshalJSON(b []byte) error {
|
|||||||
return json.Unmarshal(b, o.value)
|
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 {
|
func (o Optional[T]) EncodeValues(key string, v *url.Values) error {
|
||||||
s := o.String()
|
s := o.String()
|
||||||
if s != "<nil>" {
|
if s != "<nil>" {
|
||||||
@@ -96,6 +89,7 @@ func (o Optional[T]) EncodeValues(key string, v *url.Values) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the optional value
|
||||||
func (o Optional[T]) String() string {
|
func (o Optional[T]) String() string {
|
||||||
if o.value == nil {
|
if o.value == nil {
|
||||||
return "<nil>"
|
return "<nil>"
|
||||||
@@ -103,6 +97,7 @@ func (o Optional[T]) String() string {
|
|||||||
return fmt.Sprint(*o.value)
|
return fmt.Sprint(*o.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GoString returns the Go representation of the optional value
|
||||||
func (o Optional[T]) GoString() string {
|
func (o Optional[T]) GoString() string {
|
||||||
if o.value == nil {
|
if o.value == nil {
|
||||||
return "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