Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
971 changes: 181 additions & 790 deletions README.md

Large diffs are not rendered by default.

703 changes: 703 additions & 0 deletions pkg/resolvespec/README.md

Large diffs are not rendered by default.

445 changes: 445 additions & 0 deletions pkg/restheadspec/README.md

Large diffs are not rendered by default.

439 changes: 439 additions & 0 deletions pkg/server/staticweb/README.md

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions pkg/server/staticweb/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package staticweb

import (
"embed"
"fmt"

"github.com/bitechdev/ResolveSpec/pkg/server/staticweb/policies"
"github.com/bitechdev/ResolveSpec/pkg/server/staticweb/providers"
)

// ServiceConfig configures the static file service.
type ServiceConfig struct {
// DefaultCacheTime is the default cache duration in seconds.
// Used when a mount point doesn't specify a custom CachePolicy.
// Default: 172800 (48 hours)
DefaultCacheTime int

// DefaultMIMETypes is a map of file extensions to MIME types.
// These are added to the default MIME resolver.
// Extensions should include the leading dot (e.g., ".webp").
DefaultMIMETypes map[string]string
}

// DefaultServiceConfig returns a ServiceConfig with sensible defaults.
func DefaultServiceConfig() *ServiceConfig {
return &ServiceConfig{
DefaultCacheTime: 172800, // 48 hours
DefaultMIMETypes: make(map[string]string),
}
}

// Validate checks if the ServiceConfig is valid.
func (c *ServiceConfig) Validate() error {
if c.DefaultCacheTime < 0 {
return fmt.Errorf("DefaultCacheTime cannot be negative")
}
return nil
}

// Helper constructor functions for providers

// LocalProvider creates a FileSystemProvider for a local directory.
func LocalProvider(path string) (FileSystemProvider, error) {
return providers.NewLocalFSProvider(path)
}

// ZipProvider creates a FileSystemProvider for a zip file.
func ZipProvider(zipPath string) (FileSystemProvider, error) {
return providers.NewZipFSProvider(zipPath)
}

// EmbedProvider creates a FileSystemProvider for an embedded filesystem.
// If zipFile is empty, the embedded FS is used directly.
// If zipFile is specified, it's treated as a path to a zip file within the embedded FS.
// The embedFS parameter can be any fs.FS, but is typically *embed.FS.
func EmbedProvider(embedFS *embed.FS, zipFile string) (FileSystemProvider, error) {
return providers.NewEmbedFSProvider(embedFS, zipFile)
}

// Policy constructor functions

// SimpleCache creates a simple cache policy with the given TTL in seconds.
func SimpleCache(seconds int) CachePolicy {
return policies.NewSimpleCachePolicy(seconds)
}

// ExtensionCache creates an extension-based cache policy.
// rules maps file extensions (with leading dot) to cache times in seconds.
// defaultTime is used for files that don't match any rule.
func ExtensionCache(rules map[string]int, defaultTime int) CachePolicy {
return policies.NewExtensionBasedCachePolicy(rules, defaultTime)
}

// NoCache creates a cache policy that disables all caching.
func NoCache() CachePolicy {
return policies.NewNoCachePolicy()
}

// HTMLFallback creates a fallback strategy for SPAs that serves the given index file.
func HTMLFallback(indexFile string) FallbackStrategy {
return policies.NewHTMLFallbackStrategy(indexFile)
}

// ExtensionFallback creates an extension-based fallback strategy.
// staticExtensions is a list of file extensions that should NOT use fallback.
// fallbackPath is the file to serve when fallback is triggered.
func ExtensionFallback(staticExtensions []string, fallbackPath string) FallbackStrategy {
return policies.NewExtensionBasedFallback(staticExtensions, fallbackPath)
}

// DefaultExtensionFallback creates an extension-based fallback with common web asset extensions.
func DefaultExtensionFallback(fallbackPath string) FallbackStrategy {
return policies.NewDefaultExtensionBasedFallback(fallbackPath)
}

// DefaultMIMEResolver creates a MIME resolver with common web file types.
func DefaultMIMEResolver() MIMETypeResolver {
return policies.NewDefaultMIMEResolver()
}
60 changes: 60 additions & 0 deletions pkg/server/staticweb/example_reload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package staticweb_test

import (
"fmt"

"github.com/bitechdev/ResolveSpec/pkg/server/staticweb"
staticwebtesting "github.com/bitechdev/ResolveSpec/pkg/server/staticweb/testing"
)

// Example_reload demonstrates reloading content when files change.
func Example_reload() {
service := staticweb.NewService(nil)

// Create a provider
provider := staticwebtesting.NewMockProvider(map[string][]byte{
"version.txt": []byte("v1.0.0"),
})

service.Mount(staticweb.MountConfig{
URLPrefix: "/static",
Provider: provider,
})

// Simulate updating the file
provider.AddFile("version.txt", []byte("v2.0.0"))

// Reload to pick up changes (in real usage with zip files)
err := service.Reload()
if err != nil {
fmt.Printf("Failed to reload: %v\n", err)
} else {
fmt.Println("Successfully reloaded static files")
}

// Output: Successfully reloaded static files
}

// Example_reloadZip demonstrates reloading a zip file provider.
func Example_reloadZip() {
service := staticweb.NewService(nil)

// In production, you would use:
// provider, _ := staticweb.ZipProvider("./dist.zip")
// For this example, we use a mock
provider := staticwebtesting.NewMockProvider(map[string][]byte{
"app.js": []byte("console.log('v1')"),
})

service.Mount(staticweb.MountConfig{
URLPrefix: "/app",
Provider: provider,
})

fmt.Println("Serving from zip file")

// When the zip file is updated, call Reload()
// service.Reload()

// Output: Serving from zip file
}
138 changes: 138 additions & 0 deletions pkg/server/staticweb/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package staticweb_test

import (
"fmt"
"net/http"

"github.com/bitechdev/ResolveSpec/pkg/server/staticweb"
staticwebtesting "github.com/bitechdev/ResolveSpec/pkg/server/staticweb/testing"
"github.com/gorilla/mux"
)

// Example_basic demonstrates serving files from a local directory.
func Example_basic() {
service := staticweb.NewService(nil)

// Using mock provider for example purposes
provider := staticwebtesting.NewMockProvider(map[string][]byte{
"index.html": []byte("<html>test</html>"),
})

_ = service.Mount(staticweb.MountConfig{
URLPrefix: "/static",
Provider: provider,
})

router := mux.NewRouter()
router.PathPrefix("/").Handler(service.Handler())

fmt.Println("Serving files from ./public at /static")
// Output: Serving files from ./public at /static
}

// Example_spa demonstrates an SPA with HTML fallback routing.
func Example_spa() {
service := staticweb.NewService(nil)

// Using mock provider for example purposes
provider := staticwebtesting.NewMockProvider(map[string][]byte{
"index.html": []byte("<html>app</html>"),
})

_ = service.Mount(staticweb.MountConfig{
URLPrefix: "/",
Provider: provider,
FallbackStrategy: staticweb.HTMLFallback("index.html"),
})

router := mux.NewRouter()

// API routes take precedence
router.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("users"))
})

// Static files handle all other routes
router.PathPrefix("/").Handler(service.Handler())

fmt.Println("SPA with fallback to index.html")
// Output: SPA with fallback to index.html
}

// Example_multiple demonstrates multiple mount points with different policies.
func Example_multiple() {
service := staticweb.NewService(&staticweb.ServiceConfig{
DefaultCacheTime: 3600,
})

// Assets with long cache (using mock for example)
assetsProvider := staticwebtesting.NewMockProvider(map[string][]byte{
"app.js": []byte("console.log('test')"),
})
service.Mount(staticweb.MountConfig{
URLPrefix: "/assets",
Provider: assetsProvider,
CachePolicy: staticweb.SimpleCache(604800), // 1 week
})

// HTML with short cache (using mock for example)
htmlProvider := staticwebtesting.NewMockProvider(map[string][]byte{
"index.html": []byte("<html>test</html>"),
})
service.Mount(staticweb.MountConfig{
URLPrefix: "/",
Provider: htmlProvider,
CachePolicy: staticweb.SimpleCache(300), // 5 minutes
})

fmt.Println("Multiple mount points configured")
// Output: Multiple mount points configured
}

// Example_zip demonstrates serving from a zip file (concept).
func Example_zip() {
service := staticweb.NewService(nil)

// For actual usage, you would use:
// provider, err := staticweb.ZipProvider("./static.zip")
// For this example, we use a mock
provider := staticwebtesting.NewMockProvider(map[string][]byte{
"file.txt": []byte("content"),
})

service.Mount(staticweb.MountConfig{
URLPrefix: "/static",
Provider: provider,
})

fmt.Println("Serving from zip file")
// Output: Serving from zip file
}

// Example_extensionCache demonstrates extension-based caching.
func Example_extensionCache() {
service := staticweb.NewService(nil)

// Using mock provider for example purposes
provider := staticwebtesting.NewMockProvider(map[string][]byte{
"index.html": []byte("<html>test</html>"),
"app.js": []byte("console.log('test')"),
})

// Different cache times per file type
cacheRules := map[string]int{
".html": 3600, // 1 hour
".js": 86400, // 1 day
".css": 86400, // 1 day
".png": 604800, // 1 week
}

service.Mount(staticweb.MountConfig{
URLPrefix: "/",
Provider: provider,
CachePolicy: staticweb.ExtensionCache(cacheRules, 3600), // default 1 hour
})

fmt.Println("Extension-based caching configured")
// Output: Extension-based caching configured
}
Loading
Loading