Initial commit
commit
15bc406494
@ -0,0 +1,8 @@
|
|||||||
|
.env
|
||||||
|
*.local*
|
||||||
|
bin/
|
||||||
|
|
||||||
|
.out/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: frontend backend
|
||||||
|
|
||||||
|
.PHONY: frontend
|
||||||
|
frontend:
|
||||||
|
$(NPM) run build
|
||||||
|
|
||||||
|
.PHONY: backend
|
||||||
|
backend:
|
||||||
|
go build -v -o ./out/backend/server ./cmd/server
|
@ -0,0 +1,32 @@
|
|||||||
|
# PHC Website
|
||||||
|
|
||||||
|
Sito web del PHC
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Come usare questo progetto
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
TODO: ...
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ todo ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
TODO: ...
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ todo ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
|
||||||
|
TODO: ...
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ todo ...
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
publicDir: './frontend/public',
|
||||||
|
srcDir: './frontend',
|
||||||
|
outDir: './out/frontend',
|
||||||
|
})
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from './PageLayout.astro'
|
||||||
|
|
||||||
|
const { frontmatter } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout {...frontmatter} />
|
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
const { title, description, thumbnail } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<meta property="og:title" content={title ?? 'PHC'} />
|
||||||
|
<meta property="og:description" content={description ?? 'Sito web del PHC'} />
|
||||||
|
{thumbnail && <meta property="og:image" content={thumbnail} />}
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" sizes="512x512" href="/assets/icon.png" />
|
||||||
|
|
||||||
|
<title>{title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from './BaseLayout.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout {...Astro.props}>
|
||||||
|
<nav>
|
||||||
|
<div class="nav-item">Item 1</div>
|
||||||
|
<div class="nav-item">Item 2</div>
|
||||||
|
<div class="nav-item">Item 3</div>
|
||||||
|
<div class="nav-item">Item 4</div>
|
||||||
|
<div class="nav-item">Item 5</div>
|
||||||
|
<div class="nav-item">Item 6</div>
|
||||||
|
</nav>
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout>
|
||||||
|
<h1>Homepage</h1>
|
||||||
|
</BaseLayout>
|
@ -0,0 +1,72 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/config"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
l := sl.New()
|
||||||
|
|
||||||
|
cfg := sl.InjectValue(l, config.Slot, config.Config{
|
||||||
|
Mode: "development",
|
||||||
|
Host: ":4000",
|
||||||
|
})
|
||||||
|
|
||||||
|
sl.InjectValue[database.Database](l, database.Slot, &database.Memory{
|
||||||
|
Users: []model.User{
|
||||||
|
{
|
||||||
|
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
||||||
|
Username: "claire-doe",
|
||||||
|
FullName: db.NewOption("Claire Doe"),
|
||||||
|
Email: db.NewOption("claire.doe@example.org"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
||||||
|
Username: "john-smith",
|
||||||
|
FullName: db.NewOption("John Smith"),
|
||||||
|
Email: db.NewOption("john.smith@example.org"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
srv, err := server.Configure(l)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Fatal(srv.Router.Listen(cfg.Host))
|
||||||
|
}()
|
||||||
|
|
||||||
|
r, w := io.Pipe()
|
||||||
|
|
||||||
|
cmd := exec.Command("npm", "run", "dev")
|
||||||
|
cmd.Stdout = w
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
log.Printf(`[cmd/devserver] [vitejs] %s`, scanner.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/config"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
l := sl.New()
|
||||||
|
|
||||||
|
cfg := sl.InjectValue(l, config.Slot, config.Config{
|
||||||
|
Mode: "production",
|
||||||
|
Host: ":4000",
|
||||||
|
})
|
||||||
|
|
||||||
|
sl.InjectValue[database.Database](l, database.Slot, &database.Memory{
|
||||||
|
Users: []model.User{
|
||||||
|
{
|
||||||
|
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
||||||
|
Username: "claire-doe",
|
||||||
|
FullName: db.NewOption("Claire Doe"),
|
||||||
|
Email: db.NewOption("claire.doe@example.org"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
||||||
|
Username: "john-smith",
|
||||||
|
FullName: db.NewOption("John Smith"),
|
||||||
|
Email: db.NewOption("john.smith@example.org"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
srv, err := server.Configure(l)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Fatal(srv.Router.Listen(cfg.Host))
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
module git.phc.dm.unipi.it/phc/website
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/repr v0.2.0 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
|
github.com/gofiber/fiber/v2 v2.44.0 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
github.com/klauspost/compress v1.16.5 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
|
github.com/philhofer/fwd v1.1.2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
|
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||||
|
github.com/tinylib/msgp v1.1.8 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.46.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/sys v0.7.0 // indirect
|
||||||
|
gotest.tools v2.2.0+incompatible // indirect
|
||||||
|
)
|
@ -0,0 +1,91 @@
|
|||||||
|
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||||
|
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/gofiber/fiber/v2 v2.44.0 h1:Z90bEvPcJM5GFJnu1py0E1ojoerkyew3iiNJ78MQCM8=
|
||||||
|
github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||||
|
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||||
|
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
|
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||||
|
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
|
||||||
|
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||||
|
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
||||||
|
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||||
|
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.46.0 h1:6ZRhrFg8zBXTRYY6vdzbFhqsBd7FVv123pV2m9V87U4=
|
||||||
|
github.com/valyala/fasthttp v1.46.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||||
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
@ -0,0 +1,224 @@
|
|||||||
|
// This package provides a small framework to work with SQL databases using
|
||||||
|
// generics. The most important part for now is the "Ref[T]" type that lets
|
||||||
|
// us pass around typed IDs to table rows in a type safe manner.
|
||||||
|
//
|
||||||
|
// For now this library uses both generics for keeping the interface type safe
|
||||||
|
// and reflection to extract automatically some metedata from structs that
|
||||||
|
// represent tables.
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option è un valore che nel database può potenzialmente essere "NULL"
|
||||||
|
//
|
||||||
|
// NOTE/TODO: Magari si può fare una cosa fatta bene e nascondere
|
||||||
|
// l'implementazione al Go, ad esempio Option[string] può essere NULL o testo
|
||||||
|
// nel db mentre Option[time.Time] può essere codificato in json come
|
||||||
|
//
|
||||||
|
// "{ present: false }"
|
||||||
|
//
|
||||||
|
// oppure
|
||||||
|
//
|
||||||
|
// "{ present: true, value: '2023/04/...' }"
|
||||||
|
//
|
||||||
|
// in modo da semplificare leggermente la processazione in Go. Altrimenti si
|
||||||
|
// può separare in Option[T] e Nullable[T].
|
||||||
|
type Option[T any] struct {
|
||||||
|
present bool
|
||||||
|
value T
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOption[T any](value T) Option[T] {
|
||||||
|
return Option[T]{true, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEmptyOption[T any]() Option[T] {
|
||||||
|
return Option[T]{present: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Option[T]) SqlType() string {
|
||||||
|
return "BLOB"
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (v *Option[T]) Scan(a any) error {
|
||||||
|
// data, ok := a.([]byte)
|
||||||
|
// if !ok {
|
||||||
|
// return fmt.Errorf(`scan expected []byte`)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// m := map[string]any{}
|
||||||
|
// if err := json.Unmarshal(data, m); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if present, _ := m["present"].(bool); present {
|
||||||
|
|
||||||
|
// m["value"]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Ref è un id tipato da un'entità presente nel database. È il tipo fondamentale
|
||||||
|
// esportato da questo package. Non dovrebbe essere troppo importante ma
|
||||||
|
// internamente è una stringa, inoltre è consigliato che una struct
|
||||||
|
// con _primary key_ abbia un campo di tipo Ref a se stessa.
|
||||||
|
//
|
||||||
|
// type User struct {
|
||||||
|
// Id db.Ref[User] // meglio se unico e immutabile
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
type Ref[T any] string
|
||||||
|
|
||||||
|
func (Ref[T]) SqlType() string {
|
||||||
|
return "TEXT"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Ref[T]) Scan(a any) error {
|
||||||
|
s, ok := a.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf(`scan expected string`)
|
||||||
|
}
|
||||||
|
|
||||||
|
*v = Ref[T](s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sqlValue interface {
|
||||||
|
SqlType() string
|
||||||
|
sql.Scanner
|
||||||
|
}
|
||||||
|
|
||||||
|
type Table[T any] struct {
|
||||||
|
Name string
|
||||||
|
PrimaryKey func(value *T, pk ...string) Ref[T]
|
||||||
|
Columns [][2]string
|
||||||
|
}
|
||||||
|
|
||||||
|
var commonTypes = map[string]string{
|
||||||
|
"string": "TEXT",
|
||||||
|
"int": "INTEGER",
|
||||||
|
"int32": "INTEGER",
|
||||||
|
"int64": "INTEGER",
|
||||||
|
"float32": "REAL",
|
||||||
|
"float64": "REAL",
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSqlType(typ reflect.Type) string {
|
||||||
|
st, ok := reflect.New(typ).Interface().(sqlValue)
|
||||||
|
if ok {
|
||||||
|
return st.SqlType()
|
||||||
|
}
|
||||||
|
|
||||||
|
return commonTypes[typ.Name()]
|
||||||
|
}
|
||||||
|
|
||||||
|
func AutoTable[T any](name string) Table[T] {
|
||||||
|
columns := [][2]string{}
|
||||||
|
pkIndex := -1
|
||||||
|
|
||||||
|
var zero T
|
||||||
|
typ := reflect.TypeOf(zero)
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
fieldTyp := typ.Field(i)
|
||||||
|
isPK, _, typ, col := fieldInfo(fieldTyp)
|
||||||
|
if isPK {
|
||||||
|
pkIndex = i
|
||||||
|
}
|
||||||
|
columns = append(columns, [2]string{col, toSqlType(typ)})
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkIndex == -1 {
|
||||||
|
panic(fmt.Sprintf("struct %T has no primary key field", zero))
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug logging
|
||||||
|
// log.Printf("[auto table] struct %T has primary key %q", zero, typ.Field(pkIndex).Name)
|
||||||
|
|
||||||
|
return Table[T]{
|
||||||
|
Name: name,
|
||||||
|
PrimaryKey: func(value *T, pk ...string) Ref[T] {
|
||||||
|
// SAFETY: this is required to cast *Ref[T] to *string, internally
|
||||||
|
// they are the same type so this should be safe
|
||||||
|
ptr := reflect.ValueOf(value).Elem().Field(pkIndex).Addr().UnsafePointer()
|
||||||
|
pkPtr := (*string)(ptr)
|
||||||
|
|
||||||
|
if len(pk) > 0 {
|
||||||
|
*pkPtr = pk[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ref[T](*pkPtr)
|
||||||
|
},
|
||||||
|
Columns: columns,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table Table[T]) CreateIfNotExists(dbConn *sql.DB) error {
|
||||||
|
columns := make([]string, len(table.Columns))
|
||||||
|
|
||||||
|
for i, col := range table.Columns {
|
||||||
|
columns[i] = fmt.Sprintf("%s %s", col[0], col[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s(%s)`,
|
||||||
|
table.Name,
|
||||||
|
strings.Join(columns, ", "),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := dbConn.Exec(stmt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create[T any](dbConn *sql.DB, table Table[T], value T) (Ref[T], error) {
|
||||||
|
id := randomHex(15)
|
||||||
|
|
||||||
|
table.PrimaryKey(&value, id)
|
||||||
|
|
||||||
|
columns, values := structDatabaseColumnValues(&value)
|
||||||
|
|
||||||
|
stmt := fmt.Sprintf(`INSERT INTO %s(%s) VALUES (%s)`,
|
||||||
|
table.Name,
|
||||||
|
strings.Join(columns, ", "),
|
||||||
|
strings.Join(repeatSlice("?", len(columns)), ", "),
|
||||||
|
)
|
||||||
|
|
||||||
|
if _, err := dbConn.Exec(stmt, values...); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ref[T](id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Insert[T any](dbConn *sql.DB, table Table[T], value T) error {
|
||||||
|
columns, values := structDatabaseColumnValues(&value)
|
||||||
|
|
||||||
|
stmt := fmt.Sprintf(`INSERT INTO %s(%s) VALUES (%s)`,
|
||||||
|
table.Name,
|
||||||
|
strings.Join(columns, ", "),
|
||||||
|
strings.Join(repeatSlice("?", len(columns)), ", "),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := dbConn.Exec(stmt, values...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Read[T any](dbConn *sql.DB, table Table[T], id Ref[T]) (T, error) {
|
||||||
|
panic("todo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadAll[T any](dbConn *sql.DB, table Table[T]) ([]T, error) {
|
||||||
|
panic("todo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Update[T any](dbConn *sql.DB, table Table[T], id Ref[T], value T) error {
|
||||||
|
panic("todo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Delete[T any](dbConn *sql.DB, table Table[T], id Ref[T]) error {
|
||||||
|
panic("todo")
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
package db_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockDB struct {
|
||||||
|
*testing.T
|
||||||
|
|
||||||
|
LastQuery string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *mockDB) Prepare(query string) (driver.Stmt, error) { return nil, nil }
|
||||||
|
func (db *mockDB) Close() error { return nil }
|
||||||
|
func (db *mockDB) Begin() (driver.Tx, error) { return nil, nil }
|
||||||
|
|
||||||
|
func (db *mockDB) Query(query string, args []driver.Value) (driver.Rows, error) {
|
||||||
|
db.LastQuery = query
|
||||||
|
db.Logf("[sql mock driver] [exec] query: '%s', args: %#v", query, args)
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *mockDB) Exec(query string, args []driver.Value) (driver.Result, error) {
|
||||||
|
db.LastQuery = query
|
||||||
|
db.Logf("[sql mock driver] [exec] query: '%s', args: %#v", query, args)
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *mockDB) Open(name string) (driver.Conn, error) {
|
||||||
|
return &mockDB{T: db.T}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *mockDB) Connect(context.Context) (driver.Conn, error) {
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *mockDB) Driver() driver.Driver {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Tests
|
||||||
|
//
|
||||||
|
|
||||||
|
func TestDb1(t *testing.T) {
|
||||||
|
type Product struct {
|
||||||
|
Id db.Ref[Product] `db:"id*"`
|
||||||
|
Title string `db:"title"`
|
||||||
|
Quantity int `db:"quantity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
products := db.AutoTable[Product]("products")
|
||||||
|
|
||||||
|
mock := &mockDB{T: t}
|
||||||
|
conn := sql.OpenDB(mock)
|
||||||
|
|
||||||
|
err := products.CreateIfNotExists(conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, mock.LastQuery,
|
||||||
|
`CREATE TABLE IF NOT EXISTS products(id TEXT, title TEXT, quantity INTEGER)`,
|
||||||
|
)
|
||||||
|
|
||||||
|
ref1, err := db.Create(conn, products, Product{Title: "Foo", Quantity: 27})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Assert(t, string(ref1) != "")
|
||||||
|
assert.Equal(t, mock.LastQuery,
|
||||||
|
`INSERT INTO products(id, title, quantity) VALUES (?, ?, ?)`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := db.Insert(conn, products, Product{Id: "123", Title: "Bar", Quantity: 42}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, mock.LastQuery,
|
||||||
|
`INSERT INTO products(id, title, quantity) VALUES (?, ?, ?)`,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
package db
|
@ -0,0 +1,118 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func randomHex(len int) string {
|
||||||
|
buff := make([]byte, len/2+1)
|
||||||
|
rand.Read(buff)
|
||||||
|
str := hex.EncodeToString(buff)
|
||||||
|
return str[:len]
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldInfo(fieldTyp reflect.StructField) (bool, string, reflect.Type, string) {
|
||||||
|
fieldName := fieldTyp.Name
|
||||||
|
columnName, ok := fieldTyp.Tag.Lookup("db")
|
||||||
|
if !ok {
|
||||||
|
columnName = strings.ToLower(fieldName)
|
||||||
|
}
|
||||||
|
if ok && columnName[len(columnName)-1] == '*' {
|
||||||
|
return true, fieldName, fieldTyp.Type, columnName[:len(columnName)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fieldName, fieldTyp.Type, columnName
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldPrimaryKey(fieldTyp reflect.StructField) bool {
|
||||||
|
isPK, _, _, _ := fieldInfo(fieldTyp)
|
||||||
|
return isPK
|
||||||
|
}
|
||||||
|
|
||||||
|
// structDatabaseColumnValues takes a pointer to a struct and returns a pair
|
||||||
|
// of slices, one with the column names and the other with pointers to all
|
||||||
|
// its exported fields.
|
||||||
|
//
|
||||||
|
// The column name can be changed using the "db" tag attribute on that field
|
||||||
|
func structDatabaseColumnValues(s any) ([]string, []any) {
|
||||||
|
v := reflect.ValueOf(s).Elem()
|
||||||
|
|
||||||
|
numFields := v.NumField()
|
||||||
|
|
||||||
|
names := []string{}
|
||||||
|
values := []any{}
|
||||||
|
|
||||||
|
for i := 0; i < numFields; i++ {
|
||||||
|
fieldTyp := v.Type().Field(i)
|
||||||
|
fieldVal := v.Field(i)
|
||||||
|
|
||||||
|
if fieldTyp.IsExported() {
|
||||||
|
key := strings.ToLower(fieldTyp.Name)
|
||||||
|
|
||||||
|
if name, ok := fieldTyp.Tag.Lookup("db"); ok {
|
||||||
|
key = name
|
||||||
|
|
||||||
|
// primary key has a "*" at the end, exclude from column name
|
||||||
|
if key[len(key)-1] == '*' {
|
||||||
|
key = key[:len(key)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
names = append(names, key)
|
||||||
|
values = append(values, fieldVal.Addr().Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names, values
|
||||||
|
}
|
||||||
|
|
||||||
|
func structPrimaryKeyPtr(s any) (*string, bool) {
|
||||||
|
v := reflect.ValueOf(s).Elem()
|
||||||
|
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
fieldTyp := v.Type().Field(i)
|
||||||
|
fieldVal := v.Field(i)
|
||||||
|
|
||||||
|
if fieldTyp.IsExported() {
|
||||||
|
if fieldPrimaryKey(fieldTyp) {
|
||||||
|
// SAFETY: this is required to cast *Ref[T] to *string, internally
|
||||||
|
// they are the same type so this should be safe
|
||||||
|
ptr := fieldVal.Addr().UnsafePointer()
|
||||||
|
return (*string)(ptr), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// structDatabasePrimaryKeyColumn takes a pointer to a struct and returns the
|
||||||
|
// name of the field with the primary key
|
||||||
|
func structDatabasePrimaryKeyColumn(s any) (field, column string, ok bool) {
|
||||||
|
v := reflect.ValueOf(s).Elem()
|
||||||
|
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
fieldTyp := v.Type().Field(i)
|
||||||
|
if fieldTyp.IsExported() {
|
||||||
|
key := fieldTyp.Name
|
||||||
|
if value, ok := fieldTyp.Tag.Lookup("db"); ok {
|
||||||
|
if value[len(value)-1] == '*' {
|
||||||
|
return key, value[:len(value)-1], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func repeatSlice[T any](value T, count int) []T {
|
||||||
|
s := make([]T, count)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
s[i] = value
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
// The [sl] package has two main concepts, the [ServiceLocator] itself is the
|
||||||
|
// main object that one should pass around through the application. A
|
||||||
|
// [ServiceLocator] has a list of slots that can be filled with
|
||||||
|
// [InjectLazy] and [InjectValue] and retrieved with [Use]. Slots should
|
||||||
|
// be unique by type and they can only be created with the [NewSlot] function.
|
||||||
|
//
|
||||||
|
// The usual way to use this module is to make slots for Go interfaces and
|
||||||
|
// then pass implementations using the [InjectValue] and
|
||||||
|
// [InjectLazy] functions.
|
||||||
|
//
|
||||||
|
// Services can be of various types:
|
||||||
|
// - a service with no dependencies can be directly injected inside a
|
||||||
|
// ServiceLocator using [InjectValue].
|
||||||
|
// - a service with dependencies on other service should use
|
||||||
|
// [InjectLazy]. This lets the service to initialize itself when needed
|
||||||
|
// and makes the developer not think about correctly ordering the
|
||||||
|
// initialization of its dependencies
|
||||||
|
// - a service can also be private, in this case the slot for a service
|
||||||
|
// should be a private field in the service package. This kind of
|
||||||
|
// services should also provide a way to inject them into a
|
||||||
|
// ServiceLocator.
|
||||||
|
// - a package can also just provide a slot with some value. This is useful
|
||||||
|
// for using the ServiceLocator to easily pass around values, effectively
|
||||||
|
// threating slots just as dynamically scoped variables.
|
||||||
|
package sl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is the debug logger
|
||||||
|
//
|
||||||
|
// TODO: in the future this will be disabled and discard by default.
|
||||||
|
//
|
||||||
|
// As this is the service locator module it was meaning less to pass this
|
||||||
|
// through the ServiceLocator itself (without making the whole module more
|
||||||
|
// complex)
|
||||||
|
var Logger *log.Logger = log.New(os.Stderr, "[service locator] ", log.Lmsgprefix)
|
||||||
|
|
||||||
|
// slot is just a "typed" unique "symbol".
|
||||||
|
type slot[T any] *struct{}
|
||||||
|
|
||||||
|
// NewSlot is the only way to create instances of the slot type. Each instance
|
||||||
|
// is unique.
|
||||||
|
//
|
||||||
|
// This then lets you attach a service instance of type "T" to a
|
||||||
|
// [ServiceLocator] object.
|
||||||
|
func NewSlot[T any]() slot[T] {
|
||||||
|
return slot[T](new(struct{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// slotEntry represents a service that can lazily initialized
|
||||||
|
// (using "createFunc"). Once initialized the instance is kept in the "value"
|
||||||
|
// field and "created" will always be "true". The field "typeName" just for
|
||||||
|
// debugging purposes.
|
||||||
|
type slotEntry struct {
|
||||||
|
typeName string
|
||||||
|
|
||||||
|
createFunc func(*ServiceLocator) (any, error)
|
||||||
|
created bool
|
||||||
|
|
||||||
|
value any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slotEntry) checkInitialized(l *ServiceLocator) error {
|
||||||
|
if !s.created {
|
||||||
|
v, err := s.createFunc(l)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Printf(`[slot: %s] initialized value of type %T`, s.typeName, v)
|
||||||
|
|
||||||
|
s.created = true
|
||||||
|
s.value = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceLocator is the main context passed around to retrive service
|
||||||
|
// instances, the interface uses generics so to inject and retrive service
|
||||||
|
// instances you should use the functions [InjectValue], [InjectLazy] and
|
||||||
|
// [Use].
|
||||||
|
type ServiceLocator struct {
|
||||||
|
providers map[any]*slotEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new [ServiceLocator] context to pass around in the application.
|
||||||
|
func New() *ServiceLocator {
|
||||||
|
return &ServiceLocator{
|
||||||
|
providers: map[any]*slotEntry{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InjectValue will inject a concrete instance inside the ServiceLocator "l"
|
||||||
|
// for the given "slotKey". This should be used for injecting "static"
|
||||||
|
// services, for instances whose construction depend on other services you
|
||||||
|
// should use the [InjectLazy] function.
|
||||||
|
//
|
||||||
|
// This is generic over "T" to check that instances for the given slot type
|
||||||
|
// check as "T" can also be an interface.
|
||||||
|
func InjectValue[T any](l *ServiceLocator, slotKey slot[T], value T) T {
|
||||||
|
Logger.Printf(`[slot: %s] injected value of type %T`, getTypeName[T](), value)
|
||||||
|
|
||||||
|
l.providers[slotKey] = &slotEntry{
|
||||||
|
getTypeName[T](),
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// InjectLazy will inject an instance inside the given ServiceLocator
|
||||||
|
// and "slotKey" that is created only when requested with a call to the
|
||||||
|
// [Use] function.
|
||||||
|
//
|
||||||
|
// This is generic over "T" to check that instances for the given slot type
|
||||||
|
// check as "T" can also be an interface.
|
||||||
|
func InjectLazy[T any](l *ServiceLocator, slotKey slot[T], createFunc func(*ServiceLocator) (T, error)) {
|
||||||
|
Logger.Printf(`[slot: %s] injected lazy`, getTypeName[T]())
|
||||||
|
|
||||||
|
l.providers[slotKey] = &slotEntry{
|
||||||
|
createFunc: func(l *ServiceLocator) (any, error) {
|
||||||
|
return createFunc(l)
|
||||||
|
},
|
||||||
|
created: false,
|
||||||
|
value: nil,
|
||||||
|
typeName: getTypeName[T](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use retrieves the value of type T associated with the given slot key from
|
||||||
|
// the provided ServiceLocator instance.
|
||||||
|
//
|
||||||
|
// If the ServiceLocator does not have a value for the slot key, or if the
|
||||||
|
// value wasn't correctly initialized (in the case of a lazy slot), an error
|
||||||
|
// is returned.
|
||||||
|
func Use[T any](l *ServiceLocator, slotKey slot[T]) (T, error) {
|
||||||
|
var zero T
|
||||||
|
|
||||||
|
slot, ok := l.providers[slotKey]
|
||||||
|
if !ok {
|
||||||
|
return zero, fmt.Errorf(`no injected value for type %s`, getTypeName[T]())
|
||||||
|
}
|
||||||
|
|
||||||
|
err := slot.checkInitialized(l)
|
||||||
|
if err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v := slot.value.(T)
|
||||||
|
|
||||||
|
Logger.Printf(`[slot: %s] using slot with value of type %T`, getTypeName[T](), v)
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTypeName is a trick to get the name of a type (even if it is an
|
||||||
|
// interface type)
|
||||||
|
func getTypeName[T any]() string {
|
||||||
|
var zero T
|
||||||
|
return fmt.Sprintf(`%T`, &zero)[1:]
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompactIndentedLines removes all indentation from a string and also removes all newlines, usefull in tests
|
||||||
|
func CompactIndentedLines(s string) string {
|
||||||
|
return regexp.MustCompile(`(?m)\n\s+`).ReplaceAllString(s, "")
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "website-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Frontend for the PHC website",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "^2.3.1",
|
||||||
|
"sass": "^1.62.1"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,45 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Slot = sl.NewSlot[Config]()
|
||||||
|
|
||||||
|
var TestingProductionConfig = Config{
|
||||||
|
Mode: "production",
|
||||||
|
Host: ":4000",
|
||||||
|
}
|
||||||
|
|
||||||
|
var TestingDevelopmentConfig = Config{
|
||||||
|
Mode: "development",
|
||||||
|
Host: ":4000",
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Mode string
|
||||||
|
Host string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(l *sl.ServiceLocator) (Config, error) {
|
||||||
|
m, err := godotenv.Read(".env")
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
|
||||||
|
cfg.Mode = "production"
|
||||||
|
if v, ok := m["MODE"]; ok {
|
||||||
|
cfg.Mode = v
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Host = ":4000"
|
||||||
|
if v, ok := m["HOST"]; ok {
|
||||||
|
cfg.Host = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Slot = sl.NewSlot[Database]()
|
||||||
|
|
||||||
|
// Database is the main database service interface
|
||||||
|
type Database interface {
|
||||||
|
// User
|
||||||
|
|
||||||
|
CreateUser(user model.User) error
|
||||||
|
ReadUser(id db.Ref[model.User]) (model.User, error)
|
||||||
|
ReadUsers() ([]model.User, error)
|
||||||
|
UpdateUser(user model.User) error
|
||||||
|
DeleteUser(id db.Ref[model.User]) error
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
|
||||||
|
CreateAccount(account model.Account) (db.Ref[model.Account], error)
|
||||||
|
ReadAccount(id db.Ref[model.Account]) (model.Account, error)
|
||||||
|
UpdateAccount(user model.Account) error
|
||||||
|
DeleteAccount(id db.Ref[model.Account]) error
|
||||||
|
|
||||||
|
// Users & Accounts
|
||||||
|
|
||||||
|
ReadUserAccounts(user db.Ref[model.User]) ([]model.Account, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory is an in-memory implementation of [Database] that can be used for testing
|
||||||
|
type Memory struct {
|
||||||
|
Database // TODO: Per ora almeno così facciamo compilare il codice
|
||||||
|
|
||||||
|
Users []model.User
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) CreateUser(user model.User) error {
|
||||||
|
m.Users = append(m.Users, user)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ReadUser(id db.Ref[model.User]) (model.User, error) {
|
||||||
|
for _, u := range m.Users {
|
||||||
|
if u.Id == id {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.User{}, fmt.Errorf(`no user with id "%s"`, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ReadUsers() ([]model.User, error) {
|
||||||
|
return m.Users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdateUser(user model.User) error {
|
||||||
|
for i, u := range m.Users {
|
||||||
|
if u.Id == user.Id {
|
||||||
|
m.Users[i] = user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(`no user with id "%s"`, user.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) DeleteUser(id db.Ref[model.User]) error {
|
||||||
|
for i, u := range m.Users {
|
||||||
|
if u.Id == id {
|
||||||
|
m.Users = append(m.Users[:i], m.Users[i+1:]...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(`no user with id "%s"`, id)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package tables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Users = db.AutoTable[model.User]("users")
|
||||||
|
var Accounts = db.AutoTable[model.Account]("accounts")
|
@ -0,0 +1,34 @@
|
|||||||
|
package listautenti
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/routes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Configure(l *sl.ServiceLocator) error {
|
||||||
|
db, err := sl.Use(l, database.Slot)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := sl.Use(l, routes.Root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Get("/api/lista-utenti", func(c *fiber.Ctx) error {
|
||||||
|
users, err := db.ReadUsers()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pre-process data to strip server only fields...
|
||||||
|
return c.JSON(users)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
package listautenti_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
"github.com/valyala/fasthttp/fasthttputil"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/util"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/config"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/database"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/listautenti"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/model"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/routes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApiListaUtenti(t *testing.T) {
|
||||||
|
r := fiber.New()
|
||||||
|
memDB := &database.Memory{
|
||||||
|
Users: []model.User{
|
||||||
|
{
|
||||||
|
Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534",
|
||||||
|
Username: "claire-doe",
|
||||||
|
FullName: db.NewOption("Claire Doe"),
|
||||||
|
Email: db.NewOption("claire.doe@example.org"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "9b7109cd-95a1-41e9-a9f6-001a32c20ca1",
|
||||||
|
Username: "john-smith",
|
||||||
|
FullName: db.NewOption("John Smith"),
|
||||||
|
Email: db.NewOption("john.smith@example.org"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
l := sl.New()
|
||||||
|
sl.InjectValue(l, config.Slot, config.TestingProductionConfig)
|
||||||
|
sl.InjectValue[database.Database](l, database.Slot, memDB)
|
||||||
|
sl.InjectValue(l, routes.Root, fiber.Router(r))
|
||||||
|
listautenti.Configure(l)
|
||||||
|
|
||||||
|
// Try doing the request
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "http://localhost:4000/api/lista-utenti", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ln := fasthttputil.NewInmemoryListener()
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := fasthttp.Serve(ln, r.Handler())
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to serve: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return ln.Dial()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, string(body), util.CompactIndentedLines(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Id":"claire",
|
||||||
|
"FullName":"Claire Doe",
|
||||||
|
"Nickname":"claire-doe",
|
||||||
|
"Email":"claire.doe@example.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id":"john",
|
||||||
|
"FullName":"John Smith",
|
||||||
|
"Nickname":"john-smith",
|
||||||
|
"Email":"john.smith@example.org"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`))
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "git.phc.dm.unipi.it/phc/website/libs/db"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
// Id è l'id unico di questo utente, non è modificabile una volta creato
|
||||||
|
// l'utente.
|
||||||
|
Id db.Ref[User] `db:"id"`
|
||||||
|
|
||||||
|
// Username è il nome leggibile di questo utente utilizzato anche per le
|
||||||
|
// route per singolo utente, deve essere unico nel sito.
|
||||||
|
//
|
||||||
|
// NOTE: Quando un utente accede per la prima volta di default gli viene
|
||||||
|
// chiesto se usare quello dell'account che sta usando o se cambiarlo
|
||||||
|
// (in teoria non dovrebbe essere un problema poterlo modificare
|
||||||
|
// successivamente).
|
||||||
|
Username string `db:"username"`
|
||||||
|
|
||||||
|
// FullName da mostrare in giro per il sito
|
||||||
|
FullName db.Option[string] `db:"full_name"`
|
||||||
|
|
||||||
|
// Email per eventuale contatto
|
||||||
|
Email db.Option[string] `db:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderType string
|
||||||
|
|
||||||
|
const AteneoProvider ProviderType = "ateneo"
|
||||||
|
const PoissonProvider ProviderType = "poisson"
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
Id db.Ref[Account] `db:"id"` // Id of this entity
|
||||||
|
UserId db.Ref[User] `db:"user_id"` // UserId tells the owner of this account binding
|
||||||
|
Provider ProviderType `db:"provider"` // Provider is the name of the authentication method
|
||||||
|
|
||||||
|
Token string `db:"token"` // Token to use to make requests on behalf of this user
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Root = sl.NewSlot[fiber.Router]()
|
@ -0,0 +1,24 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.phc.dm.unipi.it/phc/website/libs/sl"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/listautenti"
|
||||||
|
"git.phc.dm.unipi.it/phc/website/server/routes"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct{ Router *fiber.App }
|
||||||
|
|
||||||
|
func Configure(l *sl.ServiceLocator) (*Server, error) {
|
||||||
|
r := fiber.New(fiber.Config{})
|
||||||
|
r.Static("/assets", "./out/frontend/assets")
|
||||||
|
|
||||||
|
sl.InjectValue(l, routes.Root, fiber.Router(r))
|
||||||
|
|
||||||
|
if err := listautenti.Configure(l); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Server{r}, nil
|
||||||
|
}
|
Loading…
Reference in New Issue