Compare commits
No commits in common. "master" and "v0.18.3" have entirely different histories.
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +0,0 @@
|
|||||||
liberapay: Elara6331
|
|
@ -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:
|
||||||
|
|
||||||
Examples can be found in the [examples](examples) directory.
|
- HTTP: [examples/http](examples/http)
|
||||||
|
|
||||||
### How to generate
|
### How to generate
|
||||||
|
|
||||||
|
@ -14,7 +14,9 @@ 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 {
|
||||||
@ -34,7 +36,6 @@ 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 {
|
||||||
@ -44,20 +45,13 @@ func New(path string) (*Extractor, error) {
|
|||||||
return &Extractor{gjson.ParseBytes(data)}, nil
|
return &Extractor{gjson.ParseBytes(data)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract reads the JSON document and extracts all the routes and structs from it.
|
func (e *Extractor) Routes() []Route {
|
||||||
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
|
||||||
@ -69,58 +63,37 @@ func (e *Extractor) Extract() ([]Route, []Struct) {
|
|||||||
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()
|
||||||
|
|
||||||
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{
|
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, getStructSlice(structs)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Extractor) getStructs(ids []int64, structs map[int64]Struct) {
|
func (e *Extractor) getStructs(ids []int64, structs map[int64]Struct) {
|
||||||
@ -129,7 +102,6 @@ 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
|
||||||
@ -150,14 +122,12 @@ 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))
|
||||||
@ -167,8 +137,6 @@ 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
|
||||||
@ -185,11 +153,8 @@ 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 {
|
||||||
@ -197,11 +162,8 @@ 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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,8 +173,6 @@ 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, " ")
|
||||||
@ -221,8 +181,7 @@ func parseHTTPInfo(httpInfo string) (method, path string) {
|
|||||||
return method, path
|
return method, path
|
||||||
}
|
}
|
||||||
|
|
||||||
// getStructSlice returns all the structs in a map
|
func getKeys(m map[int64]Struct) []Struct {
|
||||||
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,80 +22,55 @@ 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)).ParamsFunc(func(g *jen.Group) {
|
).Id(transformName(r.Name)).Params(
|
||||||
g.Id("ctx").Qual("context", "Context")
|
jen.Id("ctx").Qual("context", "Context"),
|
||||||
if r.ParamsName != "" {
|
jen.Id("data").Qual("go.elara.ws/go-lemmy/types", 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)
|
||||||
}
|
}
|
||||||
}).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()
|
g.Error()
|
||||||
}).BlockFunc(func(g *jen.Group) {
|
}).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
|
returnName := r.ReturnName
|
||||||
if returnName == "" {
|
if returnName == "" {
|
||||||
returnName = "emptyResponse"
|
returnName = "EmptyResponse"
|
||||||
}
|
}
|
||||||
|
|
||||||
if returnName == "map[string]any" {
|
g.Id("resData").Op(":=").Op("&").Qual("go.elara.ws/go-lemmy/types", returnName).Block()
|
||||||
g.Id("resData").Op(":=").Map(jen.String()).Any().Block()
|
|
||||||
} else {
|
|
||||||
g.Id("resData").Op(":=").Op("&").Id(returnName).Block()
|
|
||||||
}
|
|
||||||
|
|
||||||
funcName := "req"
|
var funcName string
|
||||||
if r.Method == "GET" {
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
funcName = "getReq"
|
funcName = "getReq"
|
||||||
|
default:
|
||||||
|
funcName = "req"
|
||||||
}
|
}
|
||||||
|
|
||||||
g.List(jen.Id("res"), jen.Err()).Op(":=").Id("c").Dot(funcName).ParamsFunc(func(g *jen.Group) {
|
g.List(jen.Id("res"), jen.Err()).Op(":=").Id("c").Dot(funcName).Params(
|
||||||
g.Id("ctx")
|
jen.Id("ctx"), jen.Lit(r.Method), jen.Lit(r.Path), jen.Id("data"), jen.Op("&").Id("resData"),
|
||||||
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) {
|
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 r.ReturnName == "map[string]any" {
|
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("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) {
|
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)).Add(getType(field)).Tag(map[string]string{
|
g.Id(transformFieldName(field.Name)).Id(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("Error").Id("Optional").Types(jen.String()).Tag(map[string]string{"json": "error"})
|
g.Id("LemmyResponse")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -51,19 +51,7 @@ func (s *StructGenerator) Generate(items []extractor.Struct) error {
|
|||||||
return f.Render(s.w)
|
return f.Render(s.w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getType(f extractor.Field) jen.Code {
|
func getType(f extractor.Field) string {
|
||||||
// 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
|
||||||
@ -71,13 +59,20 @@ func getType(f extractor.Field) jen.Code {
|
|||||||
if f.IsOptional {
|
if f.IsOptional {
|
||||||
t = "Optional[" + t + "]"
|
t = "Optional[" + t + "]"
|
||||||
}
|
}
|
||||||
return jen.Id(t)
|
return 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 "int64"
|
return "float64"
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return "bool"
|
return "bool"
|
||||||
default:
|
default:
|
||||||
@ -94,11 +89,6 @@ 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,15 +25,21 @@ func main() {
|
|||||||
log.Fatal("Error creating extractor").Err(err).Send()
|
log.Fatal("Error creating extractor").Err(err).Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
routes, structs := e.Extract()
|
routes := e.Routes()
|
||||||
|
structs := e.Structs(routes)
|
||||||
|
|
||||||
otf, err := os.Create(filepath.Join(*outDir, "types.gen.go"))
|
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"))
|
||||||
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, "lemmy").Generate(structs)
|
err = generator.NewStruct(otf, "types").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()
|
||||||
}
|
}
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
@ -4,17 +4,18 @@ 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://lemmy.ml")
|
c, err := lemmy.New("https://lemmygrad.ml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.ClientLogin(ctx, lemmy.Login{
|
err = c.ClientLogin(ctx, types.Login{
|
||||||
UsernameOrEmail: "user@example.com",
|
UsernameOrEmail: "user@example.com",
|
||||||
Password: `TestPwd`,
|
Password: `TestPwd`,
|
||||||
})
|
})
|
||||||
@ -22,8 +23,8 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.SaveUserSettings(ctx, lemmy.SaveUserSettings{
|
_, err = c.SaveUserSettings(ctx, types.SaveUserSettings{
|
||||||
BotAccount: lemmy.NewOptional(true),
|
BotAccount: types.NewOptional(true),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
@ -1,43 +0,0 @@
|
|||||||
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,18 +4,15 @@ 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
|
||||||
@ -35,30 +32,27 @@ 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 calling the login endpoint, and
|
// ClientLogin logs in to Lemmy by sending an HTTP request to the
|
||||||
// stores the returned token in the Token field for use in future requests.
|
// login endpoint. It stores the returned token in the client
|
||||||
//
|
// for future use.
|
||||||
// The Token field can be set manually if you'd like to persist the
|
func (c *Client) ClientLogin(ctx context.Context, l types.Login) error {
|
||||||
// token somewhere.
|
lr, err := c.Login(ctx, l)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
token, ok := lr.JWT.Value()
|
c.Token = lr.JWT.MustValue()
|
||||||
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)
|
||||||
@ -80,10 +74,6 @@ 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
|
||||||
@ -101,17 +91,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's separate from req() because it uses query
|
// It is 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)
|
||||||
if data != nil {
|
vals, err := query.Values(data)
|
||||||
vals, err := query.Values(data)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
getURL.RawQuery = vals.Encode()
|
|
||||||
}
|
}
|
||||||
|
getURL.RawQuery = vals.Encode()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(
|
req, err := http.NewRequestWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
@ -123,10 +113,6 @@ 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
|
||||||
@ -143,39 +129,50 @@ func (c *Client) getReq(ctx context.Context, method string, path string, data, r
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error represents an error returned by the Lemmy API
|
// resError returns an error if the given response is an error
|
||||||
type Error struct {
|
func resError(res *http.Response, lr types.LemmyResponse) error {
|
||||||
ErrStr string
|
if lr.Error.IsValid() {
|
||||||
Code int
|
return types.LemmyError{
|
||||||
}
|
|
||||||
|
|
||||||
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: errstr,
|
ErrStr: lr.Error.MustValue(),
|
||||||
}
|
}
|
||||||
} else if res.StatusCode != http.StatusOK {
|
} else if res.StatusCode != http.StatusOK {
|
||||||
return Error{
|
return types.HTTPError{
|
||||||
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()
|
||||||
|
}
|
||||||
|
790
routes.gen.go
790
routes.gen.go
File diff suppressed because it is too large
Load Diff
1552
types.gen.go
1552
types.gen.go
File diff suppressed because it is too large
Load Diff
@ -1,53 +1,64 @@
|
|||||||
package lemmy
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Optional represents an optional value
|
var ErrOptionalEmpty = errors.New("optional value is empty")
|
||||||
|
|
||||||
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}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOptionalNil creates a new nil optional value
|
func NewOptionalPtr[T any](v *T) Optional[T] {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNil sets the optional value to nil
|
func (o Optional[T]) SetPtr(v *T) Optional[T] {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Value returns the value in the optional.
|
func (o Optional[T]) MustValue() T {
|
||||||
func (o Optional[T]) Value() (T, bool) {
|
if o.value == nil {
|
||||||
if o.value != nil {
|
panic("optional value is nil")
|
||||||
return *o.value, true
|
|
||||||
}
|
}
|
||||||
return *new(T), false
|
return *o.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Optional[T]) Value() (T, error) {
|
||||||
|
if o.value != nil {
|
||||||
|
return *o.value, ErrOptionalEmpty
|
||||||
|
}
|
||||||
|
return *new(T), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@ -55,8 +66,7 @@ func (o Optional[T]) ValueOr(fallback T) T {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValueOrZero returns the value inside the optional if it exists, or else it returns the zero value of T
|
func (o Optional[T]) ValueOrEmpty() T {
|
||||||
func (o Optional[T]) ValueOrZero() T {
|
|
||||||
if o.value != nil {
|
if o.value != nil {
|
||||||
return *o.value
|
return *o.value
|
||||||
}
|
}
|
||||||
@ -64,12 +74,10 @@ func (o Optional[T]) ValueOrZero() 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
|
||||||
@ -80,7 +88,6 @@ 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>" {
|
||||||
@ -89,7 +96,6 @@ 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>"
|
||||||
@ -97,7 +103,6 @@ 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"
|
1596
types/types.gen.go
Normal file
1596
types/types.gen.go
Normal file
File diff suppressed because it is too large
Load Diff
67
types/types.go
Normal file
67
types/types.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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"`
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user