Implement GCP Cloud Function
This commit is contained in:
parent
794b873ddd
commit
15d4b66afa
13 changed files with 242686 additions and 38 deletions
20
README.md
Normal file
20
README.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Puzzle Solver
|
||||
|
||||
Match patterns and solve anagrams - handy for crossword fanatics.
|
||||
|
||||
## Standalone Server
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## Cloud Function
|
||||
|
||||
To test using the Cloud Functions Framework:
|
||||
|
||||
```bash
|
||||
env FUNCTION_TARGET=WordSearch WORDLIST_BUCKET=word-search-1729-assets \
|
||||
WORDLIST_PATH=data/wordlist.txt LOCAL_ONLY=true go run cmd/main.go
|
||||
|
||||
curl 'http://localhost:8080?mode=anagrams&pattern=idea'
|
||||
```
|
|
@ -10,6 +10,18 @@ import (
|
|||
|
||||
type DB interface {
|
||||
FindAnagrams(s string) []string
|
||||
Add(s string)
|
||||
}
|
||||
|
||||
type HashDBImpl map[string][]string
|
||||
|
||||
func New() HashDBImpl {
|
||||
return make(HashDBImpl)
|
||||
}
|
||||
|
||||
func (db HashDBImpl) Add(s string) {
|
||||
k := toKey(s)
|
||||
db[k] = append(db[k], s)
|
||||
}
|
||||
|
||||
func toKey(s string) string {
|
||||
|
@ -18,15 +30,11 @@ func toKey(s string) string {
|
|||
return string(xs)
|
||||
}
|
||||
|
||||
type HashDBImpl map[string][]string
|
||||
|
||||
func Load(r io.Reader) (DB, error) {
|
||||
db := make(HashDBImpl)
|
||||
db := New()
|
||||
sc := bufio.NewScanner(r)
|
||||
for sc.Scan() {
|
||||
s := sc.Text()
|
||||
k := toKey(s)
|
||||
db[k] = append(db[k], s)
|
||||
db.Add(sc.Text())
|
||||
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
|
|
|
@ -3,10 +3,11 @@ div.center {
|
|||
}
|
||||
|
||||
div#results {
|
||||
overflow-y: auto;
|
||||
max-height: 70dvh;
|
||||
height: 60dvh;
|
||||
}
|
||||
|
||||
div#results > ul {
|
||||
list-style-type: none;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
141
cloudfn/cloudfn.go
Normal file
141
cloudfn/cloudfn.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package cloudfn
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"text/template"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
"github.com/GoogleCloudPlatform/functions-framework-go/functions"
|
||||
"github.com/ray1729/puzzle-solver/anagram"
|
||||
"github.com/ray1729/puzzle-solver/match"
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
var anagramDB anagram.DB
|
||||
var matchDB match.DB
|
||||
|
||||
func initializeDB(ctx context.Context, bucketName, objectName string) error {
|
||||
client, err := storage.NewClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating storage client: %v", err)
|
||||
}
|
||||
r, err := client.Bucket(bucketName).Object(objectName).NewReader(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening gs://%s/%s: %v", bucketName, objectName, err)
|
||||
}
|
||||
defer r.Close()
|
||||
anagramDB = anagram.New()
|
||||
matchDB = match.New()
|
||||
sc := bufio.NewScanner(r)
|
||||
for sc.Scan() {
|
||||
s := sc.Text()
|
||||
anagramDB.Add(s)
|
||||
matchDB.Add(s)
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
return fmt.Errorf("error reading gs://%s/%s: %v", bucketName, objectName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
bucketName := mustGetenv("WORDLIST_BUCKET")
|
||||
objectName := mustGetenv("WORDLIST_PATH")
|
||||
log.Println("Initializing databases")
|
||||
if err := initializeDB(ctx, bucketName, objectName); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var corsHandler = cors.Default()
|
||||
log.Println("Registering HTTP function with the Functions Framework")
|
||||
functions.HTTP("WordSearch", func(w http.ResponseWriter, r *http.Request) {
|
||||
corsHandler.ServeHTTP(w, r, handleFormSubmission)
|
||||
})
|
||||
}
|
||||
|
||||
func mustGetenv(s string) string {
|
||||
v := os.Getenv(s)
|
||||
if len(v) == 0 {
|
||||
panic(fmt.Sprintf("environment variable %s not set", s))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func handleFormSubmission(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
log.Printf("error parsing form: %v", err)
|
||||
http.Error(w, "error parsing form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mode := r.Form.Get("mode")
|
||||
pattern := r.Form.Get("pattern")
|
||||
if len(pattern) == 0 {
|
||||
http.Error(w, "Missing pattern", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
switch mode {
|
||||
case "match":
|
||||
results := matchResults(matchDB, pattern)
|
||||
renderTemplate(w, resultsTmpl, results)
|
||||
case "anagrams":
|
||||
results := anagramResults(anagramDB, pattern)
|
||||
renderTemplate(w, resultsTmpl, results)
|
||||
default:
|
||||
log.Printf("invalid mode: %s", mode)
|
||||
http.Error(w, fmt.Sprintf("Invalid mode: %s", mode), http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func anagramResults(db anagram.DB, pattern string) ResultParams {
|
||||
var params ResultParams
|
||||
params.Results = db.FindAnagrams(pattern)
|
||||
if len(params.Results) > 0 {
|
||||
params.Preamble = fmt.Sprintf("Anagrams of %q:", pattern)
|
||||
} else {
|
||||
params.Preamble = fmt.Sprintf("Found no anagrams of %q", pattern)
|
||||
}
|
||||
sort.Slice(params.Results, func(i, j int) bool { return params.Results[i] < params.Results[j] })
|
||||
return params
|
||||
}
|
||||
|
||||
func matchResults(db match.DB, pattern string) ResultParams {
|
||||
var params ResultParams
|
||||
params.Results = db.FindMatches(pattern)
|
||||
if len(params.Results) > 0 {
|
||||
params.Preamble = fmt.Sprintf("Matches for %q:", pattern)
|
||||
} else {
|
||||
params.Preamble = fmt.Sprintf("Found no matches for %q", pattern)
|
||||
}
|
||||
sort.Slice(params.Results, func(i, j int) bool { return params.Results[i] < params.Results[j] })
|
||||
return params
|
||||
}
|
||||
|
||||
func renderTemplate(w http.ResponseWriter, t *template.Template, params any) {
|
||||
err := t.Execute(w, params)
|
||||
if err != nil {
|
||||
log.Printf("Error rendering template %s: %v", t.Name(), err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
type ResultParams struct {
|
||||
Preamble string
|
||||
Results []string
|
||||
}
|
||||
|
||||
var resultsTmpl = template.Must(template.New("results").Parse(`
|
||||
{{ with .Preamble }}
|
||||
<p>{{ . }}</p>
|
||||
{{ end }}
|
||||
<ul>
|
||||
{{ range .Results }}
|
||||
<li>{{.}}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
`))
|
29
cmd/main.go
Normal file
29
cmd/main.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
// Blank-import the function package so the init() runs
|
||||
"github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
|
||||
_ "github.com/ray1729/puzzle-solver/cloudfn"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Use PORT environment variable, or default to 8080.
|
||||
port := "8080"
|
||||
if envPort := os.Getenv("PORT"); envPort != "" {
|
||||
port = envPort
|
||||
}
|
||||
|
||||
// By default, listen on all interfaces. If testing locally, run with
|
||||
// LOCAL_ONLY=true to avoid triggering firewall warnings and
|
||||
// exposing the server outside of your own machine.
|
||||
hostname := ""
|
||||
if localOnly := os.Getenv("LOCAL_ONLY"); localOnly == "true" {
|
||||
hostname = "127.0.0.1"
|
||||
}
|
||||
if err := funcframework.StartHostPort(hostname, port); err != nil {
|
||||
log.Fatalf("funcframework.StartHostPort: %v\n", err)
|
||||
}
|
||||
}
|
25
fix-wordlist/main.go
Normal file
25
fix-wordlist/main.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dec := charmap.ISO8859_1.NewDecoder()
|
||||
sc := bufio.NewScanner(dec.Reader(os.Stdin))
|
||||
for sc.Scan() {
|
||||
s := strings.TrimSpace(sc.Text())
|
||||
if len(s) > 0 {
|
||||
fmt.Println(s)
|
||||
}
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
42
go.mod
42
go.mod
|
@ -2,4 +2,44 @@ module github.com/ray1729/puzzle-solver
|
|||
|
||||
go 1.21.1
|
||||
|
||||
require golang.org/x/text v0.13.0 // indirect
|
||||
require (
|
||||
cloud.google.com/go/storage v1.33.0
|
||||
github.com/GoogleCloudPlatform/functions-framework-go v1.8.0
|
||||
github.com/rs/cors v1.10.1
|
||||
golang.org/x/text v0.13.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.4 // indirect
|
||||
cloud.google.com/go/compute v1.20.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/functions v1.15.1 // indirect
|
||||
cloud.google.com/go/iam v1.1.0 // indirect
|
||||
github.com/cloudevents/sdk-go/v2 v2.14.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/s2a-go v0.1.4 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/json-iterator/go v1.1.10 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.uber.org/atomic v1.4.0 // indirect
|
||||
go.uber.org/multierr v1.1.0 // indirect
|
||||
go.uber.org/zap v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/api v0.132.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
|
||||
google.golang.org/grpc v1.56.2 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
)
|
||||
|
|
25
main.go
25
main.go
|
@ -1,37 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/ray1729/puzzle-solver/anagram"
|
||||
"github.com/ray1729/puzzle-solver/grep"
|
||||
"github.com/ray1729/puzzle-solver/match"
|
||||
"github.com/ray1729/puzzle-solver/server"
|
||||
)
|
||||
|
||||
var grepDB grep.DB
|
||||
var matchDB match.DB
|
||||
var anagramDB anagram.DB
|
||||
|
||||
func init() {
|
||||
f, err := os.Open("/usr/share/dict/british-english-huge")
|
||||
f, err := os.Open("wordlist.txt")
|
||||
if err != nil {
|
||||
log.Fatalf("Error opening word list: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
grepDB, err = grep.Load(f)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading grep database: %v", err)
|
||||
matchDB = match.New()
|
||||
anagramDB = anagram.New()
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
s := sc.Text()
|
||||
matchDB.Add(s)
|
||||
anagramDB.Add(s)
|
||||
}
|
||||
f.Seek(0, 0)
|
||||
anagramDB, err = anagram.Load(f)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading anagram database: %v", err)
|
||||
if err := sc.Err(); err != nil {
|
||||
log.Fatalf("Error loading databases: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
s := server.New("./assets", grepDB, anagramDB)
|
||||
s := server.New("./assets", matchDB, anagramDB)
|
||||
address := ":8000"
|
||||
log.Printf("Listening on %s", address)
|
||||
if err := http.ListenAndServe(address, s); err != nil {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package grep
|
||||
package match
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
@ -9,6 +9,20 @@ import (
|
|||
|
||||
type DB interface {
|
||||
FindMatches(s string) []string
|
||||
Add(s string)
|
||||
}
|
||||
|
||||
type PrefixTreeImpl struct {
|
||||
Root *Node
|
||||
}
|
||||
|
||||
func New() PrefixTreeImpl {
|
||||
return PrefixTreeImpl{Root: &Node{}}
|
||||
}
|
||||
|
||||
func (db PrefixTreeImpl) Add(s string) {
|
||||
xs := util.LowerCaseAlpha(s)
|
||||
db.Root.add(xs, s)
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
|
@ -17,7 +31,7 @@ type Node struct {
|
|||
Results []string
|
||||
}
|
||||
|
||||
func (n *Node) Add(xs []byte, s string) {
|
||||
func (n *Node) add(xs []byte, s string) {
|
||||
if len(xs) == 0 {
|
||||
n.Results = append(n.Results, s)
|
||||
return
|
||||
|
@ -34,16 +48,14 @@ func (n *Node) Add(xs []byte, s string) {
|
|||
child = &Node{Value: x}
|
||||
n.Children = append(n.Children, child)
|
||||
}
|
||||
child.Add(xs[1:], s)
|
||||
child.add(xs[1:], s)
|
||||
}
|
||||
|
||||
func Load(r io.Reader) (DB, error) {
|
||||
db := &Node{}
|
||||
db := New()
|
||||
sc := bufio.NewScanner(r)
|
||||
for sc.Scan() {
|
||||
s := sc.Text()
|
||||
xs := util.LowerCaseAlpha(s)
|
||||
db.Add(xs, s)
|
||||
db.Add(sc.Text())
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
return nil, err
|
||||
|
@ -51,8 +63,8 @@ func Load(r io.Reader) (DB, error) {
|
|||
return db, nil
|
||||
}
|
||||
|
||||
func (n *Node) FindMatches(s string) []string {
|
||||
return n.find(util.LowerCaseAlphaOrDot(s))
|
||||
func (db PrefixTreeImpl) FindMatches(s string) []string {
|
||||
return db.Root.find(util.LowerCaseAlphaOrDot(s))
|
||||
}
|
||||
|
||||
func (n *Node) find(xs []byte) []string {
|
|
@ -8,13 +8,16 @@ import (
|
|||
"sort"
|
||||
|
||||
"github.com/ray1729/puzzle-solver/anagram"
|
||||
"github.com/ray1729/puzzle-solver/grep"
|
||||
"github.com/ray1729/puzzle-solver/match"
|
||||
)
|
||||
|
||||
func New(assetsPath string, grepDB grep.DB, anagramDB anagram.DB) http.Handler {
|
||||
func New(assetsPath string, matchDB match.DB, anagramDB anagram.DB) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir(assetsPath))))
|
||||
mux.HandleFunc("/", handler(grepDB, anagramDB))
|
||||
mux.HandleFunc("/search", searchHandler(matchDB, anagramDB))
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
renderTemplate(w, home, nil)
|
||||
})
|
||||
return withRequestLogger(mux)
|
||||
}
|
||||
|
||||
|
@ -25,7 +28,7 @@ func withRequestLogger(h http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func handler(grepDB grep.DB, anagramDB anagram.DB) func(w http.ResponseWriter, r *http.Request) {
|
||||
func searchHandler(matchDB match.DB, anagramDB anagram.DB) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
log.Printf("error parsing form: %v", err)
|
||||
|
@ -34,13 +37,13 @@ func handler(grepDB grep.DB, anagramDB anagram.DB) func(w http.ResponseWriter, r
|
|||
}
|
||||
switch r.Form.Get("mode") {
|
||||
case "match":
|
||||
params := matchResults(grepDB, r.Form.Get("pattern"))
|
||||
params := matchResults(matchDB, r.Form.Get("pattern"))
|
||||
renderTemplate(w, results, params)
|
||||
case "anagrams":
|
||||
params := anagramResults(anagramDB, r.Form.Get("pattern"))
|
||||
renderTemplate(w, results, params)
|
||||
default:
|
||||
renderTemplate(w, home, nil)
|
||||
renderTemplate(w, results, ResultParams{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +60,7 @@ func anagramResults(db anagram.DB, pattern string) ResultParams {
|
|||
return params
|
||||
}
|
||||
|
||||
func matchResults(db grep.DB, pattern string) ResultParams {
|
||||
func matchResults(db match.DB, pattern string) ResultParams {
|
||||
var params ResultParams
|
||||
params.Results = db.FindMatches(pattern)
|
||||
if len(params.Results) > 0 {
|
||||
|
|
|
@ -19,11 +19,12 @@ var home = template.Must(template.New("home").Parse(`
|
|||
</header>
|
||||
|
||||
<main>
|
||||
<form action="/" method="get" hx-boost="true" hx-target="#results" hx-replace="innerHTML" hx-on::after-request="this.reset()">
|
||||
<form action="/search" method="post" hx-boost="true" hx-target="#results">
|
||||
<div class="center">
|
||||
<input type="text" name="pattern" required></input>
|
||||
<button name="mode" value="match">Match</button>
|
||||
<button name="mode" value="anagrams">Anagrams</button>
|
||||
<button type="reset">Clear</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="results">
|
||||
|
|
240732
wordlist.txt
Normal file
240732
wordlist.txt
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue