-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12 from web-of-things-open-source/feat/fetch
Feat/fetch
- Loading branch information
Showing
4 changed files
with
239 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
"log/slog" | ||
"os" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/web-of-things-open-source/tm-catalog-cli/internal/commands" | ||
"github.com/web-of-things-open-source/tm-catalog-cli/internal/remotes" | ||
) | ||
|
||
var fetchCmd = &cobra.Command{ | ||
Use: "fetch NAME[:SEMVER|DIGEST]", | ||
Short: "Fetches the TM by name", | ||
Long: "Fetches TM by name, optionally accepting semantic version or digest", | ||
Args: cobra.ExactArgs(1), | ||
Run: executeFetch, | ||
} | ||
|
||
func init() { | ||
rootCmd.AddCommand(fetchCmd) | ||
fetchCmd.Flags().StringP("remote", "r", "", "use named remote instead of default") | ||
} | ||
|
||
func executeFetch(cmd *cobra.Command, args []string) { | ||
log := slog.Default() | ||
|
||
remoteName := cmd.Flag("remote").Value.String() | ||
remote, err := remotes.Get(remoteName) | ||
if err != nil { | ||
log.Error(fmt.Sprintf("could not initialize a remote instance for %s. check config", remoteName), "error", err) | ||
os.Exit(1) | ||
} | ||
|
||
fn := &commands.FetchName{} | ||
err = fn.Parse(args[0]) | ||
if err != nil { | ||
fmt.Println(err.Error()) | ||
os.Exit(1) | ||
} | ||
thing, err := commands.FetchThingByName(fn, remote) | ||
if err != nil { | ||
fmt.Println(err.Error()) | ||
os.Exit(1) | ||
} | ||
fmt.Println(string(thing)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
package commands | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"log/slog" | ||
"os" | ||
"regexp" | ||
"time" | ||
|
||
"github.com/Masterminds/semver/v3" | ||
"github.com/web-of-things-open-source/tm-catalog-cli/internal/model" | ||
"github.com/web-of-things-open-source/tm-catalog-cli/internal/remotes" | ||
) | ||
|
||
type FetchName struct { | ||
Name string | ||
SemVerOrDigest string | ||
} | ||
|
||
func (fn *FetchName) Parse(fetchName string) error { | ||
pattern := `^([^:]+)(:(.+))?$` | ||
|
||
// Compile the regular expression | ||
re := regexp.MustCompile(pattern) | ||
|
||
// Find submatches in the input string | ||
matches := re.FindStringSubmatch(fetchName) | ||
|
||
// Check if there are enough submatches | ||
if len(matches) < 2 { | ||
msg := fmt.Sprintf("Invalid name format: %s - Must be NAME[:SEMVER|DIGEST]", fetchName) | ||
slog.Default().Error(msg) | ||
return fmt.Errorf(msg) | ||
} | ||
|
||
// Extract values from submatches | ||
fn.Name = matches[1] | ||
if len(matches) > 3 { | ||
fn.SemVerOrDigest = matches[3] | ||
} | ||
return nil | ||
} | ||
|
||
func FetchThingByName(fn *FetchName, remote remotes.Remote) ([]byte, error) { | ||
tocThing, err := remote.Versions(fn.Name) | ||
if err != nil { | ||
fmt.Errorf(err.Error()) | ||
os.Exit(1) | ||
} | ||
|
||
var id string | ||
var thing []byte | ||
// Just the name specified: fetch most recent | ||
if len(fn.SemVerOrDigest) == 0 { | ||
id, err = findMostRecentVersion(tocThing.Versions) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} else if fetchVersion, err := semver.NewVersion(fn.SemVerOrDigest); err == nil { | ||
id, err = findMostRecentTimeStamp(tocThing.Versions, fetchVersion) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} else { | ||
id, err = findDigest(tocThing.Versions, fn.SemVerOrDigest) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
// TODO: how to know if it is official? | ||
tmid, err := model.ParseTMID(id, false) | ||
thing, err = remote.Fetch(tmid) | ||
if err != nil { | ||
msg := fmt.Sprintf("No thing model found for %s", fn) | ||
slog.Default().Error(msg) | ||
return nil, errors.New(msg) | ||
} | ||
return thing, nil | ||
} | ||
|
||
func findMostRecentVersion(versions []model.TocVersion) (id string, err error) { | ||
log := slog.Default() | ||
if len(versions) == 0 { | ||
msg := "No versions found" | ||
log.Error(msg) | ||
return "", errors.New(msg) | ||
} | ||
|
||
latestVersion, _ := semver.NewVersion("v0.0.0") | ||
var latestTimeStamp time.Time | ||
|
||
for _, version := range versions { | ||
// TODO: use StrictNewVersion | ||
currentVersion, err := semver.NewVersion(version.Version.Model) | ||
if err != nil { | ||
log.Error(err.Error()) | ||
return "", err | ||
} | ||
if currentVersion.GreaterThan(latestVersion) { | ||
latestVersion = currentVersion | ||
latestTimeStamp, err = time.Parse(pseudoVersionTimestampFormat, version.TimeStamp) | ||
id = version.ID | ||
continue | ||
} | ||
if currentVersion.Equal(latestVersion) { | ||
currentTimeStamp, err := time.Parse(pseudoVersionTimestampFormat, version.TimeStamp) | ||
if err != nil { | ||
log.Error(err.Error()) | ||
return "", err | ||
} | ||
if currentTimeStamp.After(latestTimeStamp) { | ||
latestTimeStamp = currentTimeStamp | ||
id = version.ID | ||
continue | ||
} | ||
} | ||
} | ||
return id, nil | ||
} | ||
|
||
func findMostRecentTimeStamp(versions []model.TocVersion, ver *semver.Version) (id string, err error) { | ||
log := slog.Default() | ||
if len(versions) == 0 { | ||
msg := "No versions found" | ||
log.Error(msg) | ||
return "", errors.New(msg) | ||
} | ||
var latestTimeStamp time.Time | ||
|
||
for _, version := range versions { | ||
// TODO: use StrictNewVersion | ||
currentVersion, err := semver.NewVersion(version.Version.Model) | ||
if err != nil { | ||
log.Error(err.Error()) | ||
return "", err | ||
} | ||
|
||
if !currentVersion.Equal(ver) { | ||
continue | ||
} | ||
currentTimeStamp, err := time.Parse(pseudoVersionTimestampFormat, version.TimeStamp) | ||
if err != nil { | ||
log.Error(err.Error()) | ||
return "", err | ||
} | ||
if currentTimeStamp.After(latestTimeStamp) { | ||
latestTimeStamp = currentTimeStamp | ||
id = version.ID | ||
continue | ||
} | ||
} | ||
if len(id) == 0 { | ||
msg := fmt.Sprintf("No version %s found", ver.String()) | ||
log.Error(msg) | ||
return "", errors.New(msg) | ||
} | ||
return id, nil | ||
} | ||
|
||
func findDigest(versions []model.TocVersion, digest string) (id string, err error) { | ||
log := slog.Default() | ||
if len(versions) == 0 { | ||
msg := "No versions found" | ||
log.Error(msg) | ||
return "", errors.New(msg) | ||
} | ||
|
||
digest = prep(digest) | ||
for _, version := range versions { | ||
// TODO: how to know if it is official? | ||
tmid, err := model.ParseTMID(version.ID, false) | ||
if err != nil { | ||
log.Error(fmt.Sprintf("Unable to parse TMID from %s", version.ID)) | ||
return "", err | ||
} | ||
if tmid.Version.Hash == digest { | ||
return version.ID, nil | ||
} | ||
} | ||
msg := fmt.Sprintf("No thing model found for digest %s", digest) | ||
log.Error(msg) | ||
return "", errors.New(msg) | ||
} |