Skip to content

Commit 6b64e9f

Browse files
committed
feat: start of modrinth commands, basic search only for now
1 parent bd6b721 commit 6b64e9f

File tree

10 files changed

+593
-32
lines changed

10 files changed

+593
-32
lines changed

cmd/mc/modrinth/modrinth.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package modrinth
2+
3+
import (
4+
"github.com/mworzala/mc/internal/pkg/cli"
5+
"github.com/spf13/cobra"
6+
)
7+
8+
func NewModrinthCmd(app *cli.App) *cobra.Command {
9+
cmd := &cobra.Command{
10+
Use: "modrinth",
11+
Short: "Query the modrinth API directly",
12+
}
13+
14+
cmd.AddCommand(newSearchCmd(app))
15+
16+
return cmd
17+
}

cmd/mc/modrinth/search.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package modrinth
2+
3+
import (
4+
"context"
5+
"os"
6+
"os/signal"
7+
"strings"
8+
9+
"github.com/mworzala/mc/internal/pkg/cli"
10+
appModel "github.com/mworzala/mc/internal/pkg/cli/model"
11+
"github.com/mworzala/mc/internal/pkg/modrinth"
12+
"github.com/mworzala/mc/internal/pkg/modrinth/facet"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
type searchOpts struct {
17+
app *cli.App
18+
19+
// Project type
20+
mod bool
21+
modPack bool
22+
resourcePack bool
23+
shader bool
24+
25+
// Sort
26+
//todo
27+
}
28+
29+
func newSearchCmd(app *cli.App) *cobra.Command {
30+
var o searchOpts
31+
32+
cmd := &cobra.Command{
33+
Use: "search",
34+
Short: "Search for projects on modrinth",
35+
Args: func(cmd *cobra.Command, args []string) error {
36+
o.app = app
37+
return o.validateArgs(cmd, args)
38+
},
39+
RunE: func(_ *cobra.Command, args []string) error {
40+
o.app = app
41+
return o.execute(args)
42+
},
43+
}
44+
45+
cmd.Flags().BoolVar(&o.mod, "mod", false, "Show only mods")
46+
cmd.Flags().BoolVar(&o.modPack, "modpack", false, "Show only modpacks")
47+
cmd.Flags().BoolVar(&o.resourcePack, "resourcepack", false, "Show only resource packs")
48+
cmd.Flags().BoolVar(&o.resourcePack, "rp", false, "Show only resource packs")
49+
cmd.Flags().BoolVar(&o.shader, "shader", false, "Show only shaders")
50+
51+
cmd.Flags().FlagUsages()
52+
53+
return cmd
54+
}
55+
56+
func (o *searchOpts) validateArgs(cmd *cobra.Command, args []string) (err error) {
57+
if err := cobra.MinimumNArgs(1)(cmd, args); err != nil {
58+
return err
59+
}
60+
61+
// todo
62+
return nil
63+
}
64+
65+
func (o *searchOpts) execute(args []string) error {
66+
// Validation function has done arg validation and option population
67+
68+
client := modrinth.NewClient(o.app.Build.Version)
69+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
70+
defer cancel()
71+
72+
query := strings.Join(args, " ")
73+
var facets facet.And
74+
if o.mod || o.modPack || o.resourcePack || o.shader {
75+
var filter facet.Or
76+
if o.mod {
77+
filter = append(filter, facet.Eq{facet.ProjectType, "mod"})
78+
}
79+
if o.modPack {
80+
filter = append(filter, facet.Eq{facet.ProjectType, "modpack"})
81+
}
82+
if o.resourcePack {
83+
filter = append(filter, facet.Eq{facet.ProjectType, "resourcepack"})
84+
}
85+
if o.shader {
86+
filter = append(filter, facet.Eq{facet.ProjectType, "shader"})
87+
}
88+
facets = append(facets, filter)
89+
}
90+
91+
res, err := client.Search(ctx, modrinth.SearchRequest{
92+
Query: query,
93+
Facets: facets,
94+
})
95+
if err != nil {
96+
return err
97+
}
98+
99+
presentable := appModel.ModrinthSearchResult(*res)
100+
return o.app.Present(&presentable)
101+
}

cmd/mc/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package mc
22

33
import (
44
"github.com/MakeNowJust/heredoc"
5+
"github.com/mworzala/mc/cmd/mc/modrinth"
56

67
"github.com/mworzala/mc/cmd/mc/profile"
78

@@ -37,6 +38,7 @@ func NewRootCmd(app *cli.App) *cobra.Command {
3738
cmd.AddCommand(profile.NewProfileCmd(app))
3839
cmd.AddCommand(newLaunchCmd(app))
3940
cmd.AddCommand(newInstallCmd(app))
41+
cmd.AddCommand(modrinth.NewModrinthCmd(app))
4042
cmd.AddCommand(newVersionCmd(app))
4143
cmd.AddCommand(newDebugCmd(app))
4244

internal/pkg/cli/model/modrinth.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package model
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/gosuri/uitable"
7+
"github.com/mworzala/mc/internal/pkg/modrinth"
8+
"github.com/mworzala/mc/internal/pkg/util"
9+
)
10+
11+
type ModrinthSearchResult modrinth.SearchResponse
12+
13+
func (result *ModrinthSearchResult) String() string {
14+
table := uitable.New()
15+
table.AddRow("ID", "TYPE", "NAME", "DOWNLOADS")
16+
for _, project := range result.Hits {
17+
table.AddRow(project.ProjectID, project.ProjectType, project.Title, util.FormatCount(project.Downloads))
18+
}
19+
res := table.String()
20+
if result.TotalHits-len(result.Hits) > 0 {
21+
res += fmt.Sprintf("\n...and %d more", result.TotalHits-len(result.Hits))
22+
}
23+
return res
24+
}

internal/pkg/modrinth/facet/facet.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package facet
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"time"
7+
)
8+
9+
func ToString(facet Root) (string, error) {
10+
if facet == nil {
11+
return "", nil
12+
}
13+
return facet.asString(false, false)
14+
}
15+
16+
type And []OrFilter
17+
18+
type Or []Filter
19+
20+
type Eq struct {
21+
Type Type
22+
Value interface{}
23+
}
24+
25+
type NEq struct {
26+
Type Type
27+
Value interface{}
28+
}
29+
30+
type Gt struct {
31+
Type Type
32+
Value interface{}
33+
}
34+
35+
type GtEq struct {
36+
Type Type
37+
Value interface{}
38+
}
39+
40+
type Lt struct {
41+
Type Type
42+
Value interface{}
43+
}
44+
45+
type LtEq struct {
46+
Type Type
47+
Value interface{}
48+
}
49+
50+
type Type string
51+
52+
const (
53+
ProjectType Type = "project_type"
54+
Categories Type = "categories"
55+
Versions Type = "versions"
56+
ClientSide Type = "client_side"
57+
ServerSide Type = "server_side"
58+
OpenSource Type = "open_source"
59+
Title Type = "title"
60+
Author Type = "author"
61+
Follows Type = "follows"
62+
ProjectID Type = "project_id"
63+
License Type = "license"
64+
Downloads Type = "downloads"
65+
Color Type = "color"
66+
CreatedTimestamp Type = "created_timestamp"
67+
ModifiedTimestamp Type = "modified_timestamp"
68+
)
69+
70+
type (
71+
anyFilter interface {
72+
asString(hasAnd, hasOr bool) (string, error)
73+
}
74+
Root interface {
75+
anyFilter
76+
facetRoot()
77+
}
78+
OrFilter interface {
79+
anyFilter
80+
orFilter()
81+
}
82+
Filter interface {
83+
anyFilter
84+
facet()
85+
}
86+
)
87+
88+
func (f And) asString(_, _ bool) (string, error) {
89+
if len(f) == 0 {
90+
return "", nil
91+
}
92+
93+
out := "["
94+
for i, child := range f {
95+
childStr, err := child.asString(true, false)
96+
if err != nil {
97+
return "", err
98+
}
99+
out += childStr
100+
if i != len(f)-1 {
101+
out += ","
102+
}
103+
}
104+
105+
return out + "]", nil
106+
}
107+
108+
func (f Or) asString(hasAnd, _ bool) (string, error) {
109+
if len(f) == 0 {
110+
return "", nil
111+
}
112+
113+
out := "["
114+
for i, child := range f {
115+
childStr, err := child.asString(true, true)
116+
if err != nil {
117+
return "", err
118+
}
119+
out += childStr
120+
if i != len(f)-1 {
121+
out += ","
122+
}
123+
}
124+
out += "]"
125+
126+
if !hasAnd {
127+
out = "[" + out + "]"
128+
}
129+
130+
return out, nil
131+
}
132+
133+
func (f Eq) asString(hasAnd, hasOr bool) (string, error) {
134+
return serializeOperation(hasAnd, hasOr, "=", f.Type, f.Value)
135+
}
136+
func (f NEq) asString(hasAnd, hasOr bool) (string, error) {
137+
return serializeOperation(hasAnd, hasOr, "!=", f.Type, f.Value)
138+
}
139+
func (f Gt) asString(hasAnd, hasOr bool) (string, error) {
140+
return serializeOperation(hasAnd, hasOr, ">", f.Type, f.Value)
141+
}
142+
func (f GtEq) asString(hasAnd, hasOr bool) (string, error) {
143+
return serializeOperation(hasAnd, hasOr, ">=", f.Type, f.Value)
144+
}
145+
func (f Lt) asString(hasAnd, hasOr bool) (string, error) {
146+
return serializeOperation(hasAnd, hasOr, "<", f.Type, f.Value)
147+
}
148+
func (f LtEq) asString(hasAnd, hasOr bool) (string, error) {
149+
return serializeOperation(hasAnd, hasOr, "<=", f.Type, f.Value)
150+
}
151+
152+
func (And) facetRoot() {}
153+
func (Or) facetRoot() {}
154+
func (Eq) facetRoot() {}
155+
func (NEq) facetRoot() {}
156+
func (Gt) facetRoot() {}
157+
func (GtEq) facetRoot() {}
158+
func (Lt) facetRoot() {}
159+
func (LtEq) facetRoot() {}
160+
161+
func (Or) orFilter() {}
162+
func (Eq) orFilter() {}
163+
func (NEq) orFilter() {}
164+
func (Gt) orFilter() {}
165+
func (GtEq) orFilter() {}
166+
func (Lt) orFilter() {}
167+
func (LtEq) orFilter() {}
168+
169+
func (Or) facet() {}
170+
func (Eq) facet() {}
171+
func (NEq) facet() {}
172+
func (Gt) facet() {}
173+
func (GtEq) facet() {}
174+
func (Lt) facet() {}
175+
func (LtEq) facet() {}
176+
177+
func serializeOperation(hasAnd, hasOr bool, op string, _type Type, value interface{}) (string, error) {
178+
valueStr, err := serializeValue(value)
179+
if err != nil {
180+
return "", err
181+
}
182+
183+
out := `"` + string(_type) + op + valueStr + `"`
184+
185+
if !hasAnd {
186+
out = "[" + out + "]"
187+
}
188+
if !hasOr {
189+
out = "[" + out + "]"
190+
}
191+
return out, nil
192+
}
193+
194+
func serializeValue(value interface{}) (string, error) {
195+
switch v := value.(type) {
196+
case bool:
197+
if v {
198+
return "true", nil
199+
} else {
200+
return "false", nil
201+
}
202+
case string:
203+
return v, nil
204+
case int:
205+
return strconv.Itoa(v), nil
206+
case int64:
207+
return strconv.FormatInt(v, 10), nil
208+
case float64:
209+
return strconv.FormatFloat(v, 'g', -1, 64), nil
210+
case time.Time:
211+
return v.Format(time.RFC3339), nil
212+
default:
213+
return "", fmt.Errorf("unexpected facet value type %T", v)
214+
}
215+
}

0 commit comments

Comments
 (0)