diff --git a/apps/backend/miogest_handler.go b/apps/backend/miogest_handler.go index 889486b..188f81e 100644 --- a/apps/backend/miogest_handler.go +++ b/apps/backend/miogest_handler.go @@ -1,6 +1,7 @@ package main import ( + "backend/typedefs" "fmt" "io" "log" @@ -17,27 +18,27 @@ import ( ) type MiogestHandler struct { - Caratterisiche ListaCaratteristiche - Categorie []Categoria - AnnunciXML AnnunciXML - AnnunciParsed AnnunciParsed + Caratterisiche typedefs.ListaCaratteristiche + Categorie []typedefs.Categoria + AnnunciXML typedefs.AnnunciXML + AnnunciParsed typedefs.AnnunciParsed annunci_cache_expires time.Time vars_cache_expires time.Time } func NewMiogestHandler() *MiogestHandler { return &MiogestHandler{ - Caratterisiche: ListaCaratteristiche{}, + Caratterisiche: typedefs.ListaCaratteristiche{}, Categorie: nil, - AnnunciXML: AnnunciXML{}, - AnnunciParsed: AnnunciParsed{}, + AnnunciXML: typedefs.AnnunciXML{}, + AnnunciParsed: typedefs.AnnunciParsed{}, annunci_cache_expires: time.Now(), vars_cache_expires: time.Now(), } } // ANNUNCI FROM MIOGEST -func (m *MiogestHandler) GetAnnunci() AnnunciXML { +func (m *MiogestHandler) GetAnnunci() typedefs.AnnunciXML { if len(m.AnnunciXML.Annuncio) == 0 || time.Now().After(m.annunci_cache_expires) { m.GetAnnunciFromMiogest() } @@ -45,7 +46,7 @@ func (m *MiogestHandler) GetAnnunci() AnnunciXML { } func (m *MiogestHandler) GetAnnunciFromMiogest() { - m.AnnunciXML = GetDataXML[AnnunciXML]("http://partner.miogest.com/agenzie/infoalloggi.xml") + m.AnnunciXML = GetDataXML[typedefs.AnnunciXML]("http://partner.miogest.com/agenzie/infoalloggi.xml") m.annunci_cache_expires = time.Now().Add(1 * time.Hour) } @@ -55,14 +56,14 @@ func (m *MiogestHandler) InitDefaults() { m.GenerateCategorie() } -func (m *MiogestHandler) GetCaratteristiche() ListaCaratteristiche { +func (m *MiogestHandler) GetCaratteristiche() typedefs.ListaCaratteristiche { if len(m.Caratterisiche.Caratteristiche) == 0 || time.Now().After(m.vars_cache_expires) { m.GenerateCaratteristiche() } return m.Caratterisiche } -func (m *MiogestHandler) GetCategorie() []Categoria { +func (m *MiogestHandler) GetCategorie() []typedefs.Categoria { if len(m.Categorie) == 0 || time.Now().After(m.vars_cache_expires) { m.GenerateCategorie() } @@ -70,10 +71,10 @@ func (m *MiogestHandler) GetCategorie() []Categoria { } func (m *MiogestHandler) GenerateCaratteristiche() { - var result = GetDataXML[DefaultCaratteristiche]("https://www.miogest.com/apps/revo.aspx?tipo=schede") - var array ListaCaratteristiche + var result = GetDataXML[typedefs.DefaultCaratteristiche]("https://www.miogest.com/apps/revo.aspx?tipo=schede") + var array typedefs.ListaCaratteristiche for _, tag := range result.Scheda { - var c = Caratteristica{Id: tag.Id, Tagxml: tag.Tagxml} + var c = typedefs.Caratteristica{Id: tag.Id, Tagxml: tag.Tagxml} array.Caratteristiche = append(array.Caratteristiche, c) } m.Caratterisiche = array @@ -81,10 +82,10 @@ func (m *MiogestHandler) GenerateCaratteristiche() { } func (m *MiogestHandler) GenerateCategorie() { - var result = GetDataXML[DefaultCategories]("https://www.miogest.com/apps/revo.aspx?tipo=categorie") - var listaCategorie []Categoria + var result = GetDataXML[typedefs.DefaultCategories]("https://www.miogest.com/apps/revo.aspx?tipo=categorie") + var listaCategorie []typedefs.Categoria for _, elem := range result.Cat { - var c = Categoria{Id: elem.ID, Nome: elem.Nome} + var c = typedefs.Categoria{Id: elem.ID, Nome: elem.Nome} listaCategorie = append(listaCategorie, c) } m.Categorie = listaCategorie @@ -92,7 +93,7 @@ func (m *MiogestHandler) GenerateCategorie() { } // ANNUNCI PARSED -func (m *MiogestHandler) GetAnnunciParsed() AnnunciParsed { +func (m *MiogestHandler) GetAnnunciParsed() typedefs.AnnunciParsed { m.ParseAnnunci() return m.AnnunciParsed } @@ -112,8 +113,8 @@ func (m *MiogestHandler) ParseAnnunci() { ImageManager.InsertImagesRef(&annunci) } - var AnnunciArray AnnunciParsed - AnnunciArray.Annuncio = make([]AnnuncioParsed, 0, len(annunci.Annuncio)) + var AnnunciArray typedefs.AnnunciParsed + AnnunciArray.Annuncio = make([]typedefs.AnnuncioParsed, 0, len(annunci.Annuncio)) Miogest.InitDefaults() var wg sync.WaitGroup wg.Add(len(annunci.Annuncio)) @@ -152,8 +153,8 @@ func strUpd(s string) *string { return &s } -func extractData_update(annuncio *AnnuncioXML, updateImages bool) AnnuncioParsed { - var tmp AnnuncioParsed = AnnuncioParsed{} +func extractData_update(annuncio *typedefs.AnnuncioXML, updateImages bool) typedefs.AnnuncioParsed { + var tmp typedefs.AnnuncioParsed = typedefs.AnnuncioParsed{} tmp.Codice = RemoveWhitespace(GetStringValue(annuncio.Codice)) processLocatore(&tmp, annuncio) processIndirizzo(&tmp, annuncio) @@ -240,10 +241,11 @@ func extractData_update(annuncio *AnnuncioXML, updateImages bool) AnnuncioParsed } } } + return tmp } -func processLocatore(tmp *AnnuncioParsed, annuncio *AnnuncioXML) { +func processLocatore(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioXML) { if annuncio.Clienti == nil || annuncio.Clienti.Cliente == nil { return } @@ -266,7 +268,7 @@ func processLocatore(tmp *AnnuncioParsed, annuncio *AnnuncioXML) { tmp.Idlocatore = annuncio.Clienti.Cliente.Id } -func processIndirizzo(tmp *AnnuncioParsed, annuncio *AnnuncioXML) { +func processIndirizzo(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioXML) { tmp.Indirizzo = strUpd(MakeUppercase(GetStringValue(annuncio.Indirizzo))) tmp.Civico = annuncio.Civico tmp.Comune = strUpd(MakeUppercase(GetStringValue(annuncio.Comune))) @@ -281,7 +283,7 @@ func processIndirizzo(tmp *AnnuncioParsed, annuncio *AnnuncioXML) { tmp.Lon_secondario = annuncio.LongitudineSecondario } -func processTipologia(tmp *AnnuncioParsed, annuncio *AnnuncioXML) { +func processTipologia(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioXML) { if annuncio.Tipologia == nil { tmp.Tipo = strUpd("Errore") return @@ -306,7 +308,7 @@ func processTipologia(tmp *AnnuncioParsed, annuncio *AnnuncioXML) { } -func processCategorie(tmp *AnnuncioParsed, annuncio *AnnuncioXML) { +func processCategorie(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioXML) { if annuncio.Categoria == nil { return } @@ -340,7 +342,7 @@ func prezzoParse(prezzo *string) int { return prezzoInt } -func processDatiImmobile(tmp *AnnuncioParsed, annuncio *AnnuncioXML) { +func processDatiImmobile(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioXML) { tmpanno := GetStringValue(annuncio.Anno) if tmpanno != "0" && tmpanno != "" { tmp.Anno = &tmpanno @@ -364,7 +366,7 @@ func processDatiImmobile(tmp *AnnuncioParsed, annuncio *AnnuncioXML) { tmp.Numero_postiauto = annuncio.PostiAuto } -func processImages(tmp *AnnuncioParsed, annuncio *AnnuncioXML) { +func processImages(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioXML) { path := filepath.Join(imgbasepath, tmp.Codice) err := os.MkdirAll(path, 0755) if err != nil { @@ -463,7 +465,7 @@ func folderCleanup(codice string) error { return nil } -func parseCaratteristiche(wrkrecord *AnnuncioXML) map[string]interface{} { +func parseCaratteristiche(wrkrecord *typedefs.AnnuncioXML) map[string]interface{} { if reflect.TypeOf(wrkrecord).Kind() != reflect.Ptr || reflect.ValueOf(wrkrecord).Elem().Kind() != reflect.Struct { return nil } @@ -520,3 +522,98 @@ func parseConsegna(original *string) *int { } return ¤tMonth } + +func ParseDettaglio(dettaglio string) (disp_time *string, perm_arr *[]int, persone_arr *[]string, err error) { + + dettaglio = strings.TrimSpace(dettaglio) + if dettaglio == "" { + return + } + + disp := getKeyValueUpToXorWhitespace(dettaglio, "disp:", "perm:") + + perm := getKeyValueUpToXorWhitespace(dettaglio, "perm:", "persone:") + + persone := getKeyValueUpToXorWhitespace(dettaglio, "persone:", "") + + if disp != "" { + // Validate date format + _, err := time.Parse("02/01", disp) + if err != nil { + disp_time = nil + } else { + p := fmt.Sprintf("%s/%d", disp, time.Now().Year()) + disp_time = &p + } + } + if perm != "" { + parts := strings.Split(perm, "-") + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + num, err := strconv.Atoi(part) + if err == nil { + if perm_arr == nil { + perm_arr = &[]int{} + } + *perm_arr = append(*perm_arr, num) + } + } + } + } + if persone != "" { + parts := strings.Split(persone, "/") + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + if persone_arr == nil { + persone_arr = &[]string{} + } + *persone_arr = append(*persone_arr, part) + } + } + } + return + +} + +func getKeyValueUpToXorWhitespace(input, key, stopstring string) string { + input = strings.TrimSpace(input) + i := strings.Index(input, key) + if i == -1 { + return "" + } + start := i + len(key) + rest := strings.TrimSpace(input[start:]) + + // Find stopstring, whitespace, or repeated key + stop_pos := len(rest) + stops := []int{} + + if stopstring != "" { + if j := strings.Index(rest, stopstring); j != -1 { + stops = append(stops, j) + } + } + // Find whitespace + if ws := strings.IndexAny(rest, " \n\r\t"); ws != -1 { + stops = append(stops, ws) + } + // Find repeated key + if k := strings.Index(rest, key); k != -1 { + stops = append(stops, k) + } + // Use the earliest stop + for _, s := range stops { + if s < stop_pos { + stop_pos = s + } + } + + val := strings.TrimSpace(rest[:stop_pos]) + // Only take the first token before any whitespace or another key + if idx := strings.IndexAny(val, " \n\r\t"); idx != -1 { + val = val[:idx] + } + return val +} diff --git a/apps/backend/miogest_test.go b/apps/backend/miogest_test.go new file mode 100644 index 0000000..b0fcaee --- /dev/null +++ b/apps/backend/miogest_test.go @@ -0,0 +1,208 @@ +package main + +import ( + "fmt" + "testing" + "time" +) + +func ptr[T any](v T) *T { return &v } + +func TestParseDettaglio(t *testing.T) { + tests := []struct { + input string + wantDisp *string + wantPerm *[]int + wantPers *[]string + }{ + { + input: "disp:01/10 perm:3-6 persone:single", + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp: perm: persone:", + wantDisp: nil, + wantPerm: nil, + wantPers: nil, + }, + { + input: "disp: perm:3-6 persone:single", // disp missing + wantDisp: nil, + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm: persone:single", // perm missing + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: nil, + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6 persone:", // persone missing + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: nil, + }, + { + input: "disp: perm: persone:", // all fields missing + wantDisp: nil, + wantPerm: nil, + wantPers: nil, + }, + { + input: "disp:bad_date perm:3-6 persone:single", // invalid date + wantDisp: nil, + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:abc-6 persone:single", // non-numeric perm + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3- persone:single", // trailing delimiter in perm + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:-6 persone:single", // leading delimiter in perm + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6 persone:single/", // trailing delimiter in persone + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6 persone:/single", // leading delimiter in persone + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6 persone:single//coppia", // double delimiter in persone + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single", "coppia"}), + }, + { + input: "perm:3-6 persone:single", // disp missing + wantDisp: nil, + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6", // persone missing + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: nil, + }, + { + input: "disp:01/10 perm: persone:", // perm empty + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: nil, + wantPers: nil, + }, + { + input: "", // completely empty string + wantDisp: nil, + wantPerm: nil, + wantPers: nil, + }, + { + input: "disp: 01/10 perm: 3-6 persone: single ", + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "persone:single perm:3-6 disp:01/10", + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3--6 persone:single", + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10\tperm:3-6\tpersone:single", + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6 persone:single/coppia!", + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single", "coppia!"}), + }, + { + input: "disp:01/10 disp:02/11 perm:3-6 persone:single", + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), // Only first disp: is parsed + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + { + input: "disp:01/10perm:3-6persone:single", + wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: ptr([]int{3, 6}), + wantPers: ptr([]string{"single"}), + }, + } + + for _, tt := range tests { + disp_time, perm_arr, persone_arr, _ := ParseDettaglio(tt.input) + + //Handle *string comparison, including nil + if tt.wantDisp != nil { + if disp_time == nil { + t.Errorf("disp_time: got nil, want %v, input: %v", *tt.wantDisp, tt.input) + } else if *disp_time != *tt.wantDisp { + t.Errorf("disp_time: got %v, want %v, input: %v", *disp_time, *tt.wantDisp, tt.input) + } + } else if disp_time != nil { + t.Errorf("disp_time: got %v, want nil, input: %v", *disp_time, tt.input) + } + + // Handle *[]int comparison, including nil + if tt.wantPerm != nil { + if perm_arr == nil || len(*perm_arr) != len(*tt.wantPerm) { + t.Errorf("perm_arr: got %v, want %v, input: %v", perm_arr, *tt.wantPerm, tt.input) + } else { + for i := range *perm_arr { + if (*perm_arr)[i] != (*tt.wantPerm)[i] { + t.Errorf("perm_arr[%d]: got %v, want %v, input: %v", i, (*perm_arr)[i], (*tt.wantPerm)[i], tt.input) + } + } + } + } else if perm_arr != nil && len(*perm_arr) != 0 { + t.Errorf("perm_arr: got %v, want nil, input: %v", perm_arr, tt.input) + } + + // Handle *[]string comparison, including nil + if tt.wantPers != nil { + if persone_arr == nil || len(*persone_arr) != len(*tt.wantPers) { + t.Errorf("persone_arr: got %v, want %v, input: %v", persone_arr, *tt.wantPers, tt.input) + } else { + for i := range *persone_arr { + if (*persone_arr)[i] != (*tt.wantPers)[i] { + t.Errorf("persone_arr[%d]: got %v, want %v, input: %v", i, (*persone_arr)[i], (*tt.wantPers)[i], tt.input) + } + } + } + } else if persone_arr != nil && len(*persone_arr) != 0 { + t.Errorf("persone_arr: got %v, want nil, input: %v", persone_arr, tt.input) + } + } +} diff --git a/apps/backend/models.go b/apps/backend/typedefs/models.go similarity index 98% rename from apps/backend/models.go rename to apps/backend/typedefs/models.go index 883391d..afdda25 100644 --- a/apps/backend/models.go +++ b/apps/backend/typedefs/models.go @@ -1,4 +1,4 @@ -package main +package typedefs import ( "os" @@ -206,6 +206,7 @@ type AnnuncioXML struct { Url *string `xml:"Url"` } `xml:"AMVideo"` } `xml:"AMMedias"` + Dettaglio *string `xml:"Dettaglio"` } type AnnunciParsed struct { @@ -260,6 +261,9 @@ type AnnuncioParsed struct { Url_video *[]string Web *bool Og_url *string + Disponibile_da *time.Time + Permanenza *[]int + Persone *[]string } type AnnunciDB struct { @@ -312,6 +316,9 @@ type AnnunciDB struct { Creato_il *time.Time Modificato_il *time.Time Og_url *string + Disponibile_da *time.Time + Permanenza *[]int + Persone *[]string } type FilebrowserData struct {