diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1d7c198 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[{*.kt,*.kts}] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/restaurants/README.md b/restaurants/README.md index fc2ab6a..b835d34 100644 --- a/restaurants/README.md +++ b/restaurants/README.md @@ -1,36 +1,46 @@ # Restaurants -This module is responsible for fetching restaurant items from USP (through [USP Restaurant API][1]) and persist them in a PostgresQL table, while also providing an API for the retrieval of these items. +This module is responsible for fetching restaurant items from USP (through [USP Restaurant API][1]) and persist them in +a PostgresQL table, while also providing an API for the retrieval of these items. ## Fetching from USP -We trust the [USP Restaurant API][1] definitions, and with them the [USPRestaurantItemRepository][3] is able to fetch items from USP's api. +We trust the [USP Restaurant API][1] definitions, and with them the [USPRestaurantItemRepository][3] is able to fetch +items from USP's api. ### Parsing the menu -It's important for this application to parse all values, as clients are going to expect a smarter output instead of just the menus (otherwise, why would this module be necessary?). For that we have the [MenuParsers][4]. We try with our best effort to parse the menus, however there is some difficulty as restaurants won't always report items in the same way. +It's important for this application to parse all values, as clients are going to expect a smarter output instead of just +the menus (otherwise, why would this module be necessary?). For that we have the [MenuParsers][4]. We try with our best +effort to parse the menus, however there is some difficulty as restaurants won't always report items in the same way. A future point of improvement is being able to parse menus without such a brittle and forced solution. ## Saving to Postgres -Every item obtained from USP is piped into a PostgresQL item. This enables us to respond to clients even when USP's servers are down (which happen quite often). +Every item obtained from USP is piped into a PostgresQL item. This enables us to respond to clients even when USP's +servers are down (which happen quite often). ## Cache -When an item is successfully obtained, it's first persisted on the Postgres table (see above), and then kept in an in-memory cache. This cache allow us to quickly respond users while keeping costs down. +When an item is successfully obtained, it's first persisted on the Postgres table (see above), and then kept in an +in-memory cache. This cache allow us to quickly respond users while keeping costs down. -It's a good idea to use a cache here, as the data that clients want is usually the menus for the current week, making cache hits very likely. +It's a good idea to use a cache here, as the data that clients want is usually the menus for the current week, making +cache hits very likely. ## Restaurant Job -In order to have all records available - even for restaurants that aren't accessed regularly - we continuously try to get all restaurants from USP, using a fixed rate schedule in [RestaurantJob class][5]. +In order to have all records available - even for restaurants that aren't accessed regularly - we continuously try to +get all restaurants from USP, using a fixed rate schedule in [RestaurantJob class][5]. This will both refresh our current cache and save any new menus to our persistent storage. - [1]: https://github.com/JopiterApp/USP-Restaurant-API + [3]: src/main/kotlin/repository/usp/USPRestaurantItemRepository.kt + [4]: src/main/kotlin/repository/usp/MenuParser.kt + [5]: src/main/kotlin/RestaurantJob.kt \ No newline at end of file diff --git a/restaurants/build.gradle.kts b/restaurants/build.gradle.kts index 3ae4bfa..29629d0 100644 --- a/restaurants/build.gradle.kts +++ b/restaurants/build.gradle.kts @@ -15,7 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import org.jetbrains.kotlin.noarg.gradle.NoArgExtension plugins { kotlin("plugin.spring") @@ -51,4 +50,4 @@ flyway { url = System.getenv("JDBC_URL") ?: "jdbc:postgresql://localhost:5432/jopiter" user = System.getenv("DATABASE_USER") ?: "jopiter" password = System.getenv("DATABASE_PASS") ?: "password" -} \ No newline at end of file +} diff --git a/restaurants/classifier/ClassifierDesign.md b/restaurants/classifier/ClassifierDesign.md index d30aea2..af20b52 100644 --- a/restaurants/classifier/ClassifierDesign.md +++ b/restaurants/classifier/ClassifierDesign.md @@ -1,9 +1,9 @@ # Classifier Design - ### Temos: -Arquivos CSV onde a primeira coluna representa o nome do item e todas as outras colunas representam detalhes/categorias/classes dos itens. +Arquivos CSV onde a primeira coluna representa o nome do item e todas as outras colunas representam +detalhes/categorias/classes dos itens. ### Queremos @@ -22,27 +22,37 @@ CSV: João,Olhos Castanhos,Cabelo Preto Tupla: + ``` [nome = João, colunas=[Olhos Castanhos, Cabelo Preto]] ``` - ## Dados do Classificador ### Para treinar: -Receberá todas as tuplas de um determinado grupo. Todas são semelhantes entre si, e possuem a mesma quantidade de colunas. O classificador só será capaz de comparar tipos que ele já conhece + +Receberá todas as tuplas de um determinado grupo. Todas são semelhantes entre si, e possuem a mesma quantidade de +colunas. O classificador só será capaz de comparar tipos que ele já conhece ### Para prever/classificar + Receberá apenas o nome e terá que deduzir as outras tuplas a partir disso ## Implementação do classificador + 1. CSV é convertido para N-Tuplas 2. Tuplas passam por tratamento - 1. corpus <= Transformar o nome com BagOfWords (lib smile kotlin nlp bag) passando a lista de stop words (TODO: melhorar lista) e um stemmer (FIXME, explicar melhor) de palavras em português - 2. vocabulario <= todos os nomes das tuplas, após passado as stop words e o stemmer - 3. corpusVetorizado <= Com o corpus (nomes como bag of words) e o vocabulário, contar quantas vezes cada vocábulo aparece em cada documento, armazenando cada documento - 4. dadosProntosParaModelagem <= Usamos o algorítimo tfidf para melhorar a definição de importância de cada termo de cada documento (FIXME, explicar melhor) + 1. corpus <= Transformar o nome com BagOfWords (lib smile kotlin nlp bag) passando a lista de stop words (TODO: + melhorar lista) e um stemmer (FIXME, explicar melhor) de palavras em português + 2. vocabulario <= todos os nomes das tuplas, após passado as stop words e o stemmer + 3. corpusVetorizado <= Com o corpus (nomes como bag of words) e o vocabulário, contar quantas vezes cada vocábulo + aparece em cada documento, armazenando cada documento + 4. dadosProntosParaModelagem <= Usamos o algorítimo tfidf para melhorar a definição de importância de cada termo de + cada documento (FIXME, explicar melhor) 3. Com os dados prontos para modelagem, treinamos um modelo diferente para cada uma das N colunas: - 1. Primeiro através de um algorítimo OVR (One Versus Rest) (FIXME explicar melhor OVR) - 2. Este OVR será alimentado com os dados organizados em um vetor de suporte (support vector machine / SVM) (FIXME adicionar artigo que explicar porque svm é o melhor) -4. Estes modelos gerados compõem o clasificador, e podem ser exportados para armazenamento (economizando assim computação futura). É possível estimar pratos futuros neste classificador, tratando o novo documento da mesma forma que os antigos. Dessa forma temos as probabilidades que os modelos dão para cada uma das N colunas \ No newline at end of file + 1. Primeiro através de um algorítimo OVR (One Versus Rest) (FIXME explicar melhor OVR) + 2. Este OVR será alimentado com os dados organizados em um vetor de suporte (support vector machine / SVM) (FIXME + adicionar artigo que explicar porque svm é o melhor) +4. Estes modelos gerados compõem o clasificador, e podem ser exportados para armazenamento (economizando assim + computação futura). É possível estimar pratos futuros neste classificador, tratando o novo documento da mesma forma + que os antigos. Dessa forma temos as probabilidades que os modelos dão para cada uma das N colunas \ No newline at end of file diff --git a/restaurants/classifier/src/main/kotlin/app/jopiter/restaurants/classifier/StopwordsProvider.kt b/restaurants/classifier/src/main/kotlin/app/jopiter/restaurants/classifier/StopwordsProvider.kt index 8fbe12d..939d2b7 100644 --- a/restaurants/classifier/src/main/kotlin/app/jopiter/restaurants/classifier/StopwordsProvider.kt +++ b/restaurants/classifier/src/main/kotlin/app/jopiter/restaurants/classifier/StopwordsProvider.kt @@ -1,7 +1,5 @@ package app.jopiter.restaurants.classifier -import kotlin.streams.toList - @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") class StopwordsProvider { @@ -10,4 +8,5 @@ class StopwordsProvider { } } -private fun portugueseStopwordsFile() = StopwordsProvider::class.java.classLoader.getResourceAsStream("portuguese_stopwords.txt")!! +private fun portugueseStopwordsFile() = + StopwordsProvider::class.java.classLoader.getResourceAsStream("portuguese_stopwords.txt")!! diff --git a/restaurants/classifier/src/test/kotlin/app/jopiter/restaurants/classifier/ClassifierTests.kt b/restaurants/classifier/src/test/kotlin/app/jopiter/restaurants/classifier/ClassifierTests.kt index e419bd9..d2fae80 100644 --- a/restaurants/classifier/src/test/kotlin/app/jopiter/restaurants/classifier/ClassifierTests.kt +++ b/restaurants/classifier/src/test/kotlin/app/jopiter/restaurants/classifier/ClassifierTests.kt @@ -18,8 +18,6 @@ package app.jopiter.restaurants.classifier -import app.jopiter.restaurants.classifier.ClassifiableRow -import app.jopiter.restaurants.classifier.Classifier import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.funSpec import io.kotest.matchers.doubles.shouldBeGreaterThanOrEqual diff --git a/restaurants/src/main/kotlin/app/jopiter/restaurants/RestaurantController.kt b/restaurants/src/main/kotlin/app/jopiter/restaurants/RestaurantController.kt index aea2c98..41cb0ba 100644 --- a/restaurants/src/main/kotlin/app/jopiter/restaurants/RestaurantController.kt +++ b/restaurants/src/main/kotlin/app/jopiter/restaurants/RestaurantController.kt @@ -21,7 +21,6 @@ package app.jopiter.restaurants import app.jopiter.restaurants.classifier.RestaurantItemClassifier import app.jopiter.restaurants.model.Campus import app.jopiter.restaurants.model.ClassifiedRestaurantItem -import app.jopiter.restaurants.model.RestaurantItem import app.jopiter.restaurants.repository.RestaurantItemRepository import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter @@ -40,48 +39,56 @@ import java.time.LocalDate @RestController @RequestMapping("\${api.base.path}/restaurants") class RestaurantController( - private val restaurantItemRepository: RestaurantItemRepository, - private val restaurantItemClassifier: RestaurantItemClassifier + private val restaurantItemRepository: RestaurantItemRepository, + private val restaurantItemClassifier: RestaurantItemClassifier ) { - @Operation( - summary = "List all restaurants and their campi", - description = "List all restaurants that are available, including to which campus it belongs", - tags = ["restaurant"], - responses = [ - ApiResponse(responseCode = "200", content = [ - Content(array = ArraySchema(schema = Schema(implementation = Campus::class)), - mediaType = "application/json") - ]) + @Operation( + summary = "List all restaurants and their campi", + description = "List all restaurants that are available, including to which campus it belongs", + tags = ["restaurant"], + responses = [ + ApiResponse( + responseCode = "200", content = [ + Content( + array = ArraySchema(schema = Schema(implementation = Campus::class)), + mediaType = "application/json" + ) ] - ) - @GetMapping - fun list() = Campus.values().toList() + ) + ] + ) + @GetMapping + fun list() = Campus.values().toList() - @Operation( - summary = "List Items", - description = "Retrieves all items for the chosen dates and restaurant", - tags = ["restaurant"], + @Operation( + summary = "List Items", + description = "Retrieves all items for the chosen dates and restaurant", + tags = ["restaurant"], - parameters = [ - Parameter( - name = "restaurantId", description = "The restaurant ID as defined by /restaurants", required = true - ), - Parameter( - name = "date", description = "The dates you want to fetch items for. ISO_LOCAL_DATE format (yyyy-MM-dd)" - ) - ], + parameters = [ + Parameter( + name = "restaurantId", description = "The restaurant ID as defined by /restaurants", required = true + ), + Parameter( + name = "date", description = "The dates you want to fetch items for. ISO_LOCAL_DATE format (yyyy-MM-dd)" + ) + ], - responses = [ - ApiResponse(responseCode = "200", content = [ - Content(array = ArraySchema(schema = Schema(implementation = ClassifiedRestaurantItem::class)), - mediaType = "application/json") - ]) + responses = [ + ApiResponse( + responseCode = "200", content = [ + Content( + array = ArraySchema(schema = Schema(implementation = ClassifiedRestaurantItem::class)), + mediaType = "application/json" + ) ] - ) - @GetMapping("/items") - fun items( - @RequestParam("restaurantId") restaurantId: Int, - @RequestParam("date") @DateTimeFormat(iso = DATE) dates: Set, - ) = restaurantItemRepository.get(restaurantId, dates).map(restaurantItemClassifier::classify) + ) + ] + ) + @GetMapping("/items") + fun items( + @RequestParam("restaurantId") restaurantId: Int, + @RequestParam("date") @DateTimeFormat(iso = DATE) dates: Set, + ) = restaurantItemRepository.get(restaurantId, dates).map(restaurantItemClassifier::classify) } diff --git a/restaurants/src/main/kotlin/app/jopiter/restaurants/RestaurantJob.kt b/restaurants/src/main/kotlin/app/jopiter/restaurants/RestaurantJob.kt index 4f0b1a9..b001025 100644 --- a/restaurants/src/main/kotlin/app/jopiter/restaurants/RestaurantJob.kt +++ b/restaurants/src/main/kotlin/app/jopiter/restaurants/RestaurantJob.kt @@ -23,21 +23,18 @@ import app.jopiter.restaurants.repository.RestaurantItemRepository import org.slf4j.LoggerFactory import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component -import java.time.LocalDate.now -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.TimeUnit.MINUTES @Component class RestaurantJob( - private val repository: RestaurantItemRepository + private val repository: RestaurantItemRepository ) { - private val logger = LoggerFactory.getLogger(this::class.java) + private val logger = LoggerFactory.getLogger(this::class.java) - @Scheduled(fixedRate = 15_000) - fun run() = Restaurant.entries.forEach { - logger.info("Starting scheduled execution to fetch $it") - repository.fetchFromUsp(it.id) - logger.info("Finished scheduled execution to fetch $it") - } + @Scheduled(fixedRate = 15_000) + fun run() = Restaurant.entries.forEach { + logger.info("Starting scheduled execution to fetch $it") + repository.fetchFromUsp(it.id) + logger.info("Finished scheduled execution to fetch $it") + } } diff --git a/restaurants/src/main/kotlin/app/jopiter/restaurants/classifier/RestaurantItemClassifier.kt b/restaurants/src/main/kotlin/app/jopiter/restaurants/classifier/RestaurantItemClassifier.kt index 0cef658..1e21ccc 100644 --- a/restaurants/src/main/kotlin/app/jopiter/restaurants/classifier/RestaurantItemClassifier.kt +++ b/restaurants/src/main/kotlin/app/jopiter/restaurants/classifier/RestaurantItemClassifier.kt @@ -5,8 +5,8 @@ import app.jopiter.restaurants.model.DessertFoodGroup import app.jopiter.restaurants.model.DessertItem import app.jopiter.restaurants.model.DessertPreparation import app.jopiter.restaurants.model.ProteinFoodGroup -import app.jopiter.restaurants.model.ProteinPreparation import app.jopiter.restaurants.model.ProteinItem +import app.jopiter.restaurants.model.ProteinPreparation import app.jopiter.restaurants.model.RestaurantItem import app.jopiter.restaurants.model.VegetarianFoodGroup import app.jopiter.restaurants.model.VegetarianItem @@ -45,17 +45,29 @@ class RestaurantItemClassifier( fun classifyProtein(item: String): ProteinItem { val classification = proteinClassifier.classify(item) - return ProteinItem(item, ProteinFoodGroup.find(classification.foodGroup), ProteinPreparation.find(classification.preparation)) + return ProteinItem( + item, + ProteinFoodGroup.find(classification.foodGroup), + ProteinPreparation.find(classification.preparation) + ) } fun classifyVegetarian(item: String): VegetarianItem { val classification = vegetarianClassifier.classify(item) - return VegetarianItem(item, VegetarianFoodGroup.find(classification.foodGroup), VegetarianPreparation.find(classification.preparation)) + return VegetarianItem( + item, + VegetarianFoodGroup.find(classification.foodGroup), + VegetarianPreparation.find(classification.preparation) + ) } fun classifyDessert(item: String): DessertItem { val classification = dessertClassifier.classify(item) - return DessertItem(item, DessertFoodGroup.find(classification.foodGroup), DessertPreparation.find(classification.preparation)) + return DessertItem( + item, + DessertFoodGroup.find(classification.foodGroup), + DessertPreparation.find(classification.preparation) + ) } -} \ No newline at end of file +} diff --git a/restaurants/src/main/kotlin/app/jopiter/restaurants/model/ClassifiedRestaurantItem.kt b/restaurants/src/main/kotlin/app/jopiter/restaurants/model/ClassifiedRestaurantItem.kt index 2284dfa..860638a 100644 --- a/restaurants/src/main/kotlin/app/jopiter/restaurants/model/ClassifiedRestaurantItem.kt +++ b/restaurants/src/main/kotlin/app/jopiter/restaurants/model/ClassifiedRestaurantItem.kt @@ -1,7 +1,6 @@ package app.jopiter.restaurants.model import com.fasterxml.jackson.annotation.JsonFormat -import org.springframework.stereotype.Component import java.time.LocalDate diff --git a/restaurants/src/main/kotlin/app/jopiter/restaurants/model/Restaurant.kt b/restaurants/src/main/kotlin/app/jopiter/restaurants/model/Restaurant.kt index 3027caa..b844be3 100644 --- a/restaurants/src/main/kotlin/app/jopiter/restaurants/model/Restaurant.kt +++ b/restaurants/src/main/kotlin/app/jopiter/restaurants/model/Restaurant.kt @@ -44,23 +44,24 @@ import io.swagger.v3.oas.annotations.media.Schema */ @Schema(name = "Campus") private interface CampusSer { - @get:Schema(example = "Cidade Universitária") val campusName: String - val restaurants: List + @get:Schema(example = "Cidade Universitária") + val campusName: String + val restaurants: List } @JsonFormat(shape = JsonFormat.Shape.OBJECT) @Schema(implementation = CampusSer::class) enum class Campus(val campusName: String, val restaurants: List) { - CidadeUniversitaria("Cidade Universitária", listOf(Central, PuSPC, Fisica, Quimicas)), - QuadrilateroSaude("Quadrilátero Saúde", listOf(EscolaDeEnfermagem, SaudePublica)), - LargoSaoFrancisco("Largo São Francisco", listOf(FacDireito)), - UspLeste("USP Leste", listOf(Each)), - Bauru("Campus de Bauru", listOf(Restaurant.Bauru)), - LuizDeQueiroz("Campus \"Luiz de Queiroz\"", listOf(Piracicaba)), - FernandoCosta("Campus \"Fernando Costa\"", listOf(Pirassununga)), - SaoCarlos("Campus de São Carlos", listOf(Crhea, RestauranteArea1, RestauranteArea2)), - RibeiraoPreto("Campus de Ribeirão Preto", listOf(CentralRibeirao)), - Lorena("Campus de Lorena", listOf(Eel1, Eel2)) + CidadeUniversitaria("Cidade Universitária", listOf(Central, PuSPC, Fisica, Quimicas)), + QuadrilateroSaude("Quadrilátero Saúde", listOf(EscolaDeEnfermagem, SaudePublica)), + LargoSaoFrancisco("Largo São Francisco", listOf(FacDireito)), + UspLeste("USP Leste", listOf(Each)), + Bauru("Campus de Bauru", listOf(Restaurant.Bauru)), + LuizDeQueiroz("Campus \"Luiz de Queiroz\"", listOf(Piracicaba)), + FernandoCosta("Campus \"Fernando Costa\"", listOf(Pirassununga)), + SaoCarlos("Campus de São Carlos", listOf(Crhea, RestauranteArea1, RestauranteArea2)), + RibeiraoPreto("Campus de Ribeirão Preto", listOf(CentralRibeirao)), + Lorena("Campus de Lorena", listOf(Eel1, Eel2)) } /** @@ -70,51 +71,53 @@ enum class Campus(val campusName: String, val restaurants: List) { */ @Schema(name = "Restaurant") private interface RestaurantSer { - @get:Schema(example = "9") val id: Int - @get:Schema(example = "Químicas") val restaurantName: String + @get:Schema(example = "9") + val id: Int + @get:Schema(example = "Químicas") + val restaurantName: String } @JsonFormat(shape = JsonFormat.Shape.OBJECT) enum class Restaurant(val id: Int, val restaurantName: String) { - // Cidade Universitária - Central(6, "Central - Campus Butantã"), - PuSPC(7, "PUSP-C - Campus Butantã"), - Fisica(8, "Física - Campus Butantã"), - Quimicas(9, "Químicas - Campus Butantã"), + // Cidade Universitária + Central(6, "Central - Campus Butantã"), + PuSPC(7, "PUSP-C - Campus Butantã"), + Fisica(8, "Física - Campus Butantã"), + Quimicas(9, "Químicas - Campus Butantã"), - // Quatrilátero Saúde - EscolaDeEnfermagem(12, "Escola de Enfermagem"), - SaudePublica(11, "Fac. Saúde Pública"), + // Quatrilátero Saúde + EscolaDeEnfermagem(12, "Escola de Enfermagem"), + SaudePublica(11, "Fac. Saúde Pública"), - // Largo São Francisco - FacDireito(14, "Fac. Direito"), + // Largo São Francisco + FacDireito(14, "Fac. Direito"), - // USP Leste - Each(13, "EACH"), + // USP Leste + Each(13, "EACH"), - // Baurú - Bauru(20, "Bauru"), + // Baurú + Bauru(20, "Bauru"), - // Luiz de Queiroz - Piracicaba(1, "Piracicaba"), + // Luiz de Queiroz + Piracicaba(1, "Piracicaba"), - // Fernando Costa - Pirassununga(5, "Pirassununga"), + // Fernando Costa + Pirassununga(5, "Pirassununga"), - // São Carlos - Crhea(4, "Restaurante CRHEA"), - RestauranteArea1(2, "Restaurante área 1"), - RestauranteArea2(3, "Restaurante área 2"), + // São Carlos + Crhea(4, "Restaurante CRHEA"), + RestauranteArea1(2, "Restaurante área 1"), + RestauranteArea2(3, "Restaurante área 2"), - // Ribeirão - CentralRibeirao(19, "Restaurante Central -Campus RP"), + // Ribeirão + CentralRibeirao(19, "Restaurante Central -Campus RP"), - // Lorena - Eel1(17, "EEL - Área I"), - Eel2(23, "EEL - Área II"),; + // Lorena + Eel1(17, "EEL - Área I"), + Eel2(23, "EEL - Área II"), ; - companion object { - fun getById(id: Int) = entries.first { it.id == id } - } + companion object { + fun getById(id: Int) = entries.first { it.id == id } + } } diff --git a/restaurants/src/main/kotlin/app/jopiter/restaurants/model/RestaurantItem.kt b/restaurants/src/main/kotlin/app/jopiter/restaurants/model/RestaurantItem.kt index 0d319e8..7587ff9 100644 --- a/restaurants/src/main/kotlin/app/jopiter/restaurants/model/RestaurantItem.kt +++ b/restaurants/src/main/kotlin/app/jopiter/restaurants/model/RestaurantItem.kt @@ -23,14 +23,14 @@ import com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING import java.time.LocalDate data class RestaurantItem( - val restaurantId: Int, - @JsonFormat(pattern = "yyyy-MM-dd", shape = STRING) val date: LocalDate, - val period: Period, - val calories: Int?, - val mainItem: String?, - val vegetarianItem: String?, - val dessertItem: String?, - val mundaneItems: List, - val unparsedMenu: String, - val restaurantName: String = Restaurant.getById(restaurantId).name + val restaurantId: Int, + @JsonFormat(pattern = "yyyy-MM-dd", shape = STRING) val date: LocalDate, + val period: Period, + val calories: Int?, + val mainItem: String?, + val vegetarianItem: String?, + val dessertItem: String?, + val mundaneItems: List, + val unparsedMenu: String, + val restaurantName: String = Restaurant.getById(restaurantId).name ) diff --git a/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/RestaurantItemRepository.kt b/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/RestaurantItemRepository.kt index f0ee10a..3af02d4 100644 --- a/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/RestaurantItemRepository.kt +++ b/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/RestaurantItemRepository.kt @@ -26,20 +26,20 @@ import java.time.LocalDate @Repository class RestaurantItemRepository( - private val uspRestaurantItemRepository: USPRestaurantItemRepository, - private val postgresRestaurantItemRepository: PostgresRestaurantItemRepository + private val uspRestaurantItemRepository: USPRestaurantItemRepository, + private val postgresRestaurantItemRepository: PostgresRestaurantItemRepository ) { - fun get(restaurantId: Int, dates: Set) = dates.flatMap { fetch(restaurantId, it) }.toSet() + fun get(restaurantId: Int, dates: Set) = dates.flatMap { fetch(restaurantId, it) }.toSet() - private fun fetch(restaurantId: Int, date: LocalDate) = - fetchFromPostgres(restaurantId, date) + private fun fetch(restaurantId: Int, date: LocalDate) = + fetchFromPostgres(restaurantId, date) - fun fetchFromUsp(restaurantId: Int): Set { - val items = uspRestaurantItemRepository.fetch(restaurantId) - postgresRestaurantItemRepository.put(items) - return items - } + fun fetchFromUsp(restaurantId: Int): Set { + val items = uspRestaurantItemRepository.fetch(restaurantId) + postgresRestaurantItemRepository.put(items) + return items + } - private fun fetchFromPostgres(restaurantId: Int, date: LocalDate) = - postgresRestaurantItemRepository.get(restaurantId, date) + private fun fetchFromPostgres(restaurantId: Int, date: LocalDate) = + postgresRestaurantItemRepository.get(restaurantId, date) } diff --git a/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/postgres/KtormConfiguration.kt b/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/postgres/KtormConfiguration.kt index b58c467..679ab64 100644 --- a/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/postgres/KtormConfiguration.kt +++ b/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/postgres/KtormConfiguration.kt @@ -9,5 +9,6 @@ import javax.sql.DataSource class KtormConfiguration( private val datasource: DataSource ) { - @Bean fun database() = Database.connectWithSpringSupport(datasource) -} \ No newline at end of file + @Bean + fun database() = Database.connectWithSpringSupport(datasource) +} diff --git a/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/postgres/PostgresRestaurantItemRepository.kt b/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/postgres/PostgresRestaurantItemRepository.kt index 0f84f17..6037302 100644 --- a/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/postgres/PostgresRestaurantItemRepository.kt +++ b/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/postgres/PostgresRestaurantItemRepository.kt @@ -9,15 +9,12 @@ import org.ktorm.entity.add import org.ktorm.entity.filter import org.ktorm.entity.map import org.ktorm.entity.sequenceOf -import org.ktorm.entity.toList -import org.ktorm.entity.toSet import org.ktorm.entity.update import org.ktorm.schema.Table import org.ktorm.schema.date import org.ktorm.schema.enum import org.ktorm.schema.int import org.ktorm.schema.varchar -import org.ktorm.support.postgresql.insertOrUpdate import org.ktorm.support.postgresql.textArray import org.slf4j.LoggerFactory import org.springframework.stereotype.Repository @@ -37,7 +34,7 @@ class PostgresRestaurantItemRepository( fun put(restaurantItem: RestaurantItem) { val items = get(restaurantItem.restaurantId, restaurantItem.date, restaurantItem.period) - if(items.isEmpty()) { + if (items.isEmpty()) { restaurantItems.add(restaurantItem.toEntity()) } else { restaurantItems.update(restaurantItem.toEntity()) @@ -46,7 +43,7 @@ class PostgresRestaurantItemRepository( fun get(restaurantId: Int, date: LocalDate, period: Period? = null): Set { val query = restaurantItems.filter { it.date eq date }.filter { it.restaurantId eq restaurantId } - if(period != null) query.filter { it.period eq period } + if (period != null) query.filter { it.period eq period } return query.map { it.toItem() }.toSet() } diff --git a/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/usp/MenuParser.kt b/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/usp/MenuParser.kt index c9d02c1..3e3be75 100644 --- a/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/usp/MenuParser.kt +++ b/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/usp/MenuParser.kt @@ -26,150 +26,153 @@ import java.time.LocalDate import kotlin.text.RegexOption.IGNORE_CASE fun interface MenuParser { - fun parse(menu: String): Menu + fun parse(menu: String): Menu } val luizDeQueirozParser = MenuParser { - val items = it.cleanItems() - if(items.size < 3) return@MenuParser closedMenuParser.parse(it) - val (main, veg, dessert) = items.find("Prato principal:", "Opção Vegetariana:", "Sobremesa:") - Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) + val items = it.cleanItems() + if (items.size < 3) return@MenuParser closedMenuParser.parse(it) + val (main, veg, dessert) = items.find("Prato principal:", "Opção Vegetariana:", "Sobremesa:") + Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) } val centralSaoCarlosParser = MenuParser { - val items = it.cleanItems() - if(items.size < 3) return@MenuParser closedMenuParser.parse(it) - val (veg, dessert) = items.find("principal:", "Sobremesa:") - val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() - Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) + val items = it.cleanItems() + if (items.size < 3) return@MenuParser closedMenuParser.parse(it) + val (veg, dessert) = items.find("principal:", "Sobremesa:") + val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() + Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) } val pirassunungaParser = MenuParser { - val items = it.cleanItems() - if(items.size < 3) return@MenuParser closedMenuParser.parse(it) - val (main, veg, des) = items.find("Prato Principal:", "Opção Vegetariana:", "Sobremesa:") - Menu(main, veg, des, items.cleanStrings(main, veg, des), it) + val items = it.cleanItems() + if (items.size < 3) return@MenuParser closedMenuParser.parse(it) + val (main, veg, des) = items.find("Prato Principal:", "Opção Vegetariana:", "Sobremesa:") + Menu(main, veg, des, items.cleanStrings(main, veg, des), it) } val centralParser = MenuParser { - val items = it.cleanItems() - if(items.size < 3) return@MenuParser closedMenuParser.parse(it) - val veg = items.find("Opção:").single() - val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() - val dessert = items.getOrNull(items.lastIndex - 2)?.cleanString() - Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) + val items = it.cleanItems() + if (items.size < 3) return@MenuParser closedMenuParser.parse(it) + val veg = items.find("Opção:").single() + val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() + val dessert = items.getOrNull(items.lastIndex - 2)?.cleanString() + Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) } val quadrilateroParser = MenuParser { - val items = it.cleanItems() - if(items.size < 3) return@MenuParser closedMenuParser.parse(it) - val veg = items.find("Opção:").single() - val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() - val dessert = items.getOrNull(items.lastIndex - 3)?.cleanString() - Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) + val items = it.cleanItems() + if (items.size < 3) return@MenuParser closedMenuParser.parse(it) + val veg = items.find("Opção:").single() + val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() + val dessert = items.getOrNull(items.lastIndex - 3)?.cleanString() + Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) } val eachParser = MenuParser { - val items = it.cleanItems() - if(items.size < 3) return@MenuParser closedMenuParser.parse(it) - val veg = items.find("Opção:").single() - val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() - val dessert = items.getOrNull(items.lastIndex - 4)?.cleanString() - Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) + val items = it.cleanItems() + if (items.size < 3) return@MenuParser closedMenuParser.parse(it) + val veg = items.find("Opção:").single() + val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() + val dessert = items.getOrNull(items.lastIndex - 4)?.cleanString() + Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) } val largoSaoFranciscoParser = MenuParser { - val items = it.cleanItems() - if(items.size < 3) return@MenuParser closedMenuParser.parse(it) - val veg = items.find("Opção:").single() - val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() - val dessert = items.getOrNull(items.lastIndex - 3)?.cleanString() - Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) + val items = it.cleanItems() + if (items.size < 3) return@MenuParser closedMenuParser.parse(it) + val veg = items.find("Opção:").single() + val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() + val dessert = items.getOrNull(items.lastIndex - 3)?.cleanString() + Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) } val centralRibeiraoParser = MenuParser { - val items = it.cleanItems() - if(items.size < 3) return@MenuParser closedMenuParser.parse(it) - val veg = items.find("veg:").single() - val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() - val des = items.getOrNull(items.lastIndex - 4)?.cleanString() - Menu(main, veg, des, items.cleanStrings(main, veg, des), it) + val items = it.cleanItems() + if (items.size < 3) return@MenuParser closedMenuParser.parse(it) + val veg = items.find("veg:").single() + val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() + val des = items.getOrNull(items.lastIndex - 4)?.cleanString() + Menu(main, veg, des, items.cleanStrings(main, veg, des), it) } val bauruParser = MenuParser { - val items = it.cleanItems() - if(items.size < 3) return@MenuParser closedMenuParser.parse(it) - val main = items[0].cleanString() - val veg = items[1].cleanString() - val dessert = items[items.lastIndex - 1].cleanString() - Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) + val items = it.cleanItems() + if (items.size < 3) return@MenuParser closedMenuParser.parse(it) + val main = items[0].cleanString() + val veg = items[1].cleanString() + val dessert = items[items.lastIndex - 1].cleanString() + Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) } val lorenaParser = MenuParser { - val items = it.cleanItems() - if(items.size < 3) return@MenuParser closedMenuParser.parse(it) - val veg = items.find("Opção:").single() - val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() - val dessert = items.getOrNull(items.lastIndex - 2)?.cleanString() - Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) + val items = it.cleanItems() + if (items.size < 3) return@MenuParser closedMenuParser.parse(it) + val veg = items.find("Opção:").single() + val main = items.getOrNull(items.findIndex(veg) - 1)?.cleanString() + val dessert = items.getOrNull(items.lastIndex - 2)?.cleanString() + Menu(main, veg, dessert, items.cleanStrings(main, veg, dessert), it) } val closedMenuParser = MenuParser { Menu(null, null, null, emptyList(), it) } private fun List.find(vararg containing: String) = - filter { str -> containing.any { str.contains(it, true) } }.map { it.cleanString() } + filter { str -> containing.any { str.contains(it, true) } }.map { it.cleanString() } -private fun List.findIndex(value: String?) = if(value == null) -1 else indexOfFirst { it.contains(value, true) } +private fun List.findIndex(value: String?) = + if (value == null) -1 else indexOfFirst { it.contains(value, true) } private fun List.cleanStrings(vararg specialItems: String?) = - (map { it.cleanString() } - specialItems.toSet()).filterNot { it.isNullOrBlank() }.filterNotNull() + (map { it.cleanString() } - specialItems.toSet()).filterNot { it.isNullOrBlank() }.filterNotNull() private fun String.cleanItems() = - replace("\n(", " (") - .split("\n", "/") - .map { it.replace(Regex("[()]*\\d*,*\\.*\\d*\\s*kcal[(())]*", IGNORE_CASE), "") } - .map { it.trim(',') } - .filter { it.isNotBlank() } - .filterNot { it.contains("Marmitex", true) } + replace("\n(", " (") + .split("\n", "/") + .map { it.replace(Regex("[()]*\\d*,*\\.*\\d*\\s*kcal[(())]*", IGNORE_CASE), "") } + .map { it.trim(',') } + .filter { it.isNotBlank() } + .filterNot { it.contains("Marmitex", true) } private fun String.cleanString() = - replace("c/", "com", true).substringAfter(":").trim().lowercase().replaceFirstChar { it.titlecase() } + replace("c/", "com", true).substringAfter(":").trim().lowercase().replaceFirstChar { it.titlecase() } data class Menu( - val mainItem: String?, - val vegetarianItem: String?, - val dessertItem: String?, - val mundaneItems: List, - val unparsedMenu: String + val mainItem: String?, + val vegetarianItem: String?, + val dessertItem: String?, + val mundaneItems: List, + val unparsedMenu: String ) @Suppress("FunctionNaming") fun RestaurantItem(restaurantId: Int, date: LocalDate, period: Period, calories: Int?, menu: Menu) = menu.run { - RestaurantItem( - restaurantId, date, period, calories, mainItem, vegetarianItem, dessertItem, mundaneItems, unparsedMenu - ) + RestaurantItem( + restaurantId, date, period, calories, mainItem, vegetarianItem, dessertItem, mundaneItems, unparsedMenu + ) } -@Configuration class ParsersConfig { - @Bean fun parsers() = parsers +@Configuration +class ParsersConfig { + @Bean + fun parsers() = parsers } val parsers: Map = mapOf( - 1 to luizDeQueirozParser, - 2 to centralSaoCarlosParser, - 3 to centralSaoCarlosParser, - 5 to pirassunungaParser, - 6 to centralParser, - 7 to centralParser, - 8 to centralParser, - 9 to centralParser, - 11 to quadrilateroParser, - 12 to quadrilateroParser, - 13 to eachParser, - 14 to largoSaoFranciscoParser, - 17 to lorenaParser, - 19 to centralRibeiraoParser, - 20 to bauruParser, - 23 to lorenaParser, + 1 to luizDeQueirozParser, + 2 to centralSaoCarlosParser, + 3 to centralSaoCarlosParser, + 5 to pirassunungaParser, + 6 to centralParser, + 7 to centralParser, + 8 to centralParser, + 9 to centralParser, + 11 to quadrilateroParser, + 12 to quadrilateroParser, + 13 to eachParser, + 14 to largoSaoFranciscoParser, + 17 to lorenaParser, + 19 to centralRibeiraoParser, + 20 to bauruParser, + 23 to lorenaParser, ) + listOf(4, 10).map { it to closedMenuParser } diff --git a/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/usp/USPRestaurantItemRepository.kt b/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/usp/USPRestaurantItemRepository.kt index d8704fe..607a754 100644 --- a/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/usp/USPRestaurantItemRepository.kt +++ b/restaurants/src/main/kotlin/app/jopiter/restaurants/repository/usp/USPRestaurantItemRepository.kt @@ -33,40 +33,40 @@ private const val RequestTimeoutMillis = 2_000 @Repository class USPRestaurantItemRepository( - @Value("\${usp.base.url}") val uspBaseUrl: String, - private val parsers: Map, - @Value("\${usp.hash}")private val uspHash: String + @Value("\${usp.base.url}") val uspBaseUrl: String, + private val parsers: Map, + @Value("\${usp.hash}") private val uspHash: String ) { - fun fetch(restaurantId: Int): Set { - val (_, _, result) = "$uspBaseUrl/menu/$restaurantId" - .httpPost(listOf("hash" to uspHash)) - .timeoutRead(RequestTimeoutMillis) - .responseObject() + fun fetch(restaurantId: Int): Set { + val (_, _, result) = "$uspBaseUrl/menu/$restaurantId" + .httpPost(listOf("hash" to uspHash)) + .timeoutRead(RequestTimeoutMillis) + .responseObject() - return result.fold( - { it.toRestaurantItems(restaurantId, parsers[restaurantId] ?: return@fold emptySet()) }, - { if (it.causedByInterruption) emptySet() else throw it } - ) - } + return result.fold( + { it.toRestaurantItems(restaurantId, parsers[restaurantId] ?: return@fold emptySet()) }, + { if (it.causedByInterruption) emptySet() else throw it } + ) + } - private class MenuResponse(val meals: List) { + private class MenuResponse(val meals: List) { - fun toRestaurantItems(restaurantId: Int, parser: MenuParser) = meals.flatMap { - try { - listOf( - RestaurantItem(restaurantId, it.localDate, Lunch, it.lunch.calories, parser.parse(it.lunch.menu)), - RestaurantItem(restaurantId, it.localDate, Dinner, it.dinner.calories, parser.parse(it.dinner.menu)) - ) - } catch (_: Exception) { - emptySet() - } - }.toSet() - } + fun toRestaurantItems(restaurantId: Int, parser: MenuParser) = meals.flatMap { + try { + listOf( + RestaurantItem(restaurantId, it.localDate, Lunch, it.lunch.calories, parser.parse(it.lunch.menu)), + RestaurantItem(restaurantId, it.localDate, Dinner, it.dinner.calories, parser.parse(it.dinner.menu)) + ) + } catch (_: Exception) { + emptySet() + } + }.toSet() + } - private data class MenuMeals(val dinner: MenuPeriod, val lunch: MenuPeriod, val date: String) { - val localDate: LocalDate by lazy { parse(date, ofPattern("dd/MM/yyyy")) } - } + private data class MenuMeals(val dinner: MenuPeriod, val lunch: MenuPeriod, val date: String) { + val localDate: LocalDate by lazy { parse(date, ofPattern("dd/MM/yyyy")) } + } - private data class MenuPeriod(val menu: String, val calories: Int? = null) + private data class MenuPeriod(val menu: String, val calories: Int? = null) } diff --git a/restaurants/src/main/resources/application-restaurants.properties b/restaurants/src/main/resources/application-restaurants.properties index a28ad34..97d75af 100644 --- a/restaurants/src/main/resources/application-restaurants.properties +++ b/restaurants/src/main/resources/application-restaurants.properties @@ -15,6 +15,5 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # - usp.base.url=https://uspdigital.usp.br/rucard/servicos usp.hash=596df9effde6f877717b4e81fdb2ca9f \ No newline at end of file diff --git a/restaurants/src/main/resources/db/migration/V0__Create_Database.sql b/restaurants/src/main/resources/db/migration/V0__Create_Database.sql index 043471b..314fd5a 100644 --- a/restaurants/src/main/resources/db/migration/V0__Create_Database.sql +++ b/restaurants/src/main/resources/db/migration/V0__Create_Database.sql @@ -2,16 +2,16 @@ CREATE TYPE period AS ENUM ('Lunch', 'Dinner'); CREATE TABLE restaurant_item ( - restaurant_id int not null, - restaurant_name text not null, - date date not null, - period period not null, - calories int null default null, - main_item text null default null, - vegetarian_item text null default null, - dessert_item text null default null, + restaurant_id int not null, + restaurant_name text not null, + date date not null, + period period not null, + calories int null default null, + main_item text null default null, + vegetarian_item text null default null, + dessert_item text null default null, mundane_items text[] not null default ARRAY[]::text[], - unparsed_menu text not null, + unparsed_menu text not null, PRIMARY KEY (restaurant_id, date, period) ); \ No newline at end of file diff --git a/restaurants/src/test/kotlin/app/jopiter/restaurants/classifier/ProteinClassifierTest.kt b/restaurants/src/test/kotlin/app/jopiter/restaurants/classifier/ProteinClassifierTest.kt index f104959..010799c 100644 --- a/restaurants/src/test/kotlin/app/jopiter/restaurants/classifier/ProteinClassifierTest.kt +++ b/restaurants/src/test/kotlin/app/jopiter/restaurants/classifier/ProteinClassifierTest.kt @@ -10,10 +10,15 @@ class ProteinClassifierTest : FunSpec({ test("Smoke test") { val result = target.classify("Almôndega acebolada ao shoyo") - result shouldBe ProteinClassification("bovina", "ao molho leve", "alcatra, almôndega, bife de contra filé, bife de patinho, coxão duro, lagarto, patinho/coxão mole", "Marrom/Bege") + result shouldBe ProteinClassification( + "bovina", + "ao molho leve", + "alcatra, almôndega, bife de contra filé, bife de patinho, coxão duro, lagarto, patinho/coxão mole", + "Marrom/Bege" + ) result.foodGroup shouldBe "bovina" result.preparation shouldBe "ao molho leve" } -}) \ No newline at end of file +}) diff --git a/restaurants/src/test/kotlin/app/jopiter/restaurants/classifier/RestaurantItemClassifierTest.kt b/restaurants/src/test/kotlin/app/jopiter/restaurants/classifier/RestaurantItemClassifierTest.kt index 98fa149..db726de 100644 --- a/restaurants/src/test/kotlin/app/jopiter/restaurants/classifier/RestaurantItemClassifierTest.kt +++ b/restaurants/src/test/kotlin/app/jopiter/restaurants/classifier/RestaurantItemClassifierTest.kt @@ -5,16 +5,16 @@ import io.kotest.core.spec.style.FunSpec class RestaurantItemClassifierTest : FunSpec({ - val proteinClassifier = ProteinClassifier("classified_items/protein.csv".loadCsv()) - val vegetarianClassifier = VegetarianClassifier("classified_items/vegetarian.csv".loadCsv()) - val dessertClassifier = DessertClassifier("classified_items/dessert.csv".loadCsv()) + val proteinClassifier = ProteinClassifier("classified_items/protein.csv".loadCsv()) + val vegetarianClassifier = VegetarianClassifier("classified_items/vegetarian.csv".loadCsv()) + val dessertClassifier = DessertClassifier("classified_items/dessert.csv".loadCsv()) - val target = RestaurantItemClassifier(proteinClassifier, vegetarianClassifier, dessertClassifier) + val target = RestaurantItemClassifier(proteinClassifier, vegetarianClassifier, dessertClassifier) - test("Warms up without trowing exceptions") { - shouldNotThrowAny { target.warmupModel() } - } + test("Warms up without trowing exceptions") { + shouldNotThrowAny { target.warmupModel() } + } }) diff --git a/restaurants/src/test/kotlin/app/jopiter/restaurants/model/RestaurantTest.kt b/restaurants/src/test/kotlin/app/jopiter/restaurants/model/RestaurantTest.kt index 4b9ec8d..b772895 100644 --- a/restaurants/src/test/kotlin/app/jopiter/restaurants/model/RestaurantTest.kt +++ b/restaurants/src/test/kotlin/app/jopiter/restaurants/model/RestaurantTest.kt @@ -30,32 +30,32 @@ import io.kotest.matchers.shouldBe @Tags("Daily") class RestaurantTest : FunSpec({ - context("Restaurants model reflects what USP returns") { - val restaurantGroups = fetchRestaurantGroupsFromUSP() - - test("Should have same number of campi") { - Campus.values() shouldBeSameSizeAs restaurantGroups - } - - test("Should have a 1 to 1 mapping between USP's response and our model") { - val allRestaurants = restaurantGroups.flatMap { it.restaurants } - allRestaurants.forAll { (alias, id) -> - Restaurant.getById(id).restaurantName shouldBe alias - } - } + context("Restaurants model reflects what USP returns") { + val restaurantGroups = fetchRestaurantGroupsFromUSP() + + test("Should have same number of campi") { + Campus.values() shouldBeSameSizeAs restaurantGroups } - test("Should return restaurant by id") { - val restaurant = Restaurant.values().random() - Restaurant.getById(restaurant.id) shouldBe restaurant + test("Should have a 1 to 1 mapping between USP's response and our model") { + val allRestaurants = restaurantGroups.flatMap { it.restaurants } + allRestaurants.forAll { (alias, id) -> + Restaurant.getById(id).restaurantName shouldBe alias + } } + } + + test("Should return restaurant by id") { + val restaurant = Restaurant.values().random() + Restaurant.getById(restaurant.id) shouldBe restaurant + } }) private fun fetchRestaurantGroupsFromUSP() = - "https://uspdigital.usp.br/rucard/servicos/restaurants".httpPost() - .header(Headers.CONTENT_TYPE, "application/x-www-form-urlencoded") - .body("hash=596df9effde6f877717b4e81fdb2ca9f") - .responseObject>().third.get() + "https://uspdigital.usp.br/rucard/servicos/restaurants".httpPost() + .header(Headers.CONTENT_TYPE, "application/x-www-form-urlencoded") + .body("hash=596df9effde6f877717b4e81fdb2ca9f") + .responseObject>().third.get() data class RestaurantGroup(val name: String, val restaurants: List) data class IndividualRestaurant(val alias: String, val id: Int) diff --git a/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/RestaurantItemRepositoryTest.kt b/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/RestaurantItemRepositoryTest.kt index 6845125..c68e61a 100644 --- a/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/RestaurantItemRepositoryTest.kt +++ b/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/RestaurantItemRepositoryTest.kt @@ -29,66 +29,69 @@ import io.kotest.core.spec.style.ShouldSpec import io.kotest.extensions.testcontainers.JdbcTestContainerExtension import io.kotest.extensions.time.ConstantNowTestListener import io.kotest.matchers.shouldBe -import io.mockk.called import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.flywaydb.core.Flyway import org.ktorm.database.Database import org.testcontainers.containers.PostgreSQLContainer -import java.time.DayOfWeek.* +import java.time.DayOfWeek.WEDNESDAY import java.time.LocalDate import java.time.LocalDate.now class RestaurantItemRepositoryIntegrationTest : ShouldSpec({ - val postgres = install(JdbcTestContainerExtension(PostgreSQLContainer("postgres:16"))) - val flyway = Flyway.configure().cleanDisabled(false).dataSource(postgres).load() + val postgres = install(JdbcTestContainerExtension(PostgreSQLContainer("postgres:16"))) + val flyway = Flyway.configure().cleanDisabled(false).dataSource(postgres).load() - beforeSpec { - flyway.clean() - flyway.migrate() - } + beforeSpec { + flyway.clean() + flyway.migrate() + } - val postgresRepository = PostgresRestaurantItemRepository(Database.connect(postgres)) + val postgresRepository = PostgresRestaurantItemRepository(Database.connect(postgres)) - val uspRepository = USPRestaurantItemRepository("https://uspdigital.usp.br/rucard/servicos", parsers, "596df9effde6f877717b4e81fdb2ca9f") + val uspRepository = USPRestaurantItemRepository( + "https://uspdigital.usp.br/rucard/servicos", + parsers, + "596df9effde6f877717b4e81fdb2ca9f" + ) - val target = RestaurantItemRepository(uspRepository, postgresRepository) + val target = RestaurantItemRepository(uspRepository, postgresRepository) - should("Persist all items to the database") { - val firstItemResult = target.get(13, setOf(LocalDate.of(2024, 3, 4))) - val secondItemResult = target.get(13, setOf(LocalDate.of(2024, 3, 4))) + should("Persist all items to the database") { + val firstItemResult = target.get(13, setOf(LocalDate.of(2024, 3, 4))) + val secondItemResult = target.get(13, setOf(LocalDate.of(2024, 3, 4))) - firstItemResult shouldBe secondItemResult - } + firstItemResult shouldBe secondItemResult + } }) class RestaurantItemRepositoryTest : ShouldSpec({ - val uspRepository = mockk() - val postgresRepository = mockk(relaxed = true) + val uspRepository = mockk() + val postgresRepository = mockk(relaxed = true) - val target = RestaurantItemRepository(uspRepository, postgresRepository) + val target = RestaurantItemRepository(uspRepository, postgresRepository) - listener(ConstantNowTestListener(now().with(WEDNESDAY))) + listener(ConstantNowTestListener(now().with(WEDNESDAY))) - should("Save any value fetched from USP to Postgres") { - val today = now() - val tomorrow = today.plusDays(1) + should("Save any value fetched from USP to Postgres") { + val today = now() + val tomorrow = today.plusDays(1) - every { uspRepository.fetch(1) } returns setOf(dummyRestaurantItem(today), dummyRestaurantItem(tomorrow)) + every { uspRepository.fetch(1) } returns setOf(dummyRestaurantItem(today), dummyRestaurantItem(tomorrow)) - target.fetchFromUsp(1) + target.fetchFromUsp(1) - verify(exactly = 1) { postgresRepository.put(setOf(dummyRestaurantItem(today), dummyRestaurantItem(tomorrow))) } - } + verify(exactly = 1) { postgresRepository.put(setOf(dummyRestaurantItem(today), dummyRestaurantItem(tomorrow))) } + } - isolationMode = InstancePerTest + isolationMode = InstancePerTest }) private fun dummyRestaurantItem(date: LocalDate) = - RestaurantItem(1, date, Lunch, 0, "main", "veg", "des", emptyList(), "") \ No newline at end of file + RestaurantItem(1, date, Lunch, 0, "main", "veg", "des", emptyList(), "") diff --git a/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/usp/MenuParsersTests.kt b/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/usp/MenuParsersTests.kt index 8449656..75ca835 100644 --- a/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/usp/MenuParsersTests.kt +++ b/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/usp/MenuParsersTests.kt @@ -24,267 +24,267 @@ import io.kotest.matchers.shouldBe class MenuParsersTests : FunSpec({ - examples.forEach { - context("Parser for menu ${it.restaurant}") { - val parser = parsers.getValue(it.restaurant) - val parsed = parser.parse(it.text) + examples.forEach { + context("Parser for menu ${it.restaurant}") { + val parser = parsers.getValue(it.restaurant) + val parsed = parser.parse(it.text) - test("Main item parse") { - parsed.mainItem shouldBe it.main - } + test("Main item parse") { + parsed.mainItem shouldBe it.main + } - test("Vegetarian item parse") { - parsed.vegetarianItem shouldBe it.vegetarian - } + test("Vegetarian item parse") { + parsed.vegetarianItem shouldBe it.vegetarian + } - test("Dessert item parse") { - parsed.dessertItem shouldBe it.dessert - } + test("Dessert item parse") { + parsed.dessertItem shouldBe it.dessert + } - xtest("Mundane item parse") { - parsed.mundaneItems shouldContainExactlyInAnyOrder it.mundane.toList() - } - } + xtest("Mundane item parse") { + parsed.mundaneItems shouldContainExactlyInAnyOrder it.mundane.toList() + } } + } }) private val examples = listOf( - Example( - 1, - "Acompanhamentos: Arroz / Feijão / Arroz Integral\nPrato principal: Frango assado\nOpção Vegetariana: Ovo pizzaiolo (lactose),\nGuarnição: Polenta\nSaladas: Catalonha, cenoura c/ beterraba\nSobremesa: Melão / Suco: Laranja", - "Frango assado", - "Ovo pizzaiolo (lactose)", - "Melão", - "" - ), - Example( - 1, - "Acompanhamentos: Arroz / Feijão Preto / Arroz Integral\nPrato principal: Bife de panela\nOpção Vegetariana: PTS ao sugo\nGuarnição: Macarrão alho e óleo (glúten),\nSaladas: Repolho roxo, abóbora\nSobremesa: Gelatina de limão / banana / Suco: Laranja", - "Bife de panela", - "Pts ao sugo", - "Gelatina de limão", - "" - ), - Example( - 2, - "Arroz/Feijão/ Arroz integral/Saladas: Diversas/Filé de coxa Assado/Opção do Prato Principal: PVT alemão/Polenta cremosa/Sobremesa: Romeu e Julieta/ Abacaxi/Mini Pão e Suco", - "Filé de coxa assado", - "Pvt alemão", - "Romeu e julieta", - "" - ), - Example( - 2, - "Arroz/Feijão/ Arroz integral/Saladas: Diversas/Estrogonofe de carne /Opção do Prato Principal: Estrogonofe de grão de bico/Batata palha/Sobremesa: Banana caramelada/ Mamão/Mini Pão e Suco", - "Estrogonofe de carne", - "Estrogonofe de grão de bico", - "Banana caramelada", - "" - ), - Example( - 3, - "Arroz/Feijão/ Arroz integral/Saladas: Diversas/Filé de coxa Assado/Opção do Prato Principal: PVT alemão/Polenta cremosa/Sobremesa: Romeu e Julieta/ Abacaxi/Mini Pão e Suco", - "Filé de coxa assado", - "Pvt alemão", - "Romeu e julieta", - "" - ), - Example(3, "Fechado", null, null, null), - Example( - 5, - "Arroz branco/Arroz integral/Feijão carioca\nPrato principal: Carne assada ao molho madeira\nOpção vegetariana: Bolinho de lentilha (CONTÉM GLÚTEN), \nGuarnição: Purê de batata \nSalada: Alface - Cenoura - Abóbora cozida \nSobremesa: Paçoca\nSuco de Morango e Mini pão francês", - "Carne assada ao molho madeira", - "Bolinho de lentilha (contém glúten),", - "Paçoca", - "" - ), - Example( - 5, - "Arroz branco/Arroz integral/Feijão carioca\nPrato principal: Peixe a portuguesa\nOpção vegetariana: Escondidinho de PTS \nGuarnição: Creme de cenoura\nSalada: Alface - Pepino - Beterraba cozida \nSobremesa: Paçoca\nSuco de Morango e Mini pão francês", - "Peixe a portuguesa", - "Escondidinho de pts", - "Paçoca", - "" - ), - Example( - 6, - "Arroz / feijão / arroz integral\nFrango assado\nOpção: Ervilha com legumes\nRepolho refogado\nSalada de beterraba\nPão de mel\nMinipão / refresco", - "Frango assado", - "Ervilha com legumes", - "Pão de mel", - "" - ), - Example( - 6, - "Arroz / feijão preto / arroz integral\nBife de caçarola com molho ferrugem\nOpção: PVT com vagem\nBatata doce corada\nSalada de alface\nBanana\nMinipão / refresco", - "Bife de caçarola com molho ferrugem", - "Pvt com vagem", - "Banana", - "" - ), - Example( - 7, - "Arroz / feijão / arroz integral\nFilé de de peito de frango à pizzaiolo\nOpção: Omelete com alho-poró\nLegumes à juliana\nSalada de agrião\nSagú de maracujá\nMinipão / refresco", - "Filé de de peito de frango à pizzaiolo", - "Omelete com alho-poró", - "Sagú de maracujá", - "" - ), - Example(7, "Fechado", null, null, null, "mudane"), - Example( - 8, - "Arroz / feijão / arroz integral\nFrango xadrez\nOpção: Bolinho de PVT\nAbobrinha com manjericão\nSalada de escarola\nRomeu e julieta\nMinipão / refresco", - "Frango xadrez", - "Bolinho de pvt", - "Romeu e julieta", - - ), - Example( - 8, - "Arroz / feijão / arroz integral\nLombo com molho de ervas\nOpção: PVT com cogumelos e tomate\nCenoura com vagem\nSalada de pepino\nLaranja\nMinipão / refresco", - "Lombo com molho de ervas", - "Pvt com cogumelos e tomate", - "Laranja", - "" - ), - Example( - 9, - "Arroz / feijão / arroz integral\nLagarto com molho ferrugem\nOpção: PVT com alho-poró\nCenoura com vagem\nSalada de almeirão\nGelatina de uva \nMinipão / refresco", - "Lagarto com molho ferrugem", - "Pvt com alho-poró", - "Gelatina de uva", - "" - ), - Example( - 9, - "Arroz / feijão / arroz integral\nLinguiça à escabeche\nOpção: Lasanha de brócolis com ricota\nQuiabo refogado\nSalada de escarola\nBanana \nMinipão / refresco", - "Linguiça à escabeche", - "Lasanha de brócolis com ricota", - "Banana", - "" - ), - Example( - 11, - "Arroz / feijão preto / arroz integral\nCupim assado com molho madeira\nOpção: Hambúrguer de feijão branco\nAbóbora ao forno\nSalada de mix de folhas\nCurau\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", - "Cupim assado com molho madeira", - "Hambúrguer de feijão branco", - "Curau", - "" - ), - Example( - 11, - "Arroz / feijão / arroz integral\nFilé de coxa de frango com molho de gergelim\nOpção: PVT com gergelim\nBerinjela com uva passas\nSalada de alface\nMamão\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", - "Filé de coxa de frango com molho de gergelim", - "Pvt com gergelim", - "Mamão", - "" - ), - Example( - 12, - "Arroz / feijão / arroz integral \nFilé de coxa de frango com molho de ervas\nOpção: Fricassê de PVT\nEscarola refogada\nSalada cenoura\nMaçã\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", - "Filé de coxa de frango com molho de ervas", - "Fricassê de pvt", - "Maçã", - "" - ), - Example(12, "FECHADO", null, null, null, "mudane"), - Example( - 13, - "Arroz / arroz integral / feijão carioca\nHambúrguer à pizzaiolo\nOpção: Hambúrguer de PVT à pizzaiolo\nBatata ao forno\nSaladas: Alface / beterraba / feijão fradinho \nMaria mole / banana\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", - "Hambúrguer à pizzaiolo", - "Hambúrguer de pvt à pizzaiolo", - "Maria mole", - "" - ), - Example( - 13, - "Arroz / arroz integral / feijão carioca\nLagarto com molho roti\nOpção: Moussaka vegetariana\nVagem sauté\nSaladas: Catalonha / cenoura cozida / grão-de-bico\nSagu / melão\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", - "Lagarto com molho roti", - "Moussaka vegetariana", - "Sagu", - "" - ), - Example( - 14, - "Arroz / arroz integral / feijão carioca\nHambúrguer a pizzaiolo \nOpção: Hambúrguer de PVT\nBatata corada\nSalada de alface\nGoiabada\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", - "Hambúrguer a pizzaiolo", - "Hambúrguer de pvt", - "Goiabada", - "" - ), - Example( - 14, - "Arroz / arroz integral / feijão carioca\nLagarto com molho roti\nOpção: Moussaka vegetariana\nVagem sauté\nSalada de catalonha\nBanana\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", - "Lagarto com molho roti", - "Moussaka vegetariana", - "Banana", - "" - ), - Example( - 17, - "Arroz / feijão preto / arroz integral\nSalsicha americana\nOpção: Grão-de-bico com tomate, cebola e salsa\nMacarrão ao alho e óleo\nSalada de alface\nGelatina com maçã\nMinipão / refresco", - "Salsicha americana", - "Grão-de-bico com tomate, cebola e salsa", - "Gelatina com maçã", - "" - ), - Example( - 17, - "Arroz / feijão preto / arroz integral\nPanqueca de carne\nOpção: Panqueca de PVT\nChuchu com milho\nSalada de repolho\nMamão\nMinipão / refresco", - "Panqueca de carne", - "Panqueca de pvt", - "Mamão", - "" - ), - Example( - 19, - "Linguiça Assada \nOp veg: PTS Clara à Primavera\nMandioca Ensopada\nAlface\nBerinjela\nMaçã\nArroz / Feijão / Integral \nSuco de Abacaxi\n808 Kcal/ 755 Kcal", - "Linguiça assada", - "Pts clara à primavera", - "Maçã", - "" - ), - Example( - 19, - "Lagarto ao Molho Madeira\nOp veg: Hambúrguer de Soja\nRepolho Refogado\nAgrião com Almeirão \nPepino \nMelão \nArroz / Feijão / Integral \nSuco de Tangerina\n762 Kcal/ 702 Kcal", - "Lagarto ao molho madeira", - "Hambúrguer de soja", - "Melão", - "" - ), - Example( - 20, - "BOLINHO DE CARNE AO MOLHO BARBECUE\n(musculo),\n152 KCAL\nLASANHA DE BERINJELA\n122KCAL\nCHUCHU REFOGADO\n 19KCAL\nALFACE\n9 kcal\nREPOLHO VERDE\n25 KCAL\nABÓBORA COZIDA\n31 KCAL\nARROZ BRANCO\n138kcal\nARROZ INTEGRAL\n116kcal\nFEIJÃO CARIOCA\n86 kcal\nMINI PÃO FRANCES\n75 kcal\nMELANCIA26 KCAL\nSUCO DE UVA\n48 kcal", - "Bolinho de carne ao molho barbecue (musculo)", - "Lasanha de berinjela", - "Melancia", - "" - ), - Example( - 20, - "LINGUIÇA CHAPEADA\n143 KCAL\nQUIBE DE ABÓBORA PAULISTA COM PTS\n177 kcal\nFAROFA DE CEBOLA\n88KCAL\nALFACE\n9 kcal\nPEPINO\n24 kcal\nJILÓ\n27 KCAL\nARROZ BRANCO\n138kcal\nARROZ INTEGRAL\n116kcal\nFEIJÃO CARIOCA\n86 kcal\nMINI PÃO FRANCES\n75 kcal\nMELANCIA\n30 KCAL\nSUCO DE UVA\n48 kcal", - "Linguiça chapeada", - "Quibe de abóbora paulista com pts", - "Melancia", - "" - ), - Example( - 23, - "Arroz / feijão preto / arroz integral\nSalsicha americana\nOpção: Grão-de-bico com tomate, cebola e salsa\nMacarrão ao alho e óleo\nSalada de alface\nGelatina com maçã\nMinipão / refresco", - "Salsicha americana", - "Grão-de-bico com tomate, cebola e salsa", - "Gelatina com maçã", - "" - ), - Example(23, "Fechado", null, null, null, ""), + Example( + 1, + "Acompanhamentos: Arroz / Feijão / Arroz Integral\nPrato principal: Frango assado\nOpção Vegetariana: Ovo pizzaiolo (lactose),\nGuarnição: Polenta\nSaladas: Catalonha, cenoura c/ beterraba\nSobremesa: Melão / Suco: Laranja", + "Frango assado", + "Ovo pizzaiolo (lactose)", + "Melão", + "" + ), + Example( + 1, + "Acompanhamentos: Arroz / Feijão Preto / Arroz Integral\nPrato principal: Bife de panela\nOpção Vegetariana: PTS ao sugo\nGuarnição: Macarrão alho e óleo (glúten),\nSaladas: Repolho roxo, abóbora\nSobremesa: Gelatina de limão / banana / Suco: Laranja", + "Bife de panela", + "Pts ao sugo", + "Gelatina de limão", + "" + ), + Example( + 2, + "Arroz/Feijão/ Arroz integral/Saladas: Diversas/Filé de coxa Assado/Opção do Prato Principal: PVT alemão/Polenta cremosa/Sobremesa: Romeu e Julieta/ Abacaxi/Mini Pão e Suco", + "Filé de coxa assado", + "Pvt alemão", + "Romeu e julieta", + "" + ), + Example( + 2, + "Arroz/Feijão/ Arroz integral/Saladas: Diversas/Estrogonofe de carne /Opção do Prato Principal: Estrogonofe de grão de bico/Batata palha/Sobremesa: Banana caramelada/ Mamão/Mini Pão e Suco", + "Estrogonofe de carne", + "Estrogonofe de grão de bico", + "Banana caramelada", + "" + ), + Example( + 3, + "Arroz/Feijão/ Arroz integral/Saladas: Diversas/Filé de coxa Assado/Opção do Prato Principal: PVT alemão/Polenta cremosa/Sobremesa: Romeu e Julieta/ Abacaxi/Mini Pão e Suco", + "Filé de coxa assado", + "Pvt alemão", + "Romeu e julieta", + "" + ), + Example(3, "Fechado", null, null, null), + Example( + 5, + "Arroz branco/Arroz integral/Feijão carioca\nPrato principal: Carne assada ao molho madeira\nOpção vegetariana: Bolinho de lentilha (CONTÉM GLÚTEN), \nGuarnição: Purê de batata \nSalada: Alface - Cenoura - Abóbora cozida \nSobremesa: Paçoca\nSuco de Morango e Mini pão francês", + "Carne assada ao molho madeira", + "Bolinho de lentilha (contém glúten),", + "Paçoca", + "" + ), + Example( + 5, + "Arroz branco/Arroz integral/Feijão carioca\nPrato principal: Peixe a portuguesa\nOpção vegetariana: Escondidinho de PTS \nGuarnição: Creme de cenoura\nSalada: Alface - Pepino - Beterraba cozida \nSobremesa: Paçoca\nSuco de Morango e Mini pão francês", + "Peixe a portuguesa", + "Escondidinho de pts", + "Paçoca", + "" + ), + Example( + 6, + "Arroz / feijão / arroz integral\nFrango assado\nOpção: Ervilha com legumes\nRepolho refogado\nSalada de beterraba\nPão de mel\nMinipão / refresco", + "Frango assado", + "Ervilha com legumes", + "Pão de mel", + "" + ), + Example( + 6, + "Arroz / feijão preto / arroz integral\nBife de caçarola com molho ferrugem\nOpção: PVT com vagem\nBatata doce corada\nSalada de alface\nBanana\nMinipão / refresco", + "Bife de caçarola com molho ferrugem", + "Pvt com vagem", + "Banana", + "" + ), + Example( + 7, + "Arroz / feijão / arroz integral\nFilé de de peito de frango à pizzaiolo\nOpção: Omelete com alho-poró\nLegumes à juliana\nSalada de agrião\nSagú de maracujá\nMinipão / refresco", + "Filé de de peito de frango à pizzaiolo", + "Omelete com alho-poró", + "Sagú de maracujá", + "" + ), + Example(7, "Fechado", null, null, null, "mudane"), + Example( + 8, + "Arroz / feijão / arroz integral\nFrango xadrez\nOpção: Bolinho de PVT\nAbobrinha com manjericão\nSalada de escarola\nRomeu e julieta\nMinipão / refresco", + "Frango xadrez", + "Bolinho de pvt", + "Romeu e julieta", + "" + ), + Example( + 8, + "Arroz / feijão / arroz integral\nLombo com molho de ervas\nOpção: PVT com cogumelos e tomate\nCenoura com vagem\nSalada de pepino\nLaranja\nMinipão / refresco", + "Lombo com molho de ervas", + "Pvt com cogumelos e tomate", + "Laranja", + "" + ), + Example( + 9, + "Arroz / feijão / arroz integral\nLagarto com molho ferrugem\nOpção: PVT com alho-poró\nCenoura com vagem\nSalada de almeirão\nGelatina de uva \nMinipão / refresco", + "Lagarto com molho ferrugem", + "Pvt com alho-poró", + "Gelatina de uva", + "" + ), + Example( + 9, + "Arroz / feijão / arroz integral\nLinguiça à escabeche\nOpção: Lasanha de brócolis com ricota\nQuiabo refogado\nSalada de escarola\nBanana \nMinipão / refresco", + "Linguiça à escabeche", + "Lasanha de brócolis com ricota", + "Banana", + "" + ), + Example( + 11, + "Arroz / feijão preto / arroz integral\nCupim assado com molho madeira\nOpção: Hambúrguer de feijão branco\nAbóbora ao forno\nSalada de mix de folhas\nCurau\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", + "Cupim assado com molho madeira", + "Hambúrguer de feijão branco", + "Curau", + "" + ), + Example( + 11, + "Arroz / feijão / arroz integral\nFilé de coxa de frango com molho de gergelim\nOpção: PVT com gergelim\nBerinjela com uva passas\nSalada de alface\nMamão\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", + "Filé de coxa de frango com molho de gergelim", + "Pvt com gergelim", + "Mamão", + "" + ), + Example( + 12, + "Arroz / feijão / arroz integral \nFilé de coxa de frango com molho de ervas\nOpção: Fricassê de PVT\nEscarola refogada\nSalada cenoura\nMaçã\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", + "Filé de coxa de frango com molho de ervas", + "Fricassê de pvt", + "Maçã", + "" + ), + Example(12, "FECHADO", null, null, null, "mudane"), + Example( + 13, + "Arroz / arroz integral / feijão carioca\nHambúrguer à pizzaiolo\nOpção: Hambúrguer de PVT à pizzaiolo\nBatata ao forno\nSaladas: Alface / beterraba / feijão fradinho \nMaria mole / banana\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", + "Hambúrguer à pizzaiolo", + "Hambúrguer de pvt à pizzaiolo", + "Maria mole", + "" + ), + Example( + 13, + "Arroz / arroz integral / feijão carioca\nLagarto com molho roti\nOpção: Moussaka vegetariana\nVagem sauté\nSaladas: Catalonha / cenoura cozida / grão-de-bico\nSagu / melão\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", + "Lagarto com molho roti", + "Moussaka vegetariana", + "Sagu", + "" + ), + Example( + 14, + "Arroz / arroz integral / feijão carioca\nHambúrguer a pizzaiolo \nOpção: Hambúrguer de PVT\nBatata corada\nSalada de alface\nGoiabada\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", + "Hambúrguer a pizzaiolo", + "Hambúrguer de pvt", + "Goiabada", + "" + ), + Example( + 14, + "Arroz / arroz integral / feijão carioca\nLagarto com molho roti\nOpção: Moussaka vegetariana\nVagem sauté\nSalada de catalonha\nBanana\nMinipão / refresco\n\n**Os Restaurantes Universitários não fornecem copos descartáveis. Tragam suas canecas.**", + "Lagarto com molho roti", + "Moussaka vegetariana", + "Banana", + "" + ), + Example( + 17, + "Arroz / feijão preto / arroz integral\nSalsicha americana\nOpção: Grão-de-bico com tomate, cebola e salsa\nMacarrão ao alho e óleo\nSalada de alface\nGelatina com maçã\nMinipão / refresco", + "Salsicha americana", + "Grão-de-bico com tomate, cebola e salsa", + "Gelatina com maçã", + "" + ), + Example( + 17, + "Arroz / feijão preto / arroz integral\nPanqueca de carne\nOpção: Panqueca de PVT\nChuchu com milho\nSalada de repolho\nMamão\nMinipão / refresco", + "Panqueca de carne", + "Panqueca de pvt", + "Mamão", + "" + ), + Example( + 19, + "Linguiça Assada \nOp veg: PTS Clara à Primavera\nMandioca Ensopada\nAlface\nBerinjela\nMaçã\nArroz / Feijão / Integral \nSuco de Abacaxi\n808 Kcal/ 755 Kcal", + "Linguiça assada", + "Pts clara à primavera", + "Maçã", + "" + ), + Example( + 19, + "Lagarto ao Molho Madeira\nOp veg: Hambúrguer de Soja\nRepolho Refogado\nAgrião com Almeirão \nPepino \nMelão \nArroz / Feijão / Integral \nSuco de Tangerina\n762 Kcal/ 702 Kcal", + "Lagarto ao molho madeira", + "Hambúrguer de soja", + "Melão", + "" + ), + Example( + 20, + "BOLINHO DE CARNE AO MOLHO BARBECUE\n(musculo),\n152 KCAL\nLASANHA DE BERINJELA\n122KCAL\nCHUCHU REFOGADO\n 19KCAL\nALFACE\n9 kcal\nREPOLHO VERDE\n25 KCAL\nABÓBORA COZIDA\n31 KCAL\nARROZ BRANCO\n138kcal\nARROZ INTEGRAL\n116kcal\nFEIJÃO CARIOCA\n86 kcal\nMINI PÃO FRANCES\n75 kcal\nMELANCIA26 KCAL\nSUCO DE UVA\n48 kcal", + "Bolinho de carne ao molho barbecue (musculo)", + "Lasanha de berinjela", + "Melancia", + "" + ), + Example( + 20, + "LINGUIÇA CHAPEADA\n143 KCAL\nQUIBE DE ABÓBORA PAULISTA COM PTS\n177 kcal\nFAROFA DE CEBOLA\n88KCAL\nALFACE\n9 kcal\nPEPINO\n24 kcal\nJILÓ\n27 KCAL\nARROZ BRANCO\n138kcal\nARROZ INTEGRAL\n116kcal\nFEIJÃO CARIOCA\n86 kcal\nMINI PÃO FRANCES\n75 kcal\nMELANCIA\n30 KCAL\nSUCO DE UVA\n48 kcal", + "Linguiça chapeada", + "Quibe de abóbora paulista com pts", + "Melancia", + "" + ), + Example( + 23, + "Arroz / feijão preto / arroz integral\nSalsicha americana\nOpção: Grão-de-bico com tomate, cebola e salsa\nMacarrão ao alho e óleo\nSalada de alface\nGelatina com maçã\nMinipão / refresco", + "Salsicha americana", + "Grão-de-bico com tomate, cebola e salsa", + "Gelatina com maçã", + "" + ), + Example(23, "Fechado", null, null, null, ""), ) private class Example( - val restaurant: Int, - val text: String, - val main: String?, - val vegetarian: String?, - val dessert: String?, - vararg val mundane: String -) \ No newline at end of file + val restaurant: Int, + val text: String, + val main: String?, + val vegetarian: String?, + val dessert: String?, + vararg val mundane: String +) diff --git a/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/usp/USPRestaurantItemRepositoryTest.kt b/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/usp/USPRestaurantItemRepositoryTest.kt index 861dfe7..5ede777 100644 --- a/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/usp/USPRestaurantItemRepositoryTest.kt +++ b/restaurants/src/test/kotlin/app/jopiter/restaurants/repository/usp/USPRestaurantItemRepositoryTest.kt @@ -21,9 +21,6 @@ package app.jopiter.restaurants.repository.usp import app.jopiter.restaurants.model.Period.Dinner import app.jopiter.restaurants.model.Period.Lunch import app.jopiter.restaurants.model.RestaurantItem -import app.jopiter.restaurants.repository.usp.Menu -import app.jopiter.restaurants.repository.usp.MenuParser -import app.jopiter.restaurants.repository.usp.USPRestaurantItemRepository import io.kotest.core.spec.style.ShouldSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.shouldBe @@ -32,7 +29,6 @@ import org.mockserver.integration.ClientAndServer.startClientAndServer import org.mockserver.mock.OpenAPIExpectation.openAPIExpectation import org.mockserver.model.Delay.seconds import org.mockserver.model.HttpRequest.request -import java.lang.RuntimeException import java.time.LocalDate.of private val uspRestaurantSpecUrl = "https://raw.githubusercontent.com/JopiterApp/USP-Restaurant-API/main/openapi.yaml" @@ -40,43 +36,44 @@ private val specification = openAPIExpectation(uspRestaurantSpecUrl) class USPRestaurantItemRepositoryTest : ShouldSpec({ - val mockServer = startClientAndServer().apply { upsert(specification) } + val mockServer = startClientAndServer().apply { upsert(specification) } - val target = USPRestaurantItemRepository("http://localhost:${mockServer.localPort}", dummyParsers, "hash") + val target = USPRestaurantItemRepository("http://localhost:${mockServer.localPort}", dummyParsers, "hash") - should("Parse a menu using the right parser") { - val parsed = target.fetch(6) + should("Parse a menu using the right parser") { + val parsed = target.fetch(6) - parsed shouldBe setOf( - RestaurantItem(6, of(2020, 3, 9), Lunch, 1219, "foo", "bar", "baz", emptyList(), "", "Central"), - RestaurantItem(6, of(2020, 3, 9), Dinner, 1056, "foo", "bar", "baz", emptyList(), "", "Central"), - RestaurantItem(6, of(2020, 3, 13), Lunch, 1145, "foo", "bar", "baz", emptyList(), "", "Central"), - RestaurantItem(6, of(2020, 3, 13), Dinner, 895, "foo", "bar", "baz", emptyList(), "", "Central"), - RestaurantItem(6, of(2020, 3, 14), Lunch, 1236, "foo", "bar", "baz", emptyList(), "", "Central"), - RestaurantItem(6, of(2020, 3, 14), Dinner, 0, "foo", "bar", "baz", emptyList(), "", "Central"), - RestaurantItem(6, of(2020, 3, 15), Lunch, 0, "foo", "bar", "baz", emptyList(), "", "Central"), - RestaurantItem(6, of(2020, 3, 15), Dinner, 0, "foo", "bar", "baz", emptyList(), "", "Central"), - ) - } + parsed shouldBe setOf( + RestaurantItem(6, of(2020, 3, 9), Lunch, 1219, "foo", "bar", "baz", emptyList(), "", "Central"), + RestaurantItem(6, of(2020, 3, 9), Dinner, 1056, "foo", "bar", "baz", emptyList(), "", "Central"), + RestaurantItem(6, of(2020, 3, 13), Lunch, 1145, "foo", "bar", "baz", emptyList(), "", "Central"), + RestaurantItem(6, of(2020, 3, 13), Dinner, 895, "foo", "bar", "baz", emptyList(), "", "Central"), + RestaurantItem(6, of(2020, 3, 14), Lunch, 1236, "foo", "bar", "baz", emptyList(), "", "Central"), + RestaurantItem(6, of(2020, 3, 14), Dinner, 0, "foo", "bar", "baz", emptyList(), "", "Central"), + RestaurantItem(6, of(2020, 3, 15), Lunch, 0, "foo", "bar", "baz", emptyList(), "", "Central"), + RestaurantItem(6, of(2020, 3, 15), Dinner, 0, "foo", "bar", "baz", emptyList(), "", "Central"), + ) + } - should("Return nothing if the parser has an error") { - target.fetch(7).shouldBeEmpty() - } + should("Return nothing if the parser has an error") { + target.fetch(7).shouldBeEmpty() + } - should("Timeout after 2s") { - val delayedExpectations = mockServer.retrieveActiveExpectations(request()).map { it.apply{ httpResponse.withDelay(seconds(30)) } } - mockServer.upsert(*delayedExpectations.toTypedArray()) + should("Timeout after 2s") { + val delayedExpectations = + mockServer.retrieveActiveExpectations(request()).map { it.apply { httpResponse.withDelay(seconds(30)) } } + mockServer.upsert(*delayedExpectations.toTypedArray()) - target.fetch(6).shouldBeEmpty() - } + target.fetch(6).shouldBeEmpty() + } - afterSpec { mockServer.stop(); unmockkAll() } + afterSpec { mockServer.stop(); unmockkAll() } }) private val dummyParsers = mapOf( - 6 to MenuParser { Menu("foo", "bar", "baz", emptyList(), "") }, - 7 to MenuParser { throw InvalidMenuException(7) }, + 6 to MenuParser { Menu("foo", "bar", "baz", emptyList(), "") }, + 7 to MenuParser { throw InvalidMenuException(7) }, ) class InvalidMenuException(val menuId: Int) : RuntimeException() diff --git a/restaurants/src/test/resources/logback.xml b/restaurants/src/test/resources/logback.xml index b7f6efd..e1ea7c4 100644 --- a/restaurants/src/test/resources/logback.xml +++ b/restaurants/src/test/resources/logback.xml @@ -11,6 +11,6 @@ - + diff --git a/timetable/build.gradle.kts b/timetable/build.gradle.kts index 42a41a8..c899f41 100644 --- a/timetable/build.gradle.kts +++ b/timetable/build.gradle.kts @@ -19,10 +19,10 @@ apply(plugin = "kotlin") dependencies { - // JSoup - implementation("org.jsoup:jsoup:1.13.1") + // JSoup + implementation("org.jsoup:jsoup:1.13.1") - // Selenium - implementation("org.seleniumhq.selenium:selenium-firefox-driver:3.141.59") - implementation("org.seleniumhq.selenium:htmlunit-driver:2.33.2") -} \ No newline at end of file + // Selenium + implementation("org.seleniumhq.selenium:selenium-firefox-driver:3.141.59") + implementation("org.seleniumhq.selenium:htmlunit-driver:2.33.2") +} diff --git a/timetable/src/main/kotlin/TimetableController.kt b/timetable/src/main/kotlin/TimetableController.kt index 2b4aba5..8010266 100644 --- a/timetable/src/main/kotlin/TimetableController.kt +++ b/timetable/src/main/kotlin/TimetableController.kt @@ -32,34 +32,34 @@ import org.springframework.web.bind.annotation.RequestBody as SpringRequestBody @RestController("\${api.base.path}/timetable") class TimetableController( - private val timetableRepository: TimetableRepository, + private val timetableRepository: TimetableRepository, ) { - @Operation( - summary = "Fetch a timetable from JupitereWeb", - description = "Tries to login to user's account and obtain all information related to their timetable", - tags = ["timetable"], + @Operation( + summary = "Fetch a timetable from JupitereWeb", + description = "Tries to login to user's account and obtain all information related to their timetable", + tags = ["timetable"], - requestBody = RequestBody(content = [Content(schema = Schema(implementation = TimetableRequest::class))]), + requestBody = RequestBody(content = [Content(schema = Schema(implementation = TimetableRequest::class))]), - responses = [ - ApiResponse( - responseCode = "200", - content = [Content(array = ArraySchema(schema = Schema(implementation = Subject::class)))] - ), - ApiResponse(responseCode = "401"), - ApiResponse(responseCode = "503") - ] - ) - @PostMapping() - fun timetable(@SpringRequestBody request: TimetableRequest): ResponseEntity> { - val (user, password) = request - return try { - ResponseEntity.ok(timetableRepository.get(user, password)) - } catch (_: Throwable) { - ResponseEntity.badRequest().build() - } + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(array = ArraySchema(schema = Schema(implementation = Subject::class)))] + ), + ApiResponse(responseCode = "401"), + ApiResponse(responseCode = "503") + ] + ) + @PostMapping() + fun timetable(@SpringRequestBody request: TimetableRequest): ResponseEntity> { + val (user, password) = request + return try { + ResponseEntity.ok(timetableRepository.get(user, password)) + } catch (_: Throwable) { + ResponseEntity.badRequest().build() } + } - data class TimetableRequest(val user: String, val password: String) + data class TimetableRequest(val user: String, val password: String) } diff --git a/timetable/src/main/kotlin/repository/SubjectNameRepository.kt b/timetable/src/main/kotlin/repository/SubjectNameRepository.kt index bd2b8de..c603be2 100644 --- a/timetable/src/main/kotlin/repository/SubjectNameRepository.kt +++ b/timetable/src/main/kotlin/repository/SubjectNameRepository.kt @@ -24,8 +24,8 @@ import org.springframework.stereotype.Repository @Repository class SubjectNameRepository { - private val url = "https://uspdigital.usp.br/jupiterweb/obterDisciplina?nomdis=&sgldis=" + private val url = "https://uspdigital.usp.br/jupiterweb/obterDisciplina?nomdis=&sgldis=" - operator fun get(code: String) = - connect(url + code).get().selectFirst("b:contains($code)").text().substringAfter("-").trim() + operator fun get(code: String) = + connect(url + code).get().selectFirst("b:contains($code)").text().substringAfter("-").trim() } diff --git a/timetable/src/main/kotlin/repository/TimetableRepository.kt b/timetable/src/main/kotlin/repository/TimetableRepository.kt index 2c66a00..cd4d44c 100644 --- a/timetable/src/main/kotlin/repository/TimetableRepository.kt +++ b/timetable/src/main/kotlin/repository/TimetableRepository.kt @@ -48,89 +48,90 @@ private const val JupiterLoginUrl = "https://uspdigital.usp.br/jupiterweb/webLog @Repository class TimetableRepository( - private val subjectNameRepository: SubjectNameRepository + private val subjectNameRepository: SubjectNameRepository ) { - init { - setDriverProperty() + init { + setDriverProperty() + } + + fun get(user: String, password: String): Set = with(firefoxDriver()) { + try { + navigateToTimetable(user, password) + val joinedCourses = waiting { Select(findElement(name("codpgm"))).takeIf { it.options.size > 1 } } + joinedCourses.options.drop(1).flatMap { + val timetable = parseCourse(it) + waiting { invisibilityOfElementLocated(className("blockOverlay")).apply(this) } + findElement(id("step1-tab")).click() + timetable.flatMap(TimetableRow::asSubjects) + }.toSet() + } finally { + close() } - - fun get(user: String, password: String): Set = with(firefoxDriver()) { - try { - navigateToTimetable(user, password) - val joinedCourses = waiting { Select(findElement(name("codpgm"))).takeIf { it.options.size > 1 } } - joinedCourses.options.drop(1).flatMap { - val timetable = parseCourse(it) - waiting { invisibilityOfElementLocated(className("blockOverlay")).apply(this) } - findElement(id("step1-tab")).click() - timetable.flatMap(TimetableRow::asSubjects) - }.toSet() - } finally { - close() - } - } - - private fun parseCourse(course: WebElement) = with(course) { - click() - findElement(id("buscar")).click() - val timetable = findElement(id("tableGradeHoraria")) - val rows = timetable.findElements(className("ui-widget-content")) - rows.map { it.findElements(tagName("td")) }.map { TimetableRow(it.map { it.text }) } + } + + private fun parseCourse(course: WebElement) = with(course) { + click() + findElement(id("buscar")).click() + val timetable = findElement(id("tableGradeHoraria")) + val rows = timetable.findElements(className("ui-widget-content")) + rows.map { it.findElements(tagName("td")) }.map { TimetableRow(it.map { it.text }) } + } + + private fun FirefoxDriver.navigateToTimetable(user: String, password: String) = apply { + get(JupiterLoginUrl) + findElement(name("codpes")).sendKeys(user) + findElement(name("senusu")).sendKeys(password) + findElement(name("Submit")).click() + waiting { invisibilityOfElementLocated(className("ui-widget-overlay")) } + findElement(linkText("Grade horária")).click() + } + + private fun firefoxDriver() = FirefoxDriver(FirefoxOptions().setHeadless(false)) + + private fun FirefoxDriver.waiting(block: () -> T?) = + WebDriverWait(this, Duration.ofSeconds(10)).until { block() }!! + + @Suppress("LongParameterList") + inner class TimetableRow( + val start: String, + val end: String, + val mon: String, + val tue: String, + val wed: String, + val thu: String, + val fri: String, + val sat: String, + ) { + constructor(trs: List) : this(trs[0], trs[1], trs[2], trs[3], trs[4], trs[5], trs[6], trs[7]) + + fun asSubjects(): List { + val subjects = mutableListOf() + val time = if (start.isBlank() || end.isBlank()) MIN..MAX else parse(start)..parse(end) + + if (mon.isNotBlank()) subjects += Subject(MONDAY, mon.code, subjectNameRepository[mon.code], time) + if (tue.isNotBlank()) subjects += Subject(TUESDAY, tue.code, subjectNameRepository[tue.code], time) + if (wed.isNotBlank()) subjects += Subject(WEDNESDAY, wed.code, subjectNameRepository[wed.code], time) + if (thu.isNotBlank()) subjects += Subject(THURSDAY, thu.code, subjectNameRepository[thu.code], time) + if (fri.isNotBlank()) subjects += Subject(FRIDAY, fri.code, subjectNameRepository[fri.code], time) + if (sat.isNotBlank()) subjects += Subject(SATURDAY, sat.code, subjectNameRepository[sat.code], time) + + return subjects } - private fun FirefoxDriver.navigateToTimetable(user: String, password: String) = apply { - get(JupiterLoginUrl) - findElement(name("codpes")).sendKeys(user) - findElement(name("senusu")).sendKeys(password) - findElement(name("Submit")).click() - waiting { invisibilityOfElementLocated(className("ui-widget-overlay")) } - findElement(linkText("Grade horária")).click() - } - - private fun firefoxDriver() = FirefoxDriver(FirefoxOptions().setHeadless(false)) - - private fun FirefoxDriver.waiting(block: () -> T?) = WebDriverWait(this, Duration.ofSeconds(10)).until { block() }!! - - @Suppress("LongParameterList") - inner class TimetableRow( - val start: String, - val end: String, - val mon: String, - val tue: String, - val wed: String, - val thu: String, - val fri: String, - val sat: String, - ) { - constructor(trs: List) : this(trs[0], trs[1], trs[2], trs[3], trs[4], trs[5], trs[6], trs[7]) - - fun asSubjects(): List { - val subjects = mutableListOf() - val time = if(start.isBlank() || end.isBlank()) MIN..MAX else parse(start)..parse(end) - - if(mon.isNotBlank()) subjects += Subject(MONDAY, mon.code, subjectNameRepository[mon.code], time) - if(tue.isNotBlank()) subjects += Subject(TUESDAY, tue.code, subjectNameRepository[tue.code], time) - if(wed.isNotBlank()) subjects += Subject(WEDNESDAY, wed.code, subjectNameRepository[wed.code], time) - if(thu.isNotBlank()) subjects += Subject(THURSDAY, thu.code, subjectNameRepository[thu.code], time) - if(fri.isNotBlank()) subjects += Subject(FRIDAY, fri.code, subjectNameRepository[fri.code], time) - if(sat.isNotBlank()) subjects += Subject(SATURDAY, sat.code, subjectNameRepository[sat.code], time) - - return subjects - } - - private val String.code get() = substringBefore("-") - } + private val String.code get() = substringBefore("-") + } } data class Subject(val dayOfWeek: DayOfWeek, val code: String, val name: String, val time: ClosedRange) private fun setDriverProperty() { - if(System.getProperties().containsKey("webdriver.gecko.driver")) return - val driver = TimetableRepository::class.java.classLoader.getResourceAsStream("geckodriver")!! - val path = createTempFile("gecko", "driver").apply { - writeBytes(driver.readAllBytes()) - setExecutable(true) - }.absolutePath - System.setProperty("webdriver.gecko.driver", path) + if (System.getProperties().containsKey("webdriver.gecko.driver")) return + val driver = TimetableRepository::class.java.classLoader.getResourceAsStream("geckodriver")!! + val path = createTempFile("gecko", "driver").apply { + writeBytes(driver.readAllBytes()) + setExecutable(true) + }.absolutePath + System.setProperty("webdriver.gecko.driver", path) } diff --git a/timetable/src/test/kotlin/repository/SubjectNameRepositoryTest.kt b/timetable/src/test/kotlin/repository/SubjectNameRepositoryTest.kt index 8e603ff..ea8ad85 100644 --- a/timetable/src/test/kotlin/repository/SubjectNameRepositoryTest.kt +++ b/timetable/src/test/kotlin/repository/SubjectNameRepositoryTest.kt @@ -19,24 +19,23 @@ package app.jopiter.timetable.repository import io.kotest.assertions.throwables.shouldThrowAny -import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.ShouldSpec import io.kotest.matchers.shouldBe class SubjectNameRepositoryTest : ShouldSpec({ - val target = SubjectNameRepository() + val target = SubjectNameRepository() - should("Fetch the subjects name correctly") { - target["ACH2017"] shouldBe "Projeto Supervisionado ou de Graduação I" - target["ACH2076"] shouldBe "Segurança da Informação" - target["PMT3100"] shouldBe "Fundamentos de Ciência e Engenharia dos Materiais" - target["MAT2454"] shouldBe "Cálculo Diferencial e Integral II" - target["PCS3617"] shouldBe "Estágio Cooperativo I" - } + should("Fetch the subjects name correctly") { + target["ACH2017"] shouldBe "Projeto Supervisionado ou de Graduação I" + target["ACH2076"] shouldBe "Segurança da Informação" + target["PMT3100"] shouldBe "Fundamentos de Ciência e Engenharia dos Materiais" + target["MAT2454"] shouldBe "Cálculo Diferencial e Integral II" + target["PCS3617"] shouldBe "Estágio Cooperativo I" + } - should("Throw exception when fetching an unknown subject") { - shouldThrowAny { target["ablueblua"] } - } + should("Throw exception when fetching an unknown subject") { + shouldThrowAny { target["ablueblua"] } + } }) diff --git a/timetable/src/test/kotlin/repository/TimetableRepositoryTest.kt b/timetable/src/test/kotlin/repository/TimetableRepositoryTest.kt index 186abcb..79366a3 100644 --- a/timetable/src/test/kotlin/repository/TimetableRepositoryTest.kt +++ b/timetable/src/test/kotlin/repository/TimetableRepositoryTest.kt @@ -33,32 +33,32 @@ import java.time.LocalTime.of import kotlin.reflect.KClass class UspVariablesCondition : EnabledCondition { - override fun enabled(specKlass: KClass) = "USP_USER_1" in getenv() && "USP_USER_2" in getenv() + override fun enabled(specKlass: KClass) = "USP_USER_1" in getenv() && "USP_USER_2" in getenv() } @EnabledIf(UspVariablesCondition::class) class TimetableRepositoryTest : ShouldSpec({ - val target = TimetableRepository(SubjectNameRepository()) + val target = TimetableRepository(SubjectNameRepository()) - should("Return the right subjects (Test 1)") { - val answer = target.get(getenv("USP_USER_1"), getenv("USP_PASSWORD_1")) + should("Return the right subjects (Test 1)") { + val answer = target.get(getenv("USP_USER_1"), getenv("USP_PASSWORD_1")) - answer shouldBe setOf( - Subject(MONDAY, "ACH2017", "Projeto Supervisionado ou de Graduação I", of(12, 0)..of(13, 0)), - Subject(WEDNESDAY, "ACH2076", "Segurança da Informação", of(8, 0)..of(12, 0)), - Subject(THURSDAY, "ACH2167", "Computação Sônica", of(19, 0)..of(22, 45)) - ) - } + answer shouldBe setOf( + Subject(MONDAY, "ACH2017", "Projeto Supervisionado ou de Graduação I", of(12, 0)..of(13, 0)), + Subject(WEDNESDAY, "ACH2076", "Segurança da Informação", of(8, 0)..of(12, 0)), + Subject(THURSDAY, "ACH2167", "Computação Sônica", of(19, 0)..of(22, 45)) + ) + } - should("Return the right subjects (Test 2)") { - val answer = target.get(getenv("USP_USER_2"), getenv("USP_PASSWORD_2")) + should("Return the right subjects (Test 2)") { + val answer = target.get(getenv("USP_USER_2"), getenv("USP_PASSWORD_2")) - answer shouldBe setOf( - Subject(MONDAY, "PMT3100", "Fundamentos de Ciência e Engenharia dos Materiais", of(15, 0)..of(16, 40)), - Subject(WEDNESDAY, "MAT2454", "Cálculo Diferencial e Integral II", of(11, 10)..of(12, 50)), - Subject(FRIDAY, "MAT2454", "Cálculo Diferencial e Integral II", of(11, 10)..of(12, 50)), - Subject(SATURDAY, "PCS3617", "Estágio Cooperativo I", of(14, 0)..of(15, 40)) - ) - } + answer shouldBe setOf( + Subject(MONDAY, "PMT3100", "Fundamentos de Ciência e Engenharia dos Materiais", of(15, 0)..of(16, 40)), + Subject(WEDNESDAY, "MAT2454", "Cálculo Diferencial e Integral II", of(11, 10)..of(12, 50)), + Subject(FRIDAY, "MAT2454", "Cálculo Diferencial e Integral II", of(11, 10)..of(12, 50)), + Subject(SATURDAY, "PCS3617", "Estágio Cooperativo I", of(14, 0)..of(15, 40)) + ) + } })