Selaa lähdekoodia

Step 02 WIP: setting up for /contacts.

Frédéric G. MARAND 6 kuukautta sitten
vanhempi
sitoutus
a85bc7fe73

+ 49 - 5
contactsapp/app.go

@@ -2,30 +2,74 @@ package main
 
 import (
 	"errors"
+	"html/template"
 	"log"
 	"net/http"
+
+	"github.com/masterminds/sprig"
 )
 
 const (
 	addr = ":8080"
 )
 
+type data map[string]any
+
 /*
 @app.route("/")
 def index():
 */
-func RouteIndex(w http.ResponseWriter, r *http.Request) {
+func index(w http.ResponseWriter, r *http.Request) {
 	http.Redirect(w, r, "/contacts", http.StatusSeeOther)
 }
 
-func setupRoutes(mux *http.ServeMux) {
-	mux.HandleFunc("/", RouteIndex)
-	mux.Handle("/contacts", http.NotFoundHandler())
+/*
+@app.route("/contacts")
+*/
+func contacts(cs *ContactsStore) http.HandlerFunc {
+	tpl := makeTemplate(
+		"layout",
+		"index",
+	)
+	return func(w http.ResponseWriter, r *http.Request) {
+		search := r.URL.Query().Get("q")
+		var contacts_set []Contact
+		if search != "" {
+			contacts_set = cs.GetAll()
+		} else {
+			contacts_set = cs.Get(search)
+		}
+		if err := tpl.ExecuteTemplate(w, "layout.html", data{
+			"contacts": contacts_set,
+		}); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+	}
+}
+
+func makeTemplate(first string, others ...string) *template.Template {
+	paths := append([]string{first}, others...)
+	for i, path := range paths {
+		paths[i] = "./templates/" + path + ".gohtml"
+	}
+	tpl := template.Must(template.ParseFiles(paths...)).
+		Funcs(sprig.FuncMap())
+	return tpl
+}
+
+func setupRoutes(mux *http.ServeMux, cs *ContactsStore) {
+	mux.HandleFunc("/", index)
+	mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
+	mux.Handle("/contacts", contacts(cs))
 }
 
 func main() {
+	cs, err := NewContactsStore()
+	if err != nil {
+		log.Fatalf("initializing contacts: %v\n", err)
+	}
 	mux := http.NewServeMux()
-	setupRoutes(mux)
+	setupRoutes(mux, cs)
 	log.Printf("Listening on http://localhost%s", addr)
 	if err := http.ListenAndServe(addr, mux); err != nil {
 		if !errors.Is(err, http.ErrServerClosed) {

+ 138 - 0
contactsapp/contacts.json

@@ -0,0 +1,138 @@
+[
+  {
+    "id": 2,
+    "first": "Carson",
+    "last": "Gross",
+    "phone": "123-456-7890",
+    "email": "carson@example.com",
+    "errors": {}
+  },
+  {
+    "id": 3,
+    "first": "",
+    "last": "",
+    "phone": "",
+    "email": "joe@example2.com",
+    "errors": {}
+  },
+  {
+    "id": 5,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe@example.com",
+    "errors": {}
+  },
+  {
+    "id": 6,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe1@example.com",
+    "errors": {}
+  },
+  {
+    "id": 7,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe2@example.com",
+    "errors": {}
+  },
+  {
+    "id": 8,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe3@example.com",
+    "errors": {}
+  },
+  {
+    "id": 9,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe4@example.com",
+    "errors": {}
+  },
+  {
+    "id": 10,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe5@example.com",
+    "errors": {}
+  },
+  {
+    "id": 11,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe6@example.com",
+    "errors": {}
+  },
+  {
+    "id": 12,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe7@example.com",
+    "errors": {}
+  },
+  {
+    "id": 13,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe8@example.com",
+    "errors": {}
+  },
+  {
+    "id": 14,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe9@example.com",
+    "errors": {}
+  },
+  {
+    "id": 15,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe10@example.com",
+    "errors": {}
+  },
+  {
+    "id": 16,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe11@example.com",
+    "errors": {}
+  },
+  {
+    "id": 17,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe12@example.com",
+    "errors": {}
+  },
+  {
+    "id": 18,
+    "first": null,
+    "last": null,
+    "phone": null,
+    "email": "restexample1@example.com",
+    "errors": {}
+  },
+  {
+    "id": 19,
+    "first": null,
+    "last": null,
+    "phone": null,
+    "email": "restexample2@example.com",
+    "errors": {}
+  }
+]

+ 138 - 0
contactsapp/contacts0.json

@@ -0,0 +1,138 @@
+[
+  {
+    "id": 2,
+    "first": "Carson",
+    "last": "Gross",
+    "phone": "123-456-7890",
+    "email": "carson@example.com",
+    "errors": {}
+  },
+  {
+    "id": 3,
+    "first": "",
+    "last": "",
+    "phone": "",
+    "email": "joe@example2.com",
+    "errors": {}
+  },
+  {
+    "id": 5,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe@example.com",
+    "errors": {}
+  },
+  {
+    "id": 6,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe1@example.com",
+    "errors": {}
+  },
+  {
+    "id": 7,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe2@example.com",
+    "errors": {}
+  },
+  {
+    "id": 8,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe3@example.com",
+    "errors": {}
+  },
+  {
+    "id": 9,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe4@example.com",
+    "errors": {}
+  },
+  {
+    "id": 10,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe5@example.com",
+    "errors": {}
+  },
+  {
+    "id": 11,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe6@example.com",
+    "errors": {}
+  },
+  {
+    "id": 12,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe7@example.com",
+    "errors": {}
+  },
+  {
+    "id": 13,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe8@example.com",
+    "errors": {}
+  },
+  {
+    "id": 14,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe9@example.com",
+    "errors": {}
+  },
+  {
+    "id": 15,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe10@example.com",
+    "errors": {}
+  },
+  {
+    "id": 16,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe11@example.com",
+    "errors": {}
+  },
+  {
+    "id": 17,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe12@example.com",
+    "errors": {}
+  },
+  {
+    "id": 18,
+    "first": null,
+    "last": null,
+    "phone": null,
+    "email": "restexample1@example.com",
+    "errors": {}
+  },
+  {
+    "id": 19,
+    "first": null,
+    "last": null,
+    "phone": null,
+    "email": "restexample2@example.com",
+    "errors": {}
+  }
+]

+ 68 - 0
contactsapp/contacts_model.go

@@ -0,0 +1,68 @@
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"os"
+	"sync"
+)
+
+var (
+	ContactsFile = "contacts.json"
+)
+
+type (
+	Contacts      []Contact
+	ContactsStore struct {
+		sync.Mutex
+		data map[int]Contact
+	}
+)
+
+func NewContactsStore() (*ContactsStore, error) {
+	cs := ContactsStore{
+		data: make(map[int]Contact),
+	}
+	if err := cs.Load(); err != nil {
+		return nil, err
+	}
+	return &cs, nil
+}
+
+func (cs *ContactsStore) Get(search string) Contacts {
+	return make(Contacts, 0)
+}
+
+func (cs *ContactsStore) GetAll() Contacts {
+	return make(Contacts, 0)
+}
+
+func (cs *ContactsStore) Load() error {
+	if cs == nil {
+		return errors.New("cannot load into nil store")
+	}
+	bs, err := os.ReadFile(ContactsFile)
+	if err != nil {
+		return fmt.Errorf("reading file: %w", err)
+	}
+	contacts := make(Contacts, 0)
+	if err := json.Unmarshal(bs, &contacts); err != nil {
+		return fmt.Errorf("unmarshalling file: %w", err)
+	}
+	cs.Lock()
+	defer cs.Unlock()
+	for _, contact := range contacts {
+		cs.data[contact.ID] = contact
+	}
+	return nil
+}
+
+type Contact struct {
+	ID     int    `json:"id"`
+	First  string `json:"first"`
+	Last   string `json:"last"`
+	Phone  string `json:"phone"`
+	Email  string `json:"email"`
+	errors map[string]string
+}

+ 145 - 0
contactsapp/contacts_model.py

@@ -0,0 +1,145 @@
+import json
+from operator import attrgetter
+import time
+from threading import Thread
+from random import random
+
+
+# ========================================================
+# Contact Model
+# ========================================================
+PAGE_SIZE = 100
+
+class Contact:
+    # mock contacts database
+    db = {}
+
+    def __init__(self, id_=None, first=None, last=None, phone=None, email=None):
+        self.id = id_
+        self.first = first
+        self.last = last
+        self.phone = phone
+        self.email = email
+        self.errors = {}
+
+    def __str__(self):
+        return json.dumps(self.__dict__, ensure_ascii=False)
+
+    def update(self, first, last, phone, email):
+        self.first = first
+        self.last = last
+        self.phone = phone
+        self.email = email
+
+    def validate(self):
+        if not self.email:
+            self.errors['email'] = "Email Required"
+        existing_contact = next(filter(lambda c: c.id != self.id and c.email == self.email, Contact.db.values()), None)
+        if existing_contact:
+            self.errors['email'] = "Email Must Be Unique"
+        return len(self.errors) == 0
+
+    def save(self):
+        if not self.validate():
+            return False
+        if self.id is None:
+            if len(Contact.db) == 0:
+                max_id = 1
+            else:
+                max_id = max(contact.id for contact in Contact.db.values())
+            self.id = max_id + 1
+            Contact.db[self.id] = self
+        Contact.save_db()
+        return True
+
+    def delete(self):
+        del Contact.db[self.id]
+        Contact.save_db()
+
+    @classmethod
+    def count(cls):
+        time.sleep(2)
+        return len(cls.db)
+
+    @classmethod
+    def all(cls, page=1):
+        page = int(page)
+        start = (page - 1) * PAGE_SIZE
+        end = start + PAGE_SIZE
+        return list(cls.db.values())[start:end]
+
+    @classmethod
+    def search(cls, text):
+        result = []
+        for c in cls.db.values():
+            match_first = c.first is not None and text in c.first
+            match_last = c.last is not None and text in c.last
+            match_email = c.email is not None and text in c.email
+            match_phone = c.phone is not None and text in c.phone
+            if match_first or match_last or match_email or match_phone:
+                result.append(c)
+        return result
+
+    @classmethod
+    def load_db(cls):
+        with open('contacts.json', 'r') as contacts_file:
+            contacts = json.load(contacts_file)
+            cls.db.clear()
+            for c in contacts:
+                cls.db[c['id']] = Contact(c['id'], c['first'], c['last'], c['phone'], c['email'])
+
+    @staticmethod
+    def save_db():
+        out_arr = [c.__dict__ for c in Contact.db.values()]
+        with open("contacts.json", "w") as f:
+            json.dump(out_arr, f, indent=2)
+
+    @classmethod
+    def find(cls, id_):
+        id_ = int(id_)
+        c = cls.db.get(id_)
+        if c is not None:
+            c.errors = {}
+
+        return c
+
+
+class Archiver:
+    archive_status = "Waiting"
+    archive_progress = 0
+    thread = None
+
+    def status(self):
+        return Archiver.archive_status
+
+    def progress(self):
+        return Archiver.archive_progress
+
+    def run(self):
+        if Archiver.archive_status == "Waiting":
+            Archiver.archive_status = "Running"
+            Archiver.archive_progress = 0
+            Archiver.thread = Thread(target=self.run_impl)
+            Archiver.thread.start()
+
+    def run_impl(self):
+        for i in range(10):
+            time.sleep(1 * random())
+            if Archiver.archive_status != "Running":
+                return
+            Archiver.archive_progress = (i + 1) / 10
+            print("Here... " + str(Archiver.archive_progress))
+        time.sleep(1)
+        if Archiver.archive_status != "Running":
+            return
+        Archiver.archive_status = "Complete"
+
+    def archive_file(self):
+        return "contacts.json"
+
+    def reset(self):
+        Archiver.archive_status = "Waiting"
+
+    @classmethod
+    def get(cls):
+        return Archiver()

+ 66 - 0
contactsapp/contacts_model_test.go

@@ -0,0 +1,66 @@
+package main
+
+import "testing"
+
+func TestNewContactsStore(t *testing.T) {
+	t.Run("normal build", func(t *testing.T) {
+		cs, err := NewContactsStore()
+		if err != nil {
+			t.Fatal(err)
+		}
+		if cs == nil {
+			t.Fatal("nil store created")
+		}
+		if len(cs.data) == 0 {
+			t.Fatal("empty store")
+		}
+	})
+	t.Run("building with missing file", func(t *testing.T) {
+		scf := ContactsFile
+		t.Cleanup(func() {
+			ContactsFile = scf
+		})
+		ContactsFile = "badfile.json"
+		cs, err := NewContactsStore()
+		if err == nil || cs != nil {
+			t.Fatalf("unexpected success creation from missing file")
+		}
+	})
+}
+
+func TestContactsStore_Load(t *testing.T) {
+	var cs *ContactsStore
+	t.Run("nil store", func(t *testing.T) {
+		if err := cs.Load(); err == nil {
+			t.Fatalf("unexpected success on loading nil store")
+		}
+	})
+	cs = &ContactsStore{
+		data: make(map[int]Contact),
+	}
+	t.Run("valid load", func(t *testing.T) {
+		if err := cs.Load(); err != nil {
+			t.Fatalf("unexpected error on loading valid store: err")
+		}
+	})
+	t.Run("missing file", func(t *testing.T) {
+		scf := ContactsFile
+		t.Cleanup(func() {
+			ContactsFile = scf
+		})
+		ContactsFile = "badfile.json"
+		if err := cs.Load(); err == nil {
+			t.Fatalf("unexpected success on loading missing file")
+		}
+	})
+	t.Run("non-json file", func(t *testing.T) {
+		scf := ContactsFile
+		t.Cleanup(func() {
+			ContactsFile = scf
+		})
+		ContactsFile = "contacts_model_test.go"
+		if err := cs.Load(); err == nil {
+			t.Fatalf("unexpected success on loading non-json file")
+		}
+	})
+}

+ 55 - 0
contactsapp/static/img/spinning-circles.svg

@@ -0,0 +1,55 @@
+<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
+<svg width="58" height="58" viewBox="0 0 58 58" xmlns="http://www.w3.org/2000/svg">
+    <g fill="none" fill-rule="evenodd">
+        <g transform="translate(2 1)" stroke="#000" stroke-width="1.5">
+            <circle cx="42.601" cy="11.462" r="5" fill-opacity="1" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="1;0;0;0;0;0;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="49.063" cy="27.063" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;1;0;0;0;0;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="42.601" cy="42.663" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;1;0;0;0;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="27" cy="49.125" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;0;1;0;0;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="11.399" cy="42.663" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;0;0;1;0;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="4.938" cy="27.063" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;0;0;0;1;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="11.399" cy="11.462" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;0;0;0;0;1;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="27" cy="5" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;0;0;0;0;0;1" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+        </g>
+    </g>
+</svg>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
contactsapp/static/missing.min.css


+ 65 - 0
contactsapp/static/site.css

@@ -0,0 +1,65 @@
+.flash {
+    display: block;
+    background-color: #2fdc2f !important;
+    color: white;
+    font-weight: bold;
+    padding: 12px;
+    border: 1px solid black;
+    border-radius: 8px;
+    margin: 16px;
+}
+
+table {
+    width: 100%;
+    margin-bottom: 12px;
+}
+
+.error {
+    display: inline-block;
+    color: darkred;
+}
+
+tr.htmx-swapping {
+  opacity: 0;
+  transition: opacity 1s ease-out;
+}
+
+td {
+    vertical-align: middle;
+}
+
+#download-ui {
+    margin-bottom: 16px;
+}
+
+.progress {
+    height: 20px;
+    margin-bottom: 20px;
+    overflow: hidden;
+    background-color: #f5f5f5;
+    border-radius: 4px;
+    box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
+}
+
+.progress-bar {
+    float: left;
+    width: 0%;
+    height: 100%;
+    font-size: 12px;
+    line-height: 20px;
+    color: #fff;
+    text-align: center;
+    background-color: #337ab7;
+    -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
+    box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
+    -webkit-transition: width .6s ease;
+    -o-transition: width .6s ease;
+    transition: width .6s ease;
+}
+
+[data-overflow-menu] {
+    visibility: hidden;
+}
+    tr:is(:hover, :focus-within) [data-overflow-menu] {
+        visibility: visible;
+    }

+ 5 - 0
contactsapp/templates/index.gohtml

@@ -0,0 +1,5 @@
+{{ define "index.gohtml" }}
+    {{block "content" . }}
+      index
+    {{ end}}
+{{ end }}

+ 16 - 0
contactsapp/templates/layout.gohtml

@@ -0,0 +1,16 @@
+{{ define "layout.html" }}
+  <!DOCTYPE html>
+  <html lang="">
+  <head>
+    <title>Contact App</title>
+    <link rel="stylesheet" href="/static//missing.min.css">
+    <link rel="stylesheet" href="/static/site.css">
+  </head>
+  <body>
+
+  {{ block "content" . }}
+    Default content
+  {{ end }}
+  </body>
+  </html>
+{{ end }}

+ 14 - 0
go.mod

@@ -1,3 +1,17 @@
 module code.osinet.fr/fgm/demo-htmx
 
 go 1.22.2
+
+require github.com/masterminds/sprig v2.22.0+incompatible
+
+require (
+	github.com/Masterminds/goutils v1.1.1 // indirect
+	github.com/Masterminds/semver v1.5.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/huandu/xstrings v1.4.0 // indirect
+	github.com/imdario/mergo v0.3.16 // indirect
+	github.com/mitchellh/copystructure v1.2.0 // indirect
+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
+	github.com/stretchr/testify v1.9.0 // indirect
+	golang.org/x/crypto v0.23.0 // indirect
+)

+ 27 - 0
go.sum

@@ -0,0 +1,27 @@
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
+github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/masterminds/sprig v2.22.0+incompatible h1:p8t5GkYFLmZZdQt/xKOPyTXanAmp4ysv8Lcju1uGk2Y=
+github.com/masterminds/sprig v2.22.0+incompatible/go.mod h1:MI10VsRNvEV+FKEkd5Ptqq78DEhmrb3fCBgby33c0oY=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä