Repository: goplus/llpkgstore
Pull request #13: feat!: change metadata struct
Author: login: seri037
Created: 2025-03-24T07:55:57Z | Merged: 2025-03-28T02:44:41Z

This pull request tends to simplify the structure of `metadata struct`, and move the metadata query function from `llgo` for other components (e.g. `pkgsite`) to use. 

- [x] change `metadata struct`
- [x] move metadata query func. back
- [ ] fix related package `action`


Diff:
diff --git a/internal/actions/api_test.go b/internal/actions/api_test.go
index 6959a17..3ecef0b 100644
--- a/internal/actions/api_test.go
+++ b/internal/actions/api_test.go
@@ -48,17 +48,11 @@ func TestLegacyVersion1(t *testing.T) {
 
 	b := []byte(`{
 		"cjson": {
-			"versions" : [{
-				"c": "1.8.18",
-				"go": ["v0.1.0", "v0.1.1"]
-			},{
-				"c": "1.7.18",
-				"go": ["v0.1.2", "v0.1.3"]
-			},
-			{
-				"c": "1.7.16",
-				"go": ["v0.1.0"]
-			}]
+			"versions" : {
+				"1.7.16": ["v0.1.0"],
+				"1.7.18": ["v0.1.2", "v0.1.3"],
+				"1.8.18": ["v0.1.0", "v0.1.1"]
+			}
 		}
 	}`)
 
@@ -95,17 +89,11 @@ func TestLegacyVersion2(t *testing.T) {
 
 	b := []byte(`{
 		"cjson": {
-			"versions" : [{
-				"c": "1.8.18",
-				"go": ["v0.2.0", "v0.2.1"]
-			},{
-				"c": "1.7.18",
-				"go": ["v0.1.0", "v0.1.1"]
-			},
-			{
-				"c": "1.7.16",
-				"go": ["v1.1.0"]
-			}]
+			"versions" : {
+				"1.8.18": ["v0.2.0", "v0.2.1"],
+				"1.7.18": ["v0.1.0", "v0.1.1"],
+				"1.7.16: ["v1.1.0"]
+			}
 		}
 	}`)
 
@@ -140,17 +128,11 @@ func TestLegacyVersion3(t *testing.T) {
 
 	b := []byte(`{
 		"cjson": {
-			"versions" : [{
-				"c": "1.8.18",
-				"go": ["v0.2.0", "v0.2.1"]
-			},{
-				"c": "1.7.18",
-				"go": ["v0.1.1", "v0.1.2"]
-			},
-			{
-				"c": "1.7.16",
-				"go": ["v0.1.0"]
-			}]
+			"versions" : {
+				"1.7.16": ["v0.1.0"],
+				"1.7.18": ["v0.1.1", "v0.1.2"],
+				"1.8.18": ["v0.2.0", "v0.2.1"]
+			}
 		}
 	}`)
 
@@ -185,17 +167,11 @@ func TestLegacyVersion4(t *testing.T) {
 
 	b := []byte(`{
 		"cjson": {
-			"versions" : [{
-				"c": "1.8.18",
-				"go": ["v0.2.0", "v0.2.1"]
-			},{
-				"c": "1.7.18",
-				"go": ["v0.1.1", "v0.1.2"]
-			},
-			{
-				"c": "1.7.16",
-				"go": ["v0.1.0"]
-			}]
+			"versions" : {
+				"1.8.18": ["v0.2.0", "v0.2.1"],
+				"1.7.16": ["v0.1.0"],
+				"1.7.18": ["v0.1.1", "v0.1.2"]
+			}
 		}
 	}`)
 
@@ -231,17 +207,11 @@ func TestLegacyVersion5(t *testing.T) {
 
 	b := []byte(`{
 		"cjson": {
-			"versions" : [{
-				"c": "1.8.18",
-				"go": ["v0.2.0", "v0.2.1"]
-			},{
-				"c": "1.7.18",
-				"go": ["v0.1.2", "v0.1.3"]
-			},
-			{
-				"c": "1.7.16",
-				"go": ["v0.1.0"]
-			}]
+			"versions" : {
+				"1.7.16": ["v0.1.0"],
+				"1.7.18": ["v0.1.2", "v0.1.3"],
+				"1.8.18": ["v0.2.0", "v0.2.1"]
+			}
 		}
 	}`)
 
diff --git a/internal/actions/versions/cver.go b/internal/actions/versions/cver.go
deleted file mode 100644
index 4205d71..0000000
--- a/internal/actions/versions/cver.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package versions
-
-import (
-	"github.com/goplus/llpkgstore/metadata"
-	"golang.org/x/mod/semver"
-)
-
-// cVerMap represents a map of C versions to their corresponding version mappings
-type cVerMap struct {
-	m map[string]*metadata.VersionMapping
-
-	cVerCache       []string // cached list of C versions
-	goVersionsCache []string // cached list of Go versions
-}
-
-// newCverMap creates a new empty cVerMap instance
-func newCverMap() *cVerMap {
-	return &cVerMap{m: map[string]*metadata.VersionMapping{}}
-}
-
-// GoVersions returns all Go versions associated with all C versions
-func (c *cVerMap) GoVersions() (tmp []string) {
-	if c == nil || len(c.m) == 0 {
-		return nil
-	}
-	if c.goVersionsCache != nil {
-		tmp = c.goVersionsCache
-		return
-	}
-	for _, versions := range c.m { // Fixed typo: verions -> versions
-		tmp = append(tmp, versions.GoVersions...)
-	}
-	c.goVersionsCache = tmp // Cache computed value
-	return
-}
-
-// Versions returns all C versions stored in the map
-func (c *cVerMap) Versions() []string {
-	if c == nil || len(c.m) == 0 {
-		return nil
-	}
-	if c.cVerCache != nil {
-		return c.cVerCache
-	}
-	var tmp []string
-	for version := range c.m {
-		tmp = append(tmp, version)
-	}
-	c.cVerCache = tmp // Cache computed value
-	return tmp
-}
-
-// LatestGoVersion returns the highest semantic version of all Go versions
-func (c *cVerMap) LatestGoVersion() string {
-	if c == nil || len(c.m) == 0 {
-		return ""
-	}
-	goVersions := c.GoVersions()
-	if len(goVersions) == 0 {
-		return ""
-	}
-	semver.Sort(goVersions)
-	return goVersions[len(goVersions)-1]
-}
-
-// Get retrieves the VersionMapping for a specific C version
-func (c *cVerMap) Get(cver string) *metadata.VersionMapping {
-	if c == nil || len(c.m) == 0 {
-		return nil
-	}
-	return c.m[cver]
-}
-
-// Set adds or updates a VersionMapping entry
-func (c *cVerMap) Set(versions *metadata.VersionMapping) {
-	c.m[versions.CVersion] = versions
-	c.cVerCache = append(c.cVerCache, ToSemVer(versions.CVersion))        // Update C version cache
-	c.goVersionsCache = append(c.goVersionsCache, versions.GoVersions...) // Update Go versions cache
-}
-
-// LatestGoVersionForCVersion returns the latest Go version for a specific C version
-func (c *cVerMap) LatestGoVersionForCVersion(cver string) string {
-	mappingTable := c.Get(cver)
-	if mappingTable == nil || len(mappingTable.GoVersions) == 0 {
-		return ""
-	}
-	goVersions := make([]string, len(mappingTable.GoVersions))
-	copy(goVersions, mappingTable.GoVersions) // Fixed typo: GoVersion -> GoVersions
-	semver.Sort(goVersions)
-	return goVersions[len(goVersions)-1]
-}
-
-// SearchBySemVer finds the original C version from its semantic version string
-func (c *cVerMap) SearchBySemVer(semver string) (originalVer string) {
-	for cver := range c.m {
-		if ToSemVer(cver) == semver {
-			originalVer = cver
-			break
-		}
-	}
-	return
-}
diff --git a/internal/actions/versions/versions.go b/internal/actions/versions/versions.go
index 1b1ba33..395046f 100644
--- a/internal/actions/versions/versions.go
+++ b/internal/actions/versions/versions.go
@@ -16,11 +16,15 @@ import (
 type Versions struct {
 	metadata.MetadataMap
 
-	fileName    string
-	cVerToGoVer map[string]*cVerMap
+	fileName string
 }
 
-// appendVersion appends a version to an array, panic if the specified version has already existed.
+// appendVersion adds a new version to the slice while preventing duplicates.
+// It panics if the element already exists in the array to enforce uniqueness constraints.
+// Parameters:
+//
+//	arr: Slice of versions to modify
+//	elem: Version to append
 func appendVersion(arr []string, elem string) []string {
 	if slices.Contains(arr, elem) {
 		log.Fatalf("version %s has already existed", elem)
@@ -28,7 +32,11 @@ func appendVersion(arr []string, elem string) []string {
 	return append(arr, elem)
 }
 
-// Read reads version mappings from a file and initializes the Versions struct
+// Read initializes a Versions struct by reading version mappings from a file.
+// It creates the file if it doesn't exist and parses the JSON content into the MetadataMap.
+// Parameters:
+//
+//	fileName: Path to the version mapping file
 func Read(fileName string) *Versions {
 	// read or create a file
 	f, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDONLY, 0644)
@@ -48,99 +56,110 @@ func Read(fileName string) *Versions {
 		json.Unmarshal(b, &m)
 	}
 
-	v := &Versions{
+	return &Versions{
 		MetadataMap: m,
 		fileName:    f.Name(),
-		cVerToGoVer: map[string]*cVerMap{},
 	}
-	v.build()
-	return v
 }
 
-// build constructs the cVerToGoVer map from the metadata
-func (v *Versions) build() {
-	// O(n)
-	for clib := range v.MetadataMap {
-		cverMap := newCverMap()
-
-		versions := v.MetadataMap[clib]
-		for _, version := range versions.VersionMappings {
-			cverMap.Set(version)
-		}
-
-		v.cVerToGoVer[clib] = cverMap
+// cVersions retrieves the version mappings for a specific C library.
+// It returns a map where keys are C library versions and values are supported Go versions.
+func (v *Versions) cVersions(clib string) map[metadata.CVersion][]metadata.GoVersion {
+	versions := v.MetadataMap[clib]
+	if versions == nil {
+		return nil
 	}
+	return versions.Versions
 }
 
-// queryClibVersion finds or creates a VersionMapping for the given C library and version
-func (v *Versions) queryClibVersion(clib, clibVersion string) (versions *metadata.VersionMapping, needCreate bool) {
-	versions = v.cVerToGoVer[clib].Get(clibVersion)
-	// fast-path: we have a cache
-	if versions != nil {
+// CVersions returns all available versions of the specified C library.
+// The versions are returned as semantic version strings.
+func (v *Versions) CVersions(clib string) (ret []string) {
+	versions := v.MetadataMap[clib]
+	if versions == nil {
 		return
 	}
-	needCreate = true
-	// we find noting, make a blank one.
-	versions = &metadata.VersionMapping{CVersion: clibVersion}
+	for version := range versions.Versions {
+		ret = append(ret, ToSemVer(version))
+	}
 	return
 }
 
-func (v *Versions) CVersions(clib string) []string {
-	return v.cVerToGoVer[clib].Versions()
-}
-
-func (v *Versions) GoVersions(clib string) []string {
-	return v.cVerToGoVer[clib].GoVersions()
+// GoVersions lists all Go versions associated with the given C library.
+func (v *Versions) GoVersions(clib string) (ret []string) {
+	versions := v.MetadataMap[clib]
+	if versions == nil {
+		return
+	}
+	for _, goversion := range versions.Versions {
+		ret = append(ret, goversion...)
+	}
+	return
 }
 
+// LatestGoVersionForCVersion finds the latest Go version compatible with a specific C library version.
 func (v *Versions) LatestGoVersionForCVersion(clib, cver string) string {
-	return v.cVerToGoVer[clib].LatestGoVersionForCVersion(cver)
+	version := v.MetadataMap[clib]
+	if version == nil {
+		return ""
+	}
+	goVersions := version.Versions[cver]
+	if len(goVersions) == 0 {
+		return ""
+	}
+	semver.Sort(goVersions)
+	return goVersions[len(goVersions)-1]
 }
 
+// SearchBySemVer looks up a C library version by its semantic version string.
 func (v *Versions) SearchBySemVer(clib, semver string) string {
-	return v.cVerToGoVer[clib].SearchBySemVer(semver)
+	for version := range v.cVersions(clib) {
+		if ToSemVer(version) == semver {
+			return version
+		}
+	}
+	return ""
 }
 
-// LatestGoVersion returns the latest Go version associated with the given C library
+// LatestGoVersion retrieves the latest Go version associated with the specified C library.
+// It aggregates all Go versions across all C library versions and returns the highest one based on semantic versioning.
 func (v *Versions) LatestGoVersion(clib string) string {
-	clibVer := v.cVerToGoVer[clib].LatestGoVersion()
-	log.Println(clibVer)
-	if clibVer != "" {
-		return clibVer
-	}
-	allVersions := v.MetadataMap[clib]
-	if allVersions == nil {
-		return ""
-	}
-	var tmp []string
-	for _, verions := range allVersions.VersionMappings {
-		tmp = append(tmp, verions.GoVersions...)
-	}
-	if len(tmp) == 0 {
+	versions := v.GoVersions(clib)
+	if len(versions) == 0 {
 		return ""
 	}
-	semver.Sort(tmp)
-	return tmp[len(tmp)-1]
+	semver.Sort(versions)
+	return versions[len(versions)-1]
 }
 
-// Write records a new Go version mapping for a C library version and persists to file
+// Write records a new Go version mapping for a C library version and persists to file.
+// Parameters:
+//
+//	clib: The C library name.
+//	clibVersion: The specific version of the C library.
+//	mappedVersion: The Go version to map with the C library version.
+//
+// It appends the Go version to the existing list for the C library version and saves the updated metadata.
 func (v *Versions) Write(clib, clibVersion, mappedVersion string) {
-	versions, needCreate := v.queryClibVersion(clib, clibVersion)
-
-	versions.GoVersions = appendVersion(versions.GoVersions, mappedVersion)
-
-	if needCreate {
-		if v.MetadataMap[clib] == nil {
-			v.MetadataMap[clib] = &metadata.Metadata{}
+	clibVersions := v.MetadataMap[clib]
+	if clibVersions == nil {
+		clibVersions = &metadata.Metadata{
+			Versions: map[metadata.CVersion][]metadata.GoVersion{},
 		}
-		v.MetadataMap[clib].VersionMappings = append(v.MetadataMap[clib].VersionMappings, versions)
+		v.MetadataMap[clib] = clibVersions
 	}
+	versions := clibVersions.Versions[clibVersion]
+
+	versions = appendVersion(versions, mappedVersion)
+
+	clibVersions.Versions[clibVersion] = versions
 	// sync to disk
 	b, _ := json.Marshal(&v.MetadataMap)
 
 	os.WriteFile(v.fileName, []byte(b), 0644)
 }
 
+// String returns the JSON representation of the Versions metadata.
 func (v *Versions) String() string {
 	b, _ := json.Marshal(&v.MetadataMap)
 	return string(b)
diff --git a/internal/actions/versions/versions_test.go b/internal/actions/versions/versions_test.go
index 0dd969b..43f4ca8 100644
--- a/internal/actions/versions/versions_test.go
+++ b/internal/actions/versions/versions_test.go
@@ -5,19 +5,17 @@ import (
 	"os"
 	"reflect"
 	"testing"
+
+	"golang.org/x/mod/semver"
 )
 
 func TestCVersions(t *testing.T) {
 	b := []byte(`{
 		"cgood": {
-			"versions" : [{
-				"c": "1.3",
-				"go": ["v0.1.0", "v0.1.1"]
-			},
-			{
-				"c": "1.3.1",
-				"go": ["v1.1.0"]
-			}]
+			"versions" : {
+				"1.3": ["v0.1.0", "v0.1.1"],
+				"1.3.1": ["v1.1.0"]
+			}
 		}
 	}`)
 	path := "ttt.json"
@@ -29,13 +27,18 @@ func TestCVersions(t *testing.T) {
 	defer os.Remove(path)
 
 	v := Read(path)
+	goVersion := v.GoVersions("cgood")
+	semver.Sort(goVersion)
 
-	if !reflect.DeepEqual(v.GoVersions("cgood"), []string{"v0.1.0", "v0.1.1", "v1.1.0"}) {
-		t.Errorf("unexpected goversion: want: %v got: %v", []string{"v0.1.0", "v0.1.1", "v1.1.0"}, v.GoVersions("cgood"))
+	if !reflect.DeepEqual(goVersion, []string{"v0.1.0", "v0.1.1", "v1.1.0"}) {
+		t.Errorf("unexpected goversion: want: %v got: %v", []string{"v0.1.0", "v0.1.1", "v1.1.0"}, goVersion)
 	}
 
-	if !reflect.DeepEqual(v.CVersions("cgood"), []string{"v1.3.0", "v1.3.1"}) {
-		t.Errorf("unexpected cversion: want: %v got: %v", []string{"v1.3.0", "v1.3.1"}, v.CVersions("cgood"))
+	cVersion := v.CVersions("cgood")
+	semver.Sort(cVersion)
+
+	if !reflect.DeepEqual(cVersion, []string{"v1.3.0", "v1.3.1"}) {
+		t.Errorf("unexpected cversion: want: %v got: %v", []string{"v1.3.0", "v1.3.1"}, cVersion)
 	}
 
 	if v.LatestGoVersionForCVersion("cgood", "1.3") != "v0.1.1" {
@@ -44,21 +47,20 @@ func TestCVersions(t *testing.T) {
 
 	if v.SearchBySemVer("cgood", "v1.3.0") != "1.3" {
 		t.Errorf("unexpected search by semver result: want: %v got: %v", "1.3", v.SearchBySemVer("cgood", "v1.3.0"))
+	}
 
+	if v.SearchBySemVer("agood", "v1.3.0") != "" {
+		t.Errorf("unexpected search by semver result: want: %v got: %v", "", v.SearchBySemVer("cgood", "v1.3.0"))
 	}
 }
 
 func TestLatestVersion(t *testing.T) {
 	b := []byte(`{
     "cgood": {
-        "versions" : [{
-            "c": "1.3",
-            "go": ["v0.1.0", "v0.1.1"]
-        },
-        {
-            "c": "1.3.1",
-            "go": ["v1.1.0"]
-        }]
+        "versions" : {
+				"1.3": ["v0.1.0", "v0.1.1"],
+				"1.3.1": ["v1.1.0"]
+		}
     }
 }`)
 	path := "ttt.json"
@@ -94,7 +96,7 @@ func TestAppend(t *testing.T) {
 
 	b, _ := os.ReadFile("llpkgstore.json")
 
-	if !bytes.Equal(b, []byte(`{"cjson":{"versions":[{"c":"1.7.18","go":["v1.0.0","v1.0.1"]},{"c":"1.7.19","go":["v1.0.2"]}]},"libxml":{"versions":[{"c":"1.45.1.4","go":["v1.0.0"]},{"c":"1.45.1.5","go":["v1.0.1"]}]}}`)) {
+	if !bytes.Equal(b, []byte(`{"cjson":{"versions":{"1.7.18":["v1.0.0","v1.0.1"],"1.7.19":["v1.0.2"]}},"libxml":{"versions":{"1.45.1.4":["v1.0.0"],"1.45.1.5":["v1.0.1"]}}}`)) {
 		t.Error("unexpected append result")
 	}
 }
diff --git a/metadata/cache.go b/metadata/cache.go
new file mode 100644
index 0000000..bc52dc3
--- /dev/null
+++ b/metadata/cache.go
@@ -0,0 +1,170 @@
+package metadata
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+	"time"
+)
+
+var (
+	ErrCacheFileNotFound = errors.New("cache file not found")
+)
+
+// Cache represents a local cache for storing and retrieving data.
+// It binds a local file path and a remote data source URL
+type Cache[T any] struct {
+	data T // stores cached data
+
+	cacheFilePath string // local file path for cache storage
+	remoteUrl     string // URL of the remote data source
+
+	modTime time.Time // last modified time of the cached data
+}
+
+// NewCache initializes and loads the cache from disk or remote source
+func NewCache[T any](cacheFilePath, remoteUrl string) (*Cache[T], error) {
+	cache := &Cache[T]{
+		cacheFilePath: cacheFilePath,
+		remoteUrl:     remoteUrl,
+	}
+
+	err := cache.loadFromDisk()
+	if err != nil {
+		// local cache missing or invalid, fetch from remote
+		err = cache.Update()
+		if err != nil {
+			return nil, fmt.Errorf("error building cache: %v", err)
+		}
+	}
+
+	return cache, nil
+}
+
+// Update refreshes the cache by fetching remote data and saving to disk
+func (c *Cache[T]) Update() error {
+	err := c.fetch()
+	if err != nil {
+		return err
+	}
+
+	err = c.saveToDisk()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// fetch retrieves the latest data from the remote source using conditional requests
+func (c *Cache[T]) fetch() error {
+	// Create HTTP request with If-Modified-Since header to reduce unnecessary downloads
+	req, err := http.NewRequest("GET", c.remoteUrl, nil)
+	if err != nil {
+		return err
+	}
+	if !c.modTime.IsZero() {
+		req.Header.Set("If-Modified-Since", c.modTime.Format(http.TimeFormat))
+	}
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	switch resp.StatusCode {
+	case http.StatusNotModified:
+		return nil
+	case http.StatusOK:
+		// Read and parse the response body
+		body, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+
+		var bodyData T
+		err = json.Unmarshal(body, &bodyData)
+		if err != nil {
+			return err
+		}
+		c.data = bodyData
+
+		// Update last modified time from response headers
+		lastModified := resp.Header.Get("Last-Modified")
+		if lastModified != "" {
+			c.modTime, err = time.Parse(http.TimeFormat, lastModified)
+			if err != nil {
+				return err
+			}
+		}
+		return nil
+	default:
+		return fmt.Errorf("HTTP error %d: %s", resp.StatusCode, resp.Status)
+	}
+}
+
+func (c *Cache[T]) Data() T {
+	return c.data
+}
+
+// saveToDisk persists the current cache data to the local file system
+func (c *Cache[T]) saveToDisk() error {
+	// Create directory structure if needed
+	err := os.MkdirAll(filepath.Dir(c.cacheFilePath), 0755)
+	if err != nil {
+		return err
+	}
+
+	// Serialize data to JSON
+	file, err := json.Marshal(c.data)
+	if err != nil {
+		return err
+	}
+
+	// Write to file with proper permissions
+	err = os.WriteFile(c.cacheFilePath, file, 0644)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *Cache[T]) loadFromDisk() error {
+	if c.exists() {
+		// Read the cache file.
+		file, err := os.ReadFile(c.cacheFilePath)
+		if err != nil {
+			return fmt.Errorf("error read file from cache: %v", err)
+		}
+
+		// Unmarshal the cache file.
+		var fileData T
+		err = json.Unmarshal(file, &fileData)
+		if err != nil {
+			return fmt.Errorf("error json unmarshal from cache: %v", err)
+		}
+		c.data = fileData
+
+		// Get the last modified time of the cache.
+		fileInfo, err := os.Stat(c.cacheFilePath)
+		if err != nil {
+			return err
+		}
+		c.modTime = fileInfo.ModTime()
+	} else {
+		return ErrCacheFileNotFound
+	}
+
+	return nil
+}
+
+// Returns true if the local cache exists.
+func (c *Cache[T]) exists() bool {
+	_, err := os.Stat(c.cacheFilePath)
+	return !errors.Is(err, os.ErrNotExist)
+}
diff --git a/metadata/cache_test.go b/metadata/cache_test.go
new file mode 100644
index 0000000..deaac83
--- /dev/null
+++ b/metadata/cache_test.go
@@ -0,0 +1,116 @@
+package metadata
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"path/filepath"
+	"reflect"
+	"testing"
+	"time"
+)
+
+// Test data
+var testCacheData = MetadataMap{
+	"example-module": &Metadata{
+		Versions: map[CVersion][]GoVersion{
+			"1.7.18": {"v1.2.0"},
+		},
+	},
+}
+
+func TestCache_InitFromRemote(t *testing.T) {
+	// Create mock HTTP server
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Last-Modified", "Sat, 01 Jan 2022 00:00:00 GMT")
+		json.NewEncoder(w).Encode(testCacheData)
+	}))
+	defer server.Close()
+
+	// Create temporary directory
+	tmpDir := t.TempDir()
+	cachePath := filepath.Join(tmpDir, "test_cache.json")
+
+	// Initialize cache
+	cache, err := NewCache[MetadataMap](cachePath, server.URL)
+	if err != nil {
+		t.Fatalf("Failed to create cache: %v", err)
+	}
+
+	// Validate data
+	if !reflect.DeepEqual(cache.Data(), testCacheData) {
+		t.Errorf("Cache data mismatch. Expected: %v, Got: %v", testCacheData, cache.Data())
+	}
+}
+
+// Test loading cache from disk (skip remote request)
+func TestCache_LoadFromDisk(t *testing.T) {
+	tmpDir := t.TempDir()
+	cachePath := filepath.Join(tmpDir, "existing_cache.json")
+
+	// Manually write cache file
+	err := os.WriteFile(cachePath, []byte(`{"example-module":{"versions":{"1.7.18":["v1.2.0"]}}}`), 0644)
+	if err != nil {
+		t.Fatalf("Failed to write cache file: %v", err)
+	}
+
+	// Initialize cache (should load from local file directly)
+	cache, err := NewCache[MetadataMap](cachePath, "https://unused.com")
+	if err != nil {
+		t.Fatalf("Failed to create cache: %v", err)
+	}
+
+	// Validate data
+	if !reflect.DeepEqual(cache.Data(), testCacheData) {
+		t.Errorf("Cache data mismatch. Expected: %v, Got: %v", testCacheData, cache.Data())
+	}
+}
+
+// Test HTTP 304 Not Modified response
+func TestCache_Fetch304(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.Header.Get("If-Modified-Since") == "old-time" {
+			w.WriteHeader(http.StatusNotModified)
+		} else {
+			w.WriteHeader(http.StatusOK)
+			json.NewEncoder(w).Encode(testCacheData)
+		}
+	}))
+	defer server.Close()
+
+	tmpDir := t.TempDir()
+	cachePath := filepath.Join(tmpDir, "cache.json")
+
+	// Initial load
+	cache, _ := NewCache[MetadataMap](cachePath, server.URL)
+
+	// Set modTime to an old time
+	cache.modTime = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
+
+	// Second fetch should return 304
+	err := cache.fetch()
+	if err != nil {
+		t.Fatalf("Failed to fetch cache: %v", err)
+	}
+
+	// Validate data remains unchanged
+	if !reflect.DeepEqual(cache.Data(), testCacheData) {
+		t.Errorf("Cache data mismatch. Expected: %v, Got: %v", testCacheData, cache.Data())
+	}
+}
+
+// Test invalid JSON response
+func TestCache_InvalidJSON(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte(`invalid json`))
+	}))
+	defer server.Close()
+
+	tmpDir := t.TempDir()
+	_, err := NewCache[MetadataMap](filepath.Join(tmpDir, "cache.json"), server.URL)
+
+	if err == nil {
+		t.Errorf("Expected error for invalid JSON response, but got nil")
+	}
+}
diff --git a/metadata/metadata.go b/metadata/metadata.go
index 8d7fd5f..b2b61f1 100644
--- a/metadata/metadata.go
+++ b/metadata/metadata.go
@@ -1,13 +1,149 @@
 package metadata
 
+import (
+	"errors"
+	"path/filepath"
+)
+
+var (
+	remoteMetadataURL      = "https://llpkg.goplus.org/llpkgstore.json" // change only for testing
+	cachedMetadataFileName = "llpkgstore.json"
+	ErrMetadataNotInCache  = errors.New("metadata not in cache")
+)
+
+type CVersion = string
+type GoVersion = string
+
 // MetadataMap represents llpkgstore.json
 type MetadataMap map[string]*Metadata
 
 type Metadata struct {
-	VersionMappings []*VersionMapping `json:"versions"`
+	Versions map[CVersion][]GoVersion `json:"versions"`
+}
+
+type metadataMgr struct {
+	cache *Cache[MetadataMap]
+
+	// Add flat hash for optimization
+	flatCToGo map[flatKey][]string // "name/cversion" -> []goversion
+	flatGoToC map[flatKey]string   // "name/goversion" -> cversion
+}
+
+// NewMetadataMgr returns a new metadata manager
+func NewMetadataMgr(cacheDir string) (*metadataMgr, error) {
+	cachePath := filepath.Join(cacheDir, cachedMetadataFileName)
+	cache, err := NewCache[MetadataMap](cachePath, remoteMetadataURL)
+	if err != nil {
+		return nil, err
+	}
+
+	mgr := &metadataMgr{
+		cache:     cache,
+		flatCToGo: make(map[flatKey][]string),
+		flatGoToC: make(map[flatKey]string),
+	}
+
+	err = mgr.buildFlatVersionMaps()
+	if err != nil {
+		return nil, err
+	}
+
+	return mgr, nil
+}
+
+// Returns all up-to-date metadata
+func (m *metadataMgr) AllMetadata() (MetadataMap, error) {
+	err := m.update()
+	if err != nil {
+		return nil, err
+	}
+	return m.allCachedMetadata(), nil
+}
+
+// Returns the up-to-date module metadata by name
+func (m *metadataMgr) MetadataByName(name string) (Metadata, error) {
+	// First try to find the module metadata in the cache
+	metadata, err := m.cachedMetadataByName(name)
+	if errors.Is(err, ErrMetadataNotInCache) || errors.Is(err, ErrCacheFileNotFound) {
+		// If the module metadata is not in the cache, update the cache
+		err := m.update()
+		if err != nil {
+			return Metadata{}, err
+		}
+
+		// Find the module metadata again
+		metadata, err = m.cachedMetadataByName(name)
+		if err != nil {
+			return Metadata{}, err
+		}
+	}
+
+	return metadata, nil
 }
 
-type VersionMapping struct {
-	CVersion   string   `json:"c"`
-	GoVersions []string `json:"go"`
+// Returns true if the name is an exist module name
+func (m *metadataMgr) ModuleExists(name string) (bool, error) {
+	_, err := m.MetadataByName(name)
+	if errors.Is(err, ErrMetadataNotInCache) {
+		return false, nil
+	} else if err != nil {
+		return false, err
+	}
+	return true, nil
+}
+
+// Returns the module metadata in the cache
+func (m *metadataMgr) allCachedMetadata() MetadataMap {
+	cache := m.cache.Data()
+	return cache
+}
+
+// Returns the module metadata in the cache by name
+func (m *metadataMgr) cachedMetadataByName(name string) (Metadata, error) {
+	allMetadata := m.allCachedMetadata()
+
+	metadata, ok := allMetadata[name]
+	if !ok {
+		return Metadata{}, ErrMetadataNotInCache
+	}
+
+	return *metadata, nil
+}
+
+func (m *metadataMgr) update() error {
+	err := m.cache.Update()
+	if err != nil {
+		return err
+	}
+
+	err = m.buildFlatVersionMaps()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *metadataMgr) buildFlatVersionMaps() error {
+	// Reset flat hash
+	m.flatCToGo = make(map[flatKey][]string)
+	m.flatGoToC = make(map[flatKey]string)
+
+	allCachedMetadata := m.allCachedMetadata()
+
+	for name, metadata := range allCachedMetadata {
+		versions := metadata.Versions
+		for cVersion, goVersions := range versions {
+			// Build flat hash
+			cKey := flatKey{name, cVersion}
+			m.flatCToGo[cKey] = goVersions
+
+			for _, goVersion := range goVersions {
+				goKey := flatKey{name, goVersion}
+				m.flatGoToC[goKey] = cVersion
+			}
+		}
+	}
+
+	return nil
 }
diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go
new file mode 100644
index 0000000..5602876
--- /dev/null
+++ b/metadata/metadata_test.go
@@ -0,0 +1,293 @@
+package metadata
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"path/filepath"
+	"reflect"
+	"testing"
+)
+
+var testMetadata = MetadataMap{
+	"example-module": &Metadata{
+		Versions: map[CVersion][]GoVersion{
+			"1.7.18": {"v1.2.0"},
+		},
+	},
+}
+
+// TestNewMetadataMgr verifies that the MetadataMgr is successfully created with valid remote data.
+func TestNewMetadataMgr(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/llpkgstore.json" {
+			w.Header().Set("Last-Modified", "Sat, 01 Jan 2022 00:00:00 GMT")
+			json.NewEncoder(w).Encode(testMetadata)
+		} else {
+			http.Error(w, "Not Found", http.StatusNotFound)
+		}
+	}))
+	defer server.Close()
+
+	// Save original URL and override with mock server URL
+	originalURL := remoteMetadataURL
+	defer func() { remoteMetadataURL = originalURL }()
+	remoteMetadataURL = server.URL + "/llpkgstore.json"
+
+	tmpDir := t.TempDir()
+	mgr, err := NewMetadataMgr(tmpDir)
+	if err != nil {
+		t.Fatalf("Failed to create metadata manager: %v", err)
+	}
+
+	// Verify successful initialization
+	if mgr == nil {
+		t.Fatal("Metadata manager should not be nil")
+	} else if mgr.cache == nil {
+		t.Fatal("Metadata cache should not be nil")
+	}
+}
+
+// TestMetadataMgr_AllMetadata verifies that AllMetadata() returns the complete metadata after update.
+func TestMetadataMgr_AllMetadata(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/llpkgstore.json" {
+			json.NewEncoder(w).Encode(testMetadata)
+		} else {
+			http.Error(w, "Not Found", http.StatusNotFound)
+		}
+	}))
+	defer server.Close()
+
+	// Override remote URL to mock server
+	originalURL := remoteMetadataURL
+	defer func() { remoteMetadataURL = originalURL }()
+	remoteMetadataURL = server.URL + "/llpkgstore.json"
+
+	tmpDir := t.TempDir()
+	mgr, _ := NewMetadataMgr(tmpDir)
+
+	// Force metadata update from mock server
+	err := mgr.update()
+	if err != nil {
+		t.Fatalf("Failed to update metadata: %v", err)
+	}
+
+	// Retrieve and validate all metadata
+	data, err := mgr.AllMetadata()
+	if err != nil {
+		t.Fatalf("Failed to retrieve metadata: %v", err)
+	}
+
+	if !reflect.DeepEqual(data, testMetadata) {
+		t.Errorf("Metadata mismatch. Expected: %v, Got: %v", testMetadata, data)
+	}
+}
+
+// TestNewMetadataMgr_LocalAndRemoteFailure verifies MetadataMgr creation fails when both local cache and remote are unavailable
+func TestNewMetadataMgr_LocalAndRemoteFailure(t *testing.T) {
+	tmpDir := t.TempDir()
+	cachePath := filepath.Join(tmpDir, cachedMetadataFileName)
+	err := os.WriteFile(cachePath, []byte(`{ invalid json `), 0644)
+	if err != nil {
+		t.Fatalf("Failed to write cache file: %v", err)
+	}
+
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusInternalServerError)
+	}))
+	defer server.Close()
+
+	originalURL := remoteMetadataURL
+	defer func() { remoteMetadataURL = originalURL }()
+	remoteMetadataURL = server.URL + "/llpkgstore.json"
+
+	_, err = NewMetadataMgr(tmpDir)
+	if err == nil {
+		t.Fatal("Expected error, but got nil")
+	}
+}
+
+// TestMetadataMgr_MetadataByName_Existing validates MetadataByName returns correct metadata for existing modules
+func TestMetadataMgr_MetadataByName_Existing(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/llpkgstore.json" {
+			json.NewEncoder(w).Encode(testMetadata)
+		} else {
+			http.Error(w, "Not Found", http.StatusNotFound)
+		}
+	}))
+	defer server.Close()
+
+	originalURL := remoteMetadataURL
+	defer func() { remoteMetadataURL = originalURL }()
+	remoteMetadataURL = server.URL + "/llpkgstore.json"
+
+	tmpDir := t.TempDir()
+	mgr, _ := NewMetadataMgr(tmpDir)
+
+	metadata, err := mgr.MetadataByName("example-module")
+	if err != nil {
+		t.Fatalf("Failed to retrieve metadata: %v", err)
+	}
+
+	if !reflect.DeepEqual(metadata, *testMetadata["example-module"]) {
+		t.Errorf("Metadata mismatch. Expected: %v, Got: %v", testMetadata["example-module"], metadata)
+	}
+}
+
+// TestMetadataMgr_MetadataByName_NotFound ensures MetadataByName returns error for non-existent modules
+func TestMetadataMgr_MetadataByName_NotFound(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/llpkgstore.json" {
+			json.NewEncoder(w).Encode(MetadataMap{})
+		} else {
+			http.Error(w, "Not Found", http.StatusNotFound)
+		}
+	}))
+	defer server.Close()
+
+	originalURL := remoteMetadataURL
+	defer func() { remoteMetadataURL = originalURL }()
+	remoteMetadataURL = server.URL + "/llpkgstore.json"
+
+	tmpDir := t.TempDir()
+	mgr, _ := NewMetadataMgr(tmpDir)
+
+	_, err := mgr.MetadataByName("nonexistent-module")
+	if !errors.Is(err, ErrMetadataNotInCache) {
+		t.Errorf("Expected ErrMetadataNotInCache, but got: %v", err)
+	}
+}
+
+// TestMetadataMgr_ModuleExists checks ModuleExists correctly identifies existing/non-existent modules
+func TestMetadataMgr_ModuleExists(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/llpkgstore.json" {
+			json.NewEncoder(w).Encode(testMetadata)
+		} else {
+			http.Error(w, "Not Found", http.StatusNotFound)
+		}
+	}))
+	defer server.Close()
+
+	originalURL := remoteMetadataURL
+	defer func() { remoteMetadataURL = originalURL }()
+	remoteMetadataURL = server.URL + "/llpkgstore.json"
+
+	tmpDir := t.TempDir()
+	mgr, _ := NewMetadataMgr(tmpDir)
+
+	exists, err := mgr.ModuleExists("example-module")
+	if err != nil {
+		t.Fatalf("Failed to check module existence: %v", err)
+	}
+	if !exists {
+		t.Fatal("Expected module to exist, but it doesn't")
+	}
+
+	exists, err = mgr.ModuleExists("nonexistent-module")
+	if err != nil {
+		t.Fatalf("Failed to check module existence: %v", err)
+	}
+	if exists {
+		t.Fatal("Expected module to not exist, but it does")
+	}
+}
+
+// TestMetadataMgr_UpdateError verifies Update() returns error for failed remote requests
+func TestMetadataMgr_UpdateError(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/llpkgstore.json" {
+			w.WriteHeader(http.StatusInternalServerError)
+		} else {
+			http.Error(w, "Not Found", http.StatusNotFound)
+		}
+	}))
+	defer server.Close()
+
+	originalURL := remoteMetadataURL
+	defer func() { remoteMetadataURL = originalURL }()
+	remoteMetadataURL = server.URL + "/llpkgstore.json"
+
+	testMetadataJSON, err := json.Marshal(testMetadata)
+	if err != nil {
+		t.Fatalf("Failed to marshal test metadata: %v", err)
+	}
+
+	tmpDir := t.TempDir()
+	// ensure metadataMgr can be created
+	os.WriteFile(filepath.Join(tmpDir, "llpkgstore.json"), testMetadataJSON, 0644)
+	mgr, err := NewMetadataMgr(tmpDir)
+	if err != nil {
+		t.Fatalf("Failed to create metadata manager: %v", err)
+	}
+
+	err = mgr.update()
+	if err == nil {
+		t.Fatal("Expected error, but got nil")
+	}
+}
+
+// Test invalid remote data scenario
+// TestNewMetadataMgr_InvalidRemoteData verifies creation fails with invalid remote JSON
+func TestNewMetadataMgr_InvalidRemoteData(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/llpkgstore.json" {
+			w.WriteHeader(http.StatusOK)
+			w.Write([]byte(`invalid json`))
+		} else {
+			http.Error(w, "Not Found", http.StatusNotFound)
+		}
+	}))
+	defer server.Close()
+
+	originalURL := remoteMetadataURL
+	defer func() { remoteMetadataURL = originalURL }()
+	remoteMetadataURL = server.URL + "/llpkgstore.json"
+
+	tmpDir := t.TempDir()
+	_, err := NewMetadataMgr(tmpDir)
+	if err == nil {
+		t.Fatal("Expected error, but got nil")
+	}
+}
+
+// Test recovery from corrupted cache file
+// TestNewMetadataMgr_BadCacheFile checks recovery from corrupted cache by fetching remote data
+func TestNewMetadataMgr_BadCacheFile(t *testing.T) {
+	tmpDir := t.TempDir()
+	cachePath := filepath.Join(tmpDir, cachedMetadataFileName)
+	err := os.WriteFile(cachePath, []byte(`{ invalid json `), 0644)
+	if err != nil {
+		t.Fatalf("Failed to write cache file: %v", err)
+	}
+
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/llpkgstore.json" {
+			json.NewEncoder(w).Encode(testMetadata)
+		} else {
+			http.Error(w, "Not Found", http.StatusNotFound)
+		}
+	}))
+	defer server.Close()
+
+	originalURL := remoteMetadataURL
+	defer func() { remoteMetadataURL = originalURL }()
+	remoteMetadataURL = server.URL + "/llpkgstore.json"
+
+	mgr, err := NewMetadataMgr(tmpDir)
+	if err != nil {
+		t.Fatalf("Failed to create metadata manager: %v", err)
+	}
+
+	data, err := mgr.AllMetadata()
+	if err != nil {
+		t.Fatalf("Failed to retrieve metadata: %v", err)
+	}
+	if !reflect.DeepEqual(data, testMetadata) {
+		t.Errorf("Metadata mismatch. Expected: %v, Got: %v", testMetadata, data)
+	}
+}
diff --git a/metadata/version.go b/metadata/version.go
new file mode 100644
index 0000000..4cd74ea
--- /dev/null
+++ b/metadata/version.go
@@ -0,0 +1,209 @@
+package metadata
+
+import (
+	"errors"
+	"fmt"
+
+	"golang.org/x/mod/semver"
+)
+
+// Gets the latest C version associated with the latest Go version
+func (m *metadataMgr) LatestCVer(name string) (string, error) {
+	latestGoVersion, err := m.LatestGoVer(name)
+	if err != nil {
+		return "", err
+	}
+
+	latestCVersion, err := m.CVerFromGoVer(name, latestGoVersion)
+	if err != nil {
+		return "", err
+	}
+
+	return latestCVersion, nil
+}
+
+// Gets the latest Go version for the given module name
+func (m *metadataMgr) LatestGoVer(name string) (string, error) {
+	allGoVersions, err := m.AllGoVersFromName(name)
+	if err != nil {
+		return "", err
+	}
+
+	if len(allGoVersions) == 0 {
+		return "", fmt.Errorf("no Go versions found for %s", name)
+	}
+
+	semver.Sort(allGoVersions)
+	latestGoVersion := allGoVersions[len(allGoVersions)-1]
+
+	return latestGoVersion, nil
+}
+
+// Gets the latest Go version based on the module name and C version
+func (m *metadataMgr) LatestGoVerFromCVer(name, cVer string) (string, error) {
+	// Build the flat key
+	cKey := flatKey{name, cVer}
+
+	// Search for the latest Go version
+	goVersions, ok := m.flatCToGo[cKey]
+	if !ok {
+		// Try to update if not found
+		err := m.update()
+		if err != nil {
+			return "", err
+		}
+
+		// Try again
+		goVersions, ok = m.flatCToGo[cKey]
+		if !ok {
+			return "", fmt.Errorf("no version mappings for %s %s", name, cVer)
+		}
+	}
+
+	if len(goVersions) > 0 {
+		semver.Sort(goVersions)
+		latestGoVersion := goVersions[len(goVersions)-1]
+
+		return latestGoVersion, nil
+	}
+
+	return "", fmt.Errorf("no version mappings for %s %s", name, cVer)
+}
+
+// Gets Go versions based on the module name and C version
+func (m *metadataMgr) GoVersFromCVer(name, cVer string) ([]string, error) {
+	// Build the flat key
+	cKey := flatKey{name, cVer}
+
+	// Search for the Go versions
+	versions, ok := m.flatCToGo[cKey]
+	if !ok {
+		// Try to update if not found
+		err := m.update()
+		if err != nil {
+			return nil, err
+		}
+
+		// Try again
+		versions, ok = m.flatCToGo[cKey]
+		if !ok {
+			return nil, fmt.Errorf("no version mappings for %s %s", name, cVer)
+		}
+	}
+
+	// Return a copy
+	result := make([]string, len(versions))
+	copy(result, versions)
+
+	return result, nil
+}
+
+// Gets the C version based on the module name and Go version
+func (m *metadataMgr) CVerFromGoVer(name, goVer string) (string, error) {
+	// Build the flat key
+	goKey := flatKey{name, goVer}
+
+	// Search for the C version in the cached flat hash
+	cVersion, ok := m.flatGoToC[goKey]
+	if !ok {
+		// Update if not found
+		err := m.update()
+		if err != nil {
+			return "", err
+		}
+
+		// Try again
+		cVersion, ok = m.flatGoToC[goKey]
+		if !ok {
+			return "", fmt.Errorf("no C version found for %s %s", name, goVer)
+		}
+	}
+
+	return cVersion, nil
+}
+
+// Gets all Go versions for the given module name
+func (m *metadataMgr) AllGoVersFromName(name string) ([]string, error) {
+	queryFunc := func(name string) ([]string, error) {
+		exists, err := m.ModuleExists(name)
+		if !exists {
+			return nil, errors.New("module not found")
+		}
+		if err != nil {
+			return nil, err
+		}
+
+		// Extract Go versions
+		goVersions := make([]string, 0, len(m.flatGoToC))
+		for goVersionKey, _ := range m.flatGoToC {
+			if goVersionKey.name == name {
+				goVersions = append(goVersions, goVersionKey.version)
+			}
+		}
+
+		return goVersions, nil
+	}
+
+	goVersions, err := queryFunc(name)
+	if err != nil {
+		// update and try again
+		err := m.update()
+		if err != nil {
+			return nil, err
+		}
+
+		goVersions, err = queryFunc(name)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return goVersions, nil
+}
+
+// Gets all C versions for the given module name
+func (m *metadataMgr) AllCVersFromName(name string) ([]string, error) {
+	queryFunc := func(name string) ([]string, error) {
+		exists, err := m.ModuleExists(name)
+		if !exists {
+			return nil, errors.New("module not found")
+		}
+		if err != nil {
+			return nil, err
+		}
+
+		// Extract C versions
+		cVersions := make([]string, 0, len(m.flatCToGo))
+		for cVersionKey, _ := range m.flatCToGo {
+			if cVersionKey.name == name {
+				cVersions = append(cVersions, cVersionKey.version)
+			}
+		}
+
+		return cVersions, nil
+	}
+
+	cVersions, err := queryFunc(name)
+	if err != nil {
+		// update and try again
+		err := m.update()
+		if err != nil {
+			return nil, err
+		}
+
+		cVersions, err = queryFunc(name)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return cVersions, nil
+}
+
+type flatKey struct {
+	name, version string
+}
+
+func (m *flatKey) String(name, version string) string {
+	return fmt.Sprintf("%s/%s", m.name, m.version)
+}
diff --git a/metadata/version_test.go b/metadata/version_test.go
new file mode 100644
index 0000000..e14f7e6
--- /dev/null
+++ b/metadata/version_test.go
@@ -0,0 +1,249 @@
+package metadata
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"reflect"
+	"slices"
+	"testing"
+)
+
+// Define more enriched test data
+var enhancedTestVersionData = MetadataMap{
+	"test-module": &Metadata{
+		Versions: map[CVersion][]GoVersion{
+			"1.7.18": {"v1.2.0", "v1.2.1"},
+			"1.7.19": {"v1.3.0"},
+			"1.8.0":  {"v1.4.0", "v1.4.1"},
+		},
+	},
+	"empty-module": &Metadata{Versions: map[CVersion][]GoVersion{}},
+}
+
+// Set up test environment
+func setupTestEnv(t *testing.T, testData MetadataMap) (*metadataMgr, func()) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		json.NewEncoder(w).Encode(testData)
+	}))
+
+	originalURL := remoteMetadataURL
+	remoteMetadataURL = server.URL
+
+	tmpDir := t.TempDir()
+	mgr, err := NewMetadataMgr(tmpDir)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	cleanup := func() {
+		server.Close()
+		remoteMetadataURL = originalURL
+	}
+
+	return mgr, cleanup
+}
+
+// TestLatestCVer tests the implementation of the LatestCVer function
+func TestLatestCVer(t *testing.T) {
+	mgr, cleanup := setupTestEnv(t, enhancedTestVersionData)
+	defer cleanup()
+
+	// Test normal case
+	latestCVer, err := mgr.LatestCVer("test-module")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if latestCVer != "1.8.0" {
+		t.Errorf("Expected latestCVer '1.8.0', got '%s'", latestCVer)
+	}
+
+	// Test empty module
+	_, err = mgr.LatestCVer("empty-module")
+	if err == nil {
+		t.Fatal("Expected error for empty module")
+	}
+
+	// Test non-existent module
+	_, err = mgr.LatestCVer("non-existent-module")
+	if err == nil {
+		t.Fatal("Expected error for non-existent module")
+	}
+}
+
+// TestLatestGoVer tests the implementation of the LatestGoVer function
+func TestLatestGoVer(t *testing.T) {
+	mgr, cleanup := setupTestEnv(t, enhancedTestVersionData)
+	defer cleanup()
+
+	// Test normal case
+	latestGoVer, err := mgr.LatestGoVer("test-module")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if latestGoVer != "v1.4.1" {
+		t.Errorf("Expected latestGoVer 'v1.4.1', got '%s'", latestGoVer)
+	}
+
+	// Test empty module
+	_, err = mgr.LatestGoVer("empty-module")
+	if err == nil {
+		t.Fatal("Expected error for empty module")
+	}
+
+	// Test non-existent module
+	_, err = mgr.LatestGoVer("non-existent-module")
+	if err == nil {
+		t.Fatal("Expected error for non-existent module")
+	}
+}
+
+// TestLatestGoVerFromCVer tests the implementation of the LatestGoVerFromCVer function
+func TestLatestGoVerFromCVer(t *testing.T) {
+	mgr, cleanup := setupTestEnv(t, enhancedTestVersionData)
+	defer cleanup()
+
+	// Test normal case
+	latestGoVer, err := mgr.LatestGoVerFromCVer("test-module", "1.7.18")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if latestGoVer != "v1.2.1" {
+		t.Errorf("Expected latestGoVer 'v1.2.1', got '%s'", latestGoVer)
+	}
+
+	// Test non-existent C version
+	_, err = mgr.LatestGoVerFromCVer("test-module", "non-existent-version")
+	if err == nil {
+		t.Fatal("Expected error for non-existent C version")
+	}
+
+	// Test non-existent module
+	_, err = mgr.LatestGoVerFromCVer("non-existent-module", "1.7.18")
+	if err == nil {
+		t.Fatal("Expected error for non-existent module")
+	}
+}
+
+// TestGoVersFromCVer tests the implementation of the GoVersFromCVer function
+func TestGoVersFromCVer(t *testing.T) {
+	mgr, cleanup := setupTestEnv(t, enhancedTestVersionData)
+	defer cleanup()
+
+	// Test normal case
+	goVers, err := mgr.GoVersFromCVer("test-module", "1.7.18")
+	if err != nil {
+		t.Fatal(err)
+	}
+	expected := []string{"v1.2.0", "v1.2.1"}
+	if !reflect.DeepEqual(goVers, expected) {
+		t.Errorf("Expected %v, got %v", expected, goVers)
+	}
+
+	// Test non-existent C version
+	_, err = mgr.GoVersFromCVer("test-module", "non-existent-version")
+	if err == nil {
+		t.Fatal("Expected error for non-existent C version")
+	}
+
+	// Test non-existent module
+	_, err = mgr.GoVersFromCVer("non-existent-module", "1.7.18")
+	if err == nil {
+		t.Fatal("Expected error for non-existent module")
+	}
+}
+
+// TestCVerFromGoVer tests the implementation of the CVerFromGoVer function
+func TestCVerFromGoVer(t *testing.T) {
+	mgr, cleanup := setupTestEnv(t, enhancedTestVersionData)
+	defer cleanup()
+
+	// Test normal case
+	cVer, err := mgr.CVerFromGoVer("test-module", "v1.3.0")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if cVer != "1.7.19" {
+		t.Errorf("Expected C version '1.7.19', got '%s'", cVer)
+	}
+
+	// Test non-existent Go version
+	_, err = mgr.CVerFromGoVer("test-module", "non-existent-version")
+	if err == nil {
+		t.Fatal("Expected error for non-existent Go version")
+	}
+
+	// Test non-existent module
+	_, err = mgr.CVerFromGoVer("non-existent-module", "v1.3.0")
+	if err == nil {
+		t.Fatal("Expected error for non-existent module")
+	}
+}
+
+// TestAllGoVersFromName tests the implementation of the AllGoVersFromName function
+func TestAllGoVersFromName(t *testing.T) {
+	mgr, cleanup := setupTestEnv(t, enhancedTestVersionData)
+	defer cleanup()
+
+	// Test normal case
+	goVers, err := mgr.AllGoVersFromName("test-module")
+	if err != nil {
+		t.Fatal(err)
+	}
+	expected := []string{"v1.2.0", "v1.2.1", "v1.3.0", "v1.4.0", "v1.4.1"}
+	slices.Sort(goVers)
+	slices.Sort(expected)
+
+	if !slices.Equal(goVers, expected) {
+		t.Errorf("Expected %v, got %v", expected, goVers)
+	}
+
+	// Test empty module
+	goVers, err = mgr.AllGoVersFromName("empty-module")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(goVers) != 0 {
+		t.Errorf("Expected empty slice, got %v", goVers)
+	}
+
+	// Test non-existent module
+	_, err = mgr.AllGoVersFromName("non-existent-module")
+	if err == nil {
+		t.Fatal("Expected error for non-existent module")
+	}
+}
+
+// TestAllCVersFromName tests the implementation of the AllCVersFromName function
+func TestAllCVersFromName(t *testing.T) {
+	mgr, cleanup := setupTestEnv(t, enhancedTestVersionData)
+	defer cleanup()
+
+	// Test normal case
+	cVers, err := mgr.AllCVersFromName("test-module")
+	if err != nil {
+		t.Fatal(err)
+	}
+	expected := []string{"1.7.18", "1.7.19", "1.8.0"}
+	slices.Sort(cVers)
+	slices.Sort(expected)
+
+	if !slices.Equal(cVers, expected) {
+		t.Errorf("Expected %v, got %v", expected, cVers)
+	}
+
+	// Test empty module
+	cVers, err = mgr.AllCVersFromName("empty-module")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(cVers) != 0 {
+		t.Errorf("Expected empty slice, got %v", cVers)
+	}
+
+	// Test non-existent module
+	_, err = mgr.AllCVersFromName("non-existent-module")
+	if err == nil {
+		t.Fatal("Expected error for non-existent module")
+	}
+}


Reviews:
user: login: aofei
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T08:29:31Z
user: login: seri037
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T09:08:55Z
user: login: seri037
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T09:09:10Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request refactors the metadata structure by replacing the previous slice-based VersionMapping with a map-based structure and adjusts associated query functions and tests accordingly.  
- Refactor of metadata struct to simplify version mappings  
- Update of metadata and version query functions to work with the new structure  
- Updates to tests and removal of deprecated code

### Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                         | Description                                                           |
| -------------------------------------------- | --------------------------------------------------------------------- |
| metadata/version_test.go                     | Added comprehensive tests for updated metadata query functions        |
| metadata/version.go                          | Updated functions to use the new metadata map structure                 |
| metadata/metadata_test.go                    | Added tests for metadata manager, module existence, and error handling  |
| metadata/metadata.go                         | Refactored metadata struct to use a map instead of a slice               |
| metadata/cache_test.go                       | Verified cache functionality with updated metadata data                 |
| metadata/cache.go                            | Cache implementation remains stable and unchanged                       |
| internal/actions/versions/versions_test.go   | Updated tests to reflect the new metadata JSON structure                  |
| internal/actions/versions/versions.go        | Refactored version query methods to work with map-based version storage     |
| internal/actions/api_test.go                 | Updated API tests for legacy version formats with revised JSON structure  |
| internal/actions/versions/cver.go            | Removed deprecated cVerMap implementation                                |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**internal/actions/api_test.go:95**
* The JSON key for '1.7.16' is improperly formatted (missing a closing quote before the colon). Please correct it to "1.7.16": ["v1.1.0"].
```
    "1.7.16: ["v1.1.0"]"
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T01:18:55Z
user: login: aofei
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T01:21:31Z
user: login: MeteorsLiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T01:35:24Z
user: login: MeteorsLiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T01:47:21Z
user: login: aofei
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T02:44:20Z

Review comments:
diff_hunk: @@ -4,10 +4,5 @@ package metadata
 type MetadataMap map[string]*Metadata
 
 type Metadata struct {
-	VersionMappings []*VersionMapping `json:"versions"`
-}
-
-type VersionMapping struct {
-	CVersion   string   `json:"c"`
-	GoVersions []string `json:"go"`
+	VersionMappings map[string][]string `json:"versions"` // c_ver -> module_vers
path: metadata/metadata.go
user: login: aofei
body: 建议这个字段直接叫 `Versions`，已经足够清晰了。

如果你觉得用 `string` 语义不够明确，可以考虑使用 [type alias](https://go.dev/ref/spec#Alias_declarations)，比如：

```
type CVersion = string
type GoVersion = string

type Metadata struct {
  Versions map[CVersion][]GoVersion `json:"versions"`
}
```

这里面 `CVersion` 和 `GoVersion` 本质上还是 `string`，任何用法和 `string` 都完全一致，只是相当于它多了个名字而已。
created_at: 2025-03-24T08:29:30Z
author_association: MEMBER
diff_hunk: @@ -4,10 +4,5 @@ package metadata
 type MetadataMap map[string]*Metadata
 
 type Metadata struct {
-	VersionMappings []*VersionMapping `json:"versions"`
-}
-
-type VersionMapping struct {
-	CVersion   string   `json:"c"`
-	GoVersions []string `json:"go"`
+	VersionMappings map[string][]string `json:"versions"` // c_ver -> module_vers
path: metadata/metadata.go
user: login: seri037
body: 好的，我觉得使用类型别名会好一些
created_at: 2025-03-24T09:08:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,10 +4,5 @@ package metadata
 type MetadataMap map[string]*Metadata
 
 type Metadata struct {
-	VersionMappings []*VersionMapping `json:"versions"`
-}
-
-type VersionMapping struct {
-	CVersion   string   `json:"c"`
-	GoVersions []string `json:"go"`
+	VersionMappings map[string][]string `json:"versions"` // c_ver -> module_vers
path: metadata/metadata.go
user: login: seri037
body: done at https://github.com/goplus/llpkgstore/pull/13/commits/cd4b7799adae3ffae874471c8270446d50a506df
created_at: 2025-03-24T09:09:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,93 +47,91 @@ func Read(fileName string) *Versions {
 		json.Unmarshal(b, &m)
 	}
 
-	v := &Versions{
+	return &Versions{
 		MetadataMap: m,
 		fileName:    f.Name(),
-		cVerToGoVer: map[string]*cVerMap{},
 	}
-	v.build()
-	return v
 }
 
-// build constructs the cVerToGoVer map from the metadata
-func (v *Versions) build() {
-	// O(n)
-	for clib := range v.MetadataMap {
-		cverMap := newCverMap()
-
-		versions := v.MetadataMap[clib]
-		for _, version := range versions.VersionMappings {
-			cverMap.Set(version)
-		}
-
-		v.cVerToGoVer[clib] = cverMap
+func (v *Versions) cVersions(clib string) (ret []string) {
+	versions := v.MetadataMap[clib]
+	if versions == nil {
+		return
 	}
+	for version := range versions.Versions {
path: internal/actions/versions/versions.go
user: login: aofei
body: 看起来好像可以直接 `return versions.Versions` 的样子？这里 clone 一份是因为后续存在对返回值进行修改的可能吗？
created_at: 2025-03-28T01:21:31Z
author_association: MEMBER
diff_hunk: @@ -48,93 +47,91 @@ func Read(fileName string) *Versions {
 		json.Unmarshal(b, &m)
 	}
 
-	v := &Versions{
+	return &Versions{
 		MetadataMap: m,
 		fileName:    f.Name(),
-		cVerToGoVer: map[string]*cVerMap{},
 	}
-	v.build()
-	return v
 }
 
-// build constructs the cVerToGoVer map from the metadata
-func (v *Versions) build() {
-	// O(n)
-	for clib := range v.MetadataMap {
-		cverMap := newCverMap()
-
-		versions := v.MetadataMap[clib]
-		for _, version := range versions.VersionMappings {
-			cverMap.Set(version)
-		}
-
-		v.cVerToGoVer[clib] = cverMap
+func (v *Versions) cVersions(clib string) (ret []string) {
+	versions := v.MetadataMap[clib]
+	if versions == nil {
+		return
 	}
+	for version := range versions.Versions {
path: internal/actions/versions/versions.go
user: login: MeteorsLiu
body: Done in [67c92b1](https://github.com/goplus/llpkgstore/pull/13/commits/67c92b1920e04cf57f400386a489407c5d1370c7)
created_at: 2025-03-28T01:35:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,93 +47,91 @@ func Read(fileName string) *Versions {
 		json.Unmarshal(b, &m)
 	}
 
-	v := &Versions{
+	return &Versions{
 		MetadataMap: m,
 		fileName:    f.Name(),
-		cVerToGoVer: map[string]*cVerMap{},
 	}
-	v.build()
-	return v
 }
 
-// build constructs the cVerToGoVer map from the metadata
-func (v *Versions) build() {
-	// O(n)
-	for clib := range v.MetadataMap {
-		cverMap := newCverMap()
-
-		versions := v.MetadataMap[clib]
-		for _, version := range versions.VersionMappings {
-			cverMap.Set(version)
-		}
-
-		v.cVerToGoVer[clib] = cverMap
+func (v *Versions) cVersions(clib string) (ret []string) {
+	versions := v.MetadataMap[clib]
+	if versions == nil {
+		return
 	}
+	for version := range versions.Versions {
path: internal/actions/versions/versions.go
user: login: MeteorsLiu
body: CI fix: [47f9778](https://github.com/goplus/llpkgstore/pull/13/commits/47f9778b32cf65cc8e2f8350be13ec77d7a3733d)

Sort it to avoid order problem.
created_at: 2025-03-28T01:47:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-24T07:56:38Z
body: ## [Codecov](https://app.codecov.io/gh/goplus/llpkgstore/pull/13?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus) Report
Attention: Patch coverage is `77.74869%` with `85 lines` in your changes missing coverage. Please review.
> Project coverage is 37.96%. Comparing base [(`7c670be`)](https://app.codecov.io/gh/goplus/llpkgstore/commit/7c670bec31c6321148745e3088a19e8c5d1ea809?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus) to head [(`47f9778`)](https://app.codecov.io/gh/goplus/llpkgstore/commit/47f9778b32cf65cc8e2f8350be13ec77d7a3733d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/goplus/llpkgstore/pull/13?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus) | Patch % | Lines |
|---|---|---|
| [metadata/cache.go](https://app.codecov.io/gh/goplus/llpkgstore/pull/13?src=pr&el=tree&filepath=metadata%2Fcache.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus#diff-bWV0YWRhdGEvY2FjaGUuZ28=) | 70.64% | [22 Missing and 10 partials :warning: ](https://app.codecov.io/gh/goplus/llpkgstore/pull/13?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus) |
| [metadata/version.go](https://app.codecov.io/gh/goplus/llpkgstore/pull/13?src=pr&el=tree&filepath=metadata%2Fversion.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus#diff-bWV0YWRhdGEvdmVyc2lvbi5nbw==) | 80.43% | [19 Missing and 8 partials :warning: ](https://app.codecov.io/gh/goplus/llpkgstore/pull/13?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus) |
| [metadata/metadata.go](https://app.codecov.io/gh/goplus/llpkgstore/pull/13?src=pr&el=tree&filepath=metadata%2Fmetadata.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus#diff-bWV0YWRhdGEvbWV0YWRhdGEuZ28=) | 82.35% | [10 Missing and 5 partials :warning: ](https://app.codecov.io/gh/goplus/llpkgstore/pull/13?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus) |
| [internal/actions/versions/versions.go](https://app.codecov.io/gh/goplus/llpkgstore/pull/13?src=pr&el=tree&filepath=internal%2Factions%2Fversions%2Fversions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus#diff-aW50ZXJuYWwvYWN0aW9ucy92ZXJzaW9ucy92ZXJzaW9ucy5nbw==) | 78.00% | [6 Missing and 5 partials :warning: ](https://app.codecov.io/gh/goplus/llpkgstore/pull/13?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #13      +/-   ##
==========================================
+ Coverage   30.25%   37.96%   +7.70%     
==========================================
  Files          31       33       +2     
  Lines        1385     1657     +272     
==========================================
+ Hits          419      629     +210     
- Misses        906      950      +44     
- Partials       60       78      +18     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/goplus/llpkgstore/pull/13?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: aofei
created_at: 2025-03-24T10:37:35Z
body: 单测失败了需要处理下 https://github.com/goplus/llpkgstore/actions/runs/14031328047/job/39279437053?pr=13 @seri037 
author_association: MEMBER
user: login: MeteorsLiu
created_at: 2025-03-27T11:27:17Z
body: Mapping table for GitHub Action has been reconstructed.
author_association: CONTRIBUTOR
user: login: qiniu-x[bot]
created_at: 2025-03-28T02:02:26Z
body: **[Git-flow]** Hi @seri037, There are some suggestions for your information:

---


> 
### Rebase suggestions
* Following commits seems generated via `git merge` 
 
  > Merge branch 'goplus:main' into work/metadata_struct

  > Merge branch 'goplus:main' into work/metadata_struct
* Following commits have **duplicated** messages 
 
  > fix: ci 

  > fix: ci 

  > fix: ci 

  > Merge branch 'goplus:main' into work/metadata_struct 

  > Merge branch 'goplus:main' into work/metadata_struct 

 Which seems insignificant, recommend to use **`git rebase`** command to reorganize your PR. 



For other `git-flow` instructions, recommend refer to [these examples](https://github.com/qiniu/reviewbot/blob/master/docs/engineering-practice/git-flow-instructions_zh.md).


<details>

If you have any questions about this comment, feel free to raise an issue here:

- **https://github.com/qiniu/reviewbot**

</details>

author_association: CONTRIBUTOR