Skip to content

Commit f7672c9

Browse files
committed
Update command functionalities
1 parent bd16575 commit f7672c9

15 files changed

Lines changed: 515 additions & 48 deletions
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package commands
2+
3+
import (
4+
"archive/zip"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"os"
9+
"path"
10+
"path/filepath"
11+
"strings"
12+
)
13+
14+
const packageDescriptionFileName = "desc.json"
15+
16+
type contributionPaths struct {
17+
Inferences map[string]string
18+
Singers map[string]string
19+
}
20+
21+
type packageContributionPathsDescription struct {
22+
Contributes struct {
23+
Inferences []string `json:"inferences"`
24+
Singers []string `json:"singers"`
25+
} `json:"contributes"`
26+
}
27+
28+
type moduleIDDescription struct {
29+
ID string `json:"id"`
30+
}
31+
32+
func readArchiveContributionPaths(reader packageFileReader) (contributionPaths, error) {
33+
archive, err := zip.NewReader(reader, reader.Size())
34+
if err != nil {
35+
return contributionPaths{}, fmt.Errorf("open package zip: %w", err)
36+
}
37+
38+
readFile := func(filePath string) ([]byte, error) {
39+
normalizedPath := cleanPackageRelativePath(filePath)
40+
for _, file := range archive.File {
41+
if cleanPackageRelativePath(file.Name) != normalizedPath {
42+
continue
43+
}
44+
handle, err := file.Open()
45+
if err != nil {
46+
return nil, fmt.Errorf("open zip entry %q: %w", normalizedPath, err)
47+
}
48+
defer handle.Close()
49+
data, err := io.ReadAll(handle)
50+
if err != nil {
51+
return nil, fmt.Errorf("read zip entry %q: %w", normalizedPath, err)
52+
}
53+
return data, nil
54+
}
55+
return nil, fmt.Errorf("zip entry %q not found", normalizedPath)
56+
}
57+
58+
return readContributionPaths(readFile, cleanPackageRelativePath)
59+
}
60+
61+
func readInstalledContributionPaths(packageDir string) (contributionPaths, error) {
62+
descriptionPath := filepath.Join(packageDir, packageDescriptionFileName)
63+
descriptionData, err := os.ReadFile(descriptionPath)
64+
if err != nil {
65+
if isMissingPathError(err) {
66+
return newContributionPaths(), nil
67+
}
68+
return contributionPaths{}, fmt.Errorf("read %s: %w", packageDescriptionFileName, err)
69+
}
70+
71+
readFile := func(filePath string) ([]byte, error) {
72+
if cleanPackageRelativePath(filePath) == packageDescriptionFileName {
73+
return descriptionData, nil
74+
}
75+
absolutePath := filepath.Join(packageDir, filepath.FromSlash(cleanPackageRelativePath(filePath)))
76+
data, err := os.ReadFile(absolutePath)
77+
if err != nil {
78+
return nil, err
79+
}
80+
return data, nil
81+
}
82+
83+
paths, err := readContributionPaths(readFile, func(filePath string) string {
84+
relativePath := cleanPackageRelativePath(filePath)
85+
if relativePath == "" {
86+
return ""
87+
}
88+
return filepath.Join(packageDir, filepath.FromSlash(relativePath))
89+
})
90+
return paths, err
91+
}
92+
93+
func readContributionPaths(readFile func(string) ([]byte, error), displayPath func(string) string) (contributionPaths, error) {
94+
descriptionData, err := readFile(packageDescriptionFileName)
95+
if err != nil {
96+
return contributionPaths{}, fmt.Errorf("read %s: %w", packageDescriptionFileName, err)
97+
}
98+
99+
var description packageContributionPathsDescription
100+
if err := json.Unmarshal(descriptionData, &description); err != nil {
101+
return contributionPaths{}, fmt.Errorf("parse %s: %w", packageDescriptionFileName, err)
102+
}
103+
104+
paths := newContributionPaths()
105+
if err := addModuleContributionPaths(paths.Inferences, description.Contributes.Inferences, readFile, displayPath); err != nil {
106+
return contributionPaths{}, err
107+
}
108+
if err := addModuleContributionPaths(paths.Singers, description.Contributes.Singers, readFile, displayPath); err != nil {
109+
return contributionPaths{}, err
110+
}
111+
return paths, nil
112+
}
113+
114+
func addModuleContributionPaths(paths map[string]string, filePaths []string, readFile func(string) ([]byte, error), displayPath func(string) string) error {
115+
for _, filePath := range filePaths {
116+
data, err := readFile(filePath)
117+
if err != nil {
118+
return fmt.Errorf("read contribution %q: %w", filePath, err)
119+
}
120+
121+
var description moduleIDDescription
122+
if err := json.Unmarshal(data, &description); err != nil {
123+
return fmt.Errorf("parse contribution %q: %w", filePath, err)
124+
}
125+
if description.ID != "" {
126+
paths[description.ID] = displayPath(filePath)
127+
}
128+
}
129+
return nil
130+
}
131+
132+
func newContributionPaths() contributionPaths {
133+
return contributionPaths{
134+
Inferences: make(map[string]string),
135+
Singers: make(map[string]string),
136+
}
137+
}
138+
139+
func cleanPackageRelativePath(value string) string {
140+
normalized := strings.ReplaceAll(value, "\\", "/")
141+
cleaned := path.Clean(normalized)
142+
if cleaned == "." {
143+
return ""
144+
}
145+
return cleaned
146+
}
147+
148+
func isMissingPathError(err error) bool {
149+
if os.IsNotExist(err) {
150+
return true
151+
}
152+
message := strings.ToLower(err.Error())
153+
return strings.Contains(message, "cannot find the path specified") ||
154+
strings.Contains(message, "no such file or directory")
155+
}

internal/cli/commands/info.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type infoDependencyJSON struct {
4949

5050
type infoSingerJSON struct {
5151
ID string `json:"id"`
52+
Path string `json:"path"`
5253
Class string `json:"class"`
5354
Name any `json:"name,omitempty"`
5455
Avatar any `json:"avatar,omitempty"`
@@ -131,21 +132,25 @@ func ShowInfo(target string, packagesDir string, languageCode string, jsonOutput
131132
return writeInfoError(jsonOutput, out, "NOT_FOUND", err.Error(), nil, err)
132133
}
133134
filterInfoTarget(&info, resolved)
134-
packageDir, err := filepath.Abs(filepath.Join(packagesDir, info.Package.Hash))
135+
packageDir, err := filepath.Abs(installedPackageDir(packagesDir, info.Package.ID, info.Package.Version))
135136
if err != nil {
136137
return writeInfoError(jsonOutput, out, "IO_ERROR", fmt.Sprintf("resolve package installation path: %v", err), nil, err)
137138
}
138139
absolutizeInfoPaths(&info, packageDir)
140+
contributionPaths, err := readInstalledContributionPaths(packageDir)
141+
if err != nil {
142+
return writeInfoError(jsonOutput, out, "SCHEMA_ERROR", err.Error(), nil, err)
143+
}
139144

140145
if jsonOutput {
141146
return json.NewEncoder(out).Encode(infoOutput{
142147
OK: true,
143148
Command: "info",
144-
Data: buildInfoData(info, resolved.Type, languageCode, packageDir),
149+
Data: buildInfoData(info, resolved.Type, contributionPaths, languageCode, packageDir),
145150
})
146151
}
147152

148-
printInfoText(out, info, resolved.Type, languageCode, packageDir)
153+
printInfoText(out, info, resolved.Type, contributionPaths, languageCode, packageDir)
149154
return nil
150155
}
151156

@@ -572,7 +577,7 @@ func absolutizeInfoPath(value string, packageDir string) string {
572577
return filepath.Join(packageDir, value)
573578
}
574579

575-
func buildInfoData(info infoPackage, targetType packageinfo.PackageReferenceType, languageCode string, packageDir string) infoData {
580+
func buildInfoData(info infoPackage, targetType packageinfo.PackageReferenceType, contributionPaths contributionPaths, languageCode string, packageDir string) infoData {
576581
data := infoData{
577582
Type: targetTypeText(targetType),
578583
Installation: infoInstallationJSON{
@@ -606,13 +611,15 @@ func buildInfoData(info infoPackage, targetType packageinfo.PackageReferenceType
606611
for _, inference := range info.Inspection.Contributes.Inferences {
607612
data.Inferences = append(data.Inferences, inspectInferenceJSON{
608613
ID: inference.ID,
614+
Path: contributionPaths.Inferences[inference.ID],
609615
Name: multilingualJSONValue(inference.Name, languageCode),
610616
})
611617
}
612618

613619
for _, singer := range info.Inspection.Contributes.Singers {
614620
item := infoSingerJSON{
615621
ID: singer.ID,
622+
Path: contributionPaths.Singers[singer.ID],
616623
Class: singer.Class,
617624
Name: multilingualJSONValue(singer.Name, languageCode),
618625
Avatar: multilingualJSONValue(singer.Avatar, languageCode),
@@ -641,7 +648,7 @@ func buildInfoData(info infoPackage, targetType packageinfo.PackageReferenceType
641648
return data
642649
}
643650

644-
func printInfoText(out io.Writer, info infoPackage, targetType packageinfo.PackageReferenceType, languageCode string, packageDir string) {
651+
func printInfoText(out io.Writer, info infoPackage, targetType packageinfo.PackageReferenceType, contributionPaths contributionPaths, languageCode string, packageDir string) {
645652
printSectionTitle(out, "Installation")
646653
printField(out, " ", "Path", packageDir)
647654
printField(out, " ", "Hash", info.Package.Hash)
@@ -676,7 +683,7 @@ func printInfoText(out io.Writer, info infoPackage, targetType packageinfo.Packa
676683
printEmpty(out, " ")
677684
}
678685
for _, inference := range info.Inspection.Contributes.Inferences {
679-
fmt.Fprintf(out, " %s\n", inference.ID)
686+
fmt.Fprintf(out, " %s -> %s\n", inference.ID, contributionPaths.Inferences[inference.ID])
680687
printOptionalText(out, " Name", inference.Name, languageCode)
681688
}
682689
fmt.Fprintln(out)
@@ -688,7 +695,7 @@ func printInfoText(out io.Writer, info infoPackage, targetType packageinfo.Packa
688695
printEmpty(out, " ")
689696
}
690697
for _, singer := range info.Inspection.Contributes.Singers {
691-
fmt.Fprintf(out, " %s\n", singer.ID)
698+
fmt.Fprintf(out, " %s -> %s\n", singer.ID, contributionPaths.Singers[singer.ID])
692699
printOptionalText(out, " Name", singer.Name, languageCode)
693700
printField(out, " ", "Class", singer.Class)
694701
printOptionalText(out, " Avatar", singer.Avatar, languageCode)

internal/cli/commands/info_test.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package commands
33
import (
44
"bytes"
55
"encoding/json"
6+
"os"
67
"path/filepath"
78
"strings"
89
"testing"
@@ -31,7 +32,8 @@ func TestShowInfoJSONForInferenceFiltersModulesAndIncludesInstallation(t *testin
3132
if payload.Data.Type != "inference" {
3233
t.Fatalf("type = %q", payload.Data.Type)
3334
}
34-
if payload.Data.Installation.Path != filepath.Join(packagesDir, "hash-package") ||
35+
packageDir := installedPackageDir(packagesDir, "vendor/package", "1.0.0.0")
36+
if payload.Data.Installation.Path != packageDir ||
3537
payload.Data.Installation.Hash != "hash-package" ||
3638
payload.Data.Installation.InstalledAt != "1970-01-01T00:00:02.345Z" {
3739
t.Fatalf("installation = %#v", payload.Data.Installation)
@@ -41,7 +43,7 @@ func TestShowInfoJSONForInferenceFiltersModulesAndIncludesInstallation(t *testin
4143
payload.Data.Package.Name != "Package" {
4244
t.Fatalf("package = %#v", payload.Data.Package)
4345
}
44-
if payload.Data.Package.Readme != filepath.Join(packagesDir, "hash-package", "README.md") ||
46+
if payload.Data.Package.Readme != filepath.Join(packageDir, "README.md") ||
4547
payload.Data.Package.License != filepath.Join(packagesDir, "absolute-license.txt") {
4648
t.Fatalf("package paths = readme %#v license %#v", payload.Data.Package.Readme, payload.Data.Package.License)
4749
}
@@ -53,7 +55,8 @@ func TestShowInfoJSONForInferenceFiltersModulesAndIncludesInstallation(t *testin
5355
}
5456
if len(payload.Data.Inferences) != 1 ||
5557
payload.Data.Inferences[0].ID != "acoustic" ||
56-
payload.Data.Inferences[0].Name != "Acoustic" {
58+
payload.Data.Inferences[0].Name != "Acoustic" ||
59+
payload.Data.Inferences[0].Path != filepath.Join(packageDir, "acoustic.json") {
5760
t.Fatalf("inferences = %#v", payload.Data.Inferences)
5861
}
5962
if len(payload.Data.Singers) != 0 {
@@ -71,7 +74,7 @@ func TestShowInfoTextForSingerFiltersModulesAndOmitsStatuses(t *testing.T) {
7174

7275
got := output.String()
7376
if !strings.Contains(got, "Installation") ||
74-
!strings.Contains(got, "Path: "+filepath.Join(packagesDir, "hash-package")) ||
77+
!strings.Contains(got, "Path: "+installedPackageDir(packagesDir, "vendor/package", "1.0.0.0")) ||
7578
!strings.Contains(got, "Hash: hash-package") ||
7679
!strings.Contains(got, "InstalledAt: ") {
7780
t.Fatalf("output missing installation block:\n%s", got)
@@ -83,10 +86,11 @@ func TestShowInfoTextForSingerFiltersModulesAndOmitsStatuses(t *testing.T) {
8386
t.Fatalf("singer target should not print Inferences block:\n%s", got)
8487
}
8588
if !strings.Contains(got, "Singers") ||
89+
!strings.Contains(got, "singer -> "+filepath.Join(installedPackageDir(packagesDir, "vendor/package", "1.0.0.0"), "singer.json")) ||
8690
!strings.Contains(got, "Singer") ||
8791
!strings.Contains(got, "Acoustic (vendor/package@1.0.0.0:acoustic)") ||
88-
!strings.Contains(got, filepath.Join(packagesDir, "hash-package", "avatar.png")) ||
89-
!strings.Contains(got, filepath.Join(packagesDir, "hash-package", "background.png")) {
92+
!strings.Contains(got, filepath.Join(installedPackageDir(packagesDir, "vendor/package", "1.0.0.0"), "avatar.png")) ||
93+
!strings.Contains(got, filepath.Join(installedPackageDir(packagesDir, "vendor/package", "1.0.0.0"), "background.png")) {
9094
t.Fatalf("output missing singer details:\n%s", got)
9195
}
9296
if strings.Contains(got, "Ready") ||
@@ -251,5 +255,25 @@ func makeInfoTestDatabase(t *testing.T) string {
251255
t.Fatalf("create singer import: %v", err)
252256
}
253257

258+
packageDir := installedPackageDir(packagesDir, "vendor/package", "1.0.0.0")
259+
if err := os.MkdirAll(packageDir, 0o755); err != nil {
260+
t.Fatalf("create package dir: %v", err)
261+
}
262+
files := map[string]string{
263+
"desc.json": `{
264+
"contributes": {
265+
"inferences": ["acoustic.json"],
266+
"singers": ["singer.json"]
267+
}
268+
}`,
269+
"acoustic.json": `{"id": "acoustic"}`,
270+
"singer.json": `{"id": "singer"}`,
271+
}
272+
for name, body := range files {
273+
if err := os.WriteFile(filepath.Join(packageDir, name), []byte(body), 0o644); err != nil {
274+
t.Fatalf("write %s: %v", name, err)
275+
}
276+
}
277+
254278
return packagesDir
255279
}

0 commit comments

Comments
 (0)