├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── ctx ├── build.gradle └── src │ └── main │ └── kotlin │ └── ac │ └── obl │ └── cc │ ├── Meta.kt │ └── ctx │ ├── Ctx.kt │ ├── CtxD.kt │ └── wrapper.kt ├── doc ├── 10-data.md ├── 11-builder.md ├── 12-state.md ├── 13-function.md ├── 14-action.md ├── 15-binding.md ├── 16-context.md ├── 17-misc.md ├── 20-color-wheel.md ├── 30-example-calculator.md ├── 31-example-todo.md ├── 32-example-restaurants.md └── iscan.md ├── examples ├── build.gradle └── src │ └── main │ └── kotlin │ └── ac │ └── obl │ └── cc │ ├── calc │ ├── CalcRunner.kt │ ├── CalcRunner2.kt │ ├── CalcRunner3.kt │ └── Calculator.kt │ └── todo │ ├── AddNewTodo.kt │ ├── FindTodoByText.kt │ ├── ToDoItem.kt │ ├── ToDoRunner.kt │ ├── _bind.kt │ └── repo │ ├── Db.kt │ ├── FilterToDoRecords.kt │ ├── SaveToDoInRepo.kt │ ├── ToDoRecord.kt │ └── _bind.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pectopah ├── .env ├── api │ ├── http-client.env.json │ ├── reservations.http │ └── restaurants.http ├── boot │ ├── build.gradle │ └── src │ │ └── main │ │ ├── kotlin │ │ └── ac │ │ │ └── obl │ │ │ └── pectopah │ │ │ └── boot │ │ │ ├── Application.kt │ │ │ ├── server │ │ │ ├── DB.kt │ │ │ ├── HTTP.kt │ │ │ ├── Log.kt │ │ │ ├── Monitoring.kt │ │ │ ├── Routing.kt │ │ │ └── Serialization.kt │ │ │ └── wire.kt │ │ └── resources │ │ ├── application.conf │ │ ├── hikari.properties │ │ └── logback.xml ├── db.sqs ├── db_reset.sh ├── docker-compose.yml ├── init.sql ├── model-serializers │ ├── build.gradle │ └── src │ │ └── main │ │ └── kotlin │ │ └── ac │ │ └── obl │ │ └── pectopah │ │ └── model │ │ ├── NewRestaurantSerializer.kt │ │ ├── ReservationRequestSerializer.kt │ │ ├── RestaurantSerializer.kt │ │ └── serializers.kt ├── model │ ├── build.gradle │ └── src │ │ └── main │ │ └── kotlin │ │ └── ac │ │ └── obl │ │ └── pectopah │ │ └── model │ │ ├── Id.kt │ │ ├── Reservation.kt │ │ ├── ReservationRequest.kt │ │ ├── ReservationSlot.kt │ │ ├── ReservedRestaurantTables.kt │ │ ├── Restaurant.kt │ │ ├── RestaurantTable.kt │ │ └── RestaurantTopology.kt ├── office-api │ ├── build.gradle │ └── src │ │ └── main │ │ └── kotlin │ │ └── ac │ │ └── obl │ │ └── pectopah │ │ └── api.kt ├── office │ ├── build.gradle │ └── src │ │ └── main │ │ └── kotlin │ │ └── ac │ │ └── obl │ │ └── pectopah │ │ ├── CreateNewRestaurant.kt │ │ ├── ListAllRestaurants.kt │ │ ├── MakeReservation.kt │ │ ├── RestaurantTables.kt │ │ ├── Slots.kt │ │ └── api.kt ├── repo-api │ ├── build.gradle │ └── src │ │ └── main │ │ └── kotlin │ │ └── ac │ │ └── obl │ │ └── pectopah │ │ └── repo │ │ └── RepoApi.kt └── repo-db │ ├── build.gradle │ └── src │ └── main │ └── kotlin │ └── ac │ └── obl │ └── pectopah │ └── repo │ ├── FetchAllRestaurantTables.kt │ ├── FetchAllRestaurants.kt │ ├── FindRestaurantById.kt │ ├── FindRestaurantReservationsForDay.kt │ ├── StoreNewRestaurant.kt │ ├── StoreReservation.kt │ ├── api.kt │ ├── db.kt │ └── tables │ ├── Reservations.kt │ ├── ReservedTables.kt │ ├── Restaurants.kt │ └── Tables.kt └── settings.gradle /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_size = 4 7 | tab_width = 4 8 | ij_continuation_indent_size = 4 9 | ij_formatter_off_tag = @formatter:off 10 | ij_formatter_on_tag = @formatter:on 11 | ij_formatter_tags_enabled = false 12 | ij_smart_tabs = false 13 | ij_wrap_on_typing = false 14 | max_line_length = 120 15 | 16 | [*.java] 17 | indent_style = space 18 | max_line_length = off 19 | ij_java_class_count_to_use_import_on_demand = 999 20 | ij_java_names_count_to_use_import_on_demand = 999 21 | 22 | [*.kt] 23 | indent_style = space 24 | max_line_length = off 25 | 26 | [Dockerfile] 27 | indent_style = space 28 | max_line_length = off 29 | 30 | [*.xml] 31 | indent_style = tab 32 | max_line_length = off 33 | 34 | [*.md] 35 | indent_style = space 36 | indent_size = 2 37 | 38 | [{*.yaml,*.yml}] 39 | indent_style = space 40 | indent_size = 2 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | build 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Igor Spasić 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Color Code 2 | 3 | Standard programming languages are generic. While that is the purpose of such language, it leads to many ways of understanding and doing the same thing. 4 | 5 | The idea is to establish specific guides that should lead to uniquely designed code, hopefully, a better one, leaving less space for a different interpretation. 6 | 7 | > Why 'color'? 8 | 9 | We need a new term for the code categorization. Common terms, 'type' or 'kind,' are already used in a different context. And it's fun! 10 | 11 | Let's begin. 12 | 13 | ## Table of Contents 14 | 15 | + [🟦 DATA](doc/10-data.md) 16 | + [🟪 BUILDER](doc/11-builder.md) 17 | + [🟥 STATE](doc/12-state.md) 18 | + [🟨 FUNCTION](doc/13-function.md) 19 | + [🟧 ACTION](doc/14-action.md) 20 | + [Binding](doc/15-binding.md) 21 | + [⬛️ CONTEXT](doc/16-context.md) 22 | + [Misc topics](doc/17-misc.md) 23 | + [🍭 Color wheel](doc/20-color-wheel.md) 24 | 25 | ### Examples 26 | 27 | + [Calculator](doc/30-example-calculator.md) - using `Ctx` 28 | + [ToDo App](doc/31-example-todo.md) - simple app with two layers 29 | + [Restaurants](doc/32-example-restaurants.md) - real-world web app with Ktor & Exposed. 30 | 31 | ### Tools 32 | 33 | + [ISCAN](doc/iscan.md) - pen-n-paper tool for analyzing code quality. 34 | 35 | ## Primary colors 36 | 37 | ``` 38 | 🟦 == calm, unchanged, stable 39 | 🟨 == pure, light, combined 40 | 🟥 == contagious, changes other colors 41 | ``` 42 | 43 | Finally: 44 | 45 | > ⚡️ **Pro Tip**: don't take this too serious. 46 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.jvm") version "${kotlin_version}" apply false 3 | } 4 | 5 | allprojects { 6 | version = "1.0.0-SNAPSHOT" 7 | group = "ac.obl.pectopah" 8 | 9 | repositories { 10 | mavenCentral() 11 | maven { url 'https://maven.pkg.jetbrains.space/public/p/compose/dev' } 12 | } 13 | } 14 | 15 | tasks.named('wrapper') { 16 | gradleVersion = "${gradle_version}" 17 | distributionType = Wrapper.DistributionType.ALL 18 | } 19 | 20 | -------------------------------------------------------------------------------- /ctx/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id('org.jetbrains.kotlin.jvm') 3 | } 4 | 5 | test { 6 | useJUnitPlatform() 7 | } 8 | 9 | dependencies { 10 | testImplementation 'org.jetbrains.kotlin:kotlin-test' 11 | } 12 | -------------------------------------------------------------------------------- /ctx/src/main/kotlin/ac/obl/cc/Meta.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc 2 | 3 | /* 4 | * Just a bunch of source annotations. 5 | * You don't need to have them in your codebase. Still, it may be helpful for 6 | * static code analysis. 7 | */ 8 | 9 | /** 10 | * 🟦 11 | */ 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class DATA 15 | 16 | /** 17 | * 🟪 18 | */ 19 | @Target(AnnotationTarget.CLASS) 20 | @Retention(AnnotationRetention.SOURCE) 21 | annotation class BUILDER 22 | 23 | /** 24 | * 🟥 25 | */ 26 | @Target(AnnotationTarget.CLASS) 27 | @Retention(AnnotationRetention.SOURCE) 28 | annotation class STATE 29 | 30 | /** 31 | * 🟨 32 | */ 33 | @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) 34 | @Retention(AnnotationRetention.SOURCE) 35 | annotation class FUNCTION 36 | 37 | /** 38 | * 🟧 39 | */ 40 | @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) 41 | @Retention(AnnotationRetention.SOURCE) 42 | annotation class ACTION 43 | 44 | /** 45 | * ⬛️ 46 | */ 47 | @Target(AnnotationTarget.CLASS) 48 | @Retention(AnnotationRetention.SOURCE) 49 | annotation class CONTEXT 50 | -------------------------------------------------------------------------------- /ctx/src/main/kotlin/ac/obl/cc/ctx/Ctx.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.ctx 2 | 3 | import ac.obl.cc.CONTEXT 4 | 5 | /** 6 | * Sequential context. 7 | */ 8 | @CONTEXT 9 | class Ctx(private val value: A) { 10 | 11 | companion object { 12 | /** 13 | * Alt. constructor. 14 | */ 15 | fun of(value: V) = Ctx(value) 16 | 17 | operator fun plus(value: V): Ctx { 18 | return Ctx(value) 19 | } 20 | } 21 | 22 | // map 23 | 24 | /** 25 | * Basic transformation of the context. 26 | */ 27 | fun map(fn: (A) -> B): Ctx { 28 | return Ctx(fn(value)) 29 | } 30 | 31 | operator fun plus(fn: (A) -> B): Ctx { 32 | return Ctx(fn(value)) 33 | } 34 | 35 | // consume 36 | 37 | /** 38 | * Consumes the value of this context. 39 | */ 40 | fun use(consumer: (A) -> Unit): Ctx { 41 | consumer(value) 42 | return this 43 | } 44 | 45 | operator fun minus(consumer: (A) -> Unit): Ctx { 46 | consumer(value) 47 | return this 48 | } 49 | 50 | // return 51 | 52 | /** 53 | * Returns a provider function that returns the value of this context. 54 | */ 55 | fun get(): () -> A { 56 | return { invoke() } 57 | } 58 | 59 | operator fun invoke(): A { 60 | return value 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ctx/src/main/kotlin/ac/obl/cc/ctx/CtxD.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.ctx 2 | 3 | import ac.obl.cc.CONTEXT 4 | 5 | /** 6 | * Context definition. 7 | */ 8 | @CONTEXT 9 | class CtxD(private val provider: () -> A) { 10 | companion object { 11 | /** 12 | * Alt. constructor. 13 | */ 14 | fun of(value: V) = CtxD { value } 15 | 16 | operator fun plus(value: V): CtxD = CtxD { value } 17 | 18 | operator fun plus(provider: () -> V): CtxD = CtxD(provider) 19 | } 20 | 21 | // map 22 | 23 | /** 24 | * Basic transformation of the context. 25 | */ 26 | fun map(fn: (A) -> B): CtxD { 27 | return CtxD { fn(provider()) } 28 | } 29 | 30 | operator fun plus(fn: (A) -> B): CtxD = map(fn) 31 | 32 | // consume 33 | 34 | /** 35 | * Consumes the value of this context. 36 | */ 37 | fun use(consumer: (A) -> Unit): CtxD { 38 | return CtxD { 39 | val v = provider() 40 | consumer(v) 41 | v 42 | } 43 | } 44 | 45 | operator fun minus(consumer: (A) -> Unit): CtxD = use(consumer) 46 | 47 | // return 48 | 49 | /** 50 | * Returns a provider function that returns the value of this context. 51 | */ 52 | fun get(): () -> A { 53 | return { invoke() } 54 | } 55 | 56 | operator fun invoke(): A { 57 | return provider() 58 | } 59 | 60 | // wrap 61 | 62 | fun wrap(wrapper: (() -> A) -> A): CtxD { 63 | return CtxD { 64 | wrapper { 65 | invoke() 66 | } 67 | } 68 | } 69 | 70 | operator fun times(wrapper: (() -> A) -> A): CtxD = wrap(wrapper) 71 | 72 | } 73 | -------------------------------------------------------------------------------- /ctx/src/main/kotlin/ac/obl/cc/ctx/wrapper.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.ctx 2 | 3 | fun wrapper(impl: (() -> Unit) -> Unit): Wrapper = object : Wrapper() { 4 | override fun impl(it: () -> Unit) = impl(it) 5 | } 6 | 7 | abstract class Wrapper { 8 | abstract fun impl(it: () -> Unit) 9 | 10 | operator fun invoke(body: () -> R): R { 11 | var result: R? = null 12 | impl { result = body() } 13 | @Suppress("UNCHECKED_CAST") 14 | return result as R 15 | } 16 | 17 | operator fun plus(other: Wrapper) = wrapper { 18 | impl { 19 | other.impl { it() } 20 | } 21 | } 22 | } 23 | 24 | // example 25 | 26 | val logged = wrapper { 27 | println("before logged") 28 | try { it() } finally { 29 | println("after logged") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /doc/10-data.md: -------------------------------------------------------------------------------- 1 | # 🟦 DATA 2 | 3 | + DATA is an _immutable_ instance that holds only _data_. 4 | + DATA has _just_ properties. Some properties can aggregate others. 5 | + There are _no_ operations in DATA! 6 | 7 | ```kotlin 8 | data class Point(val x: Int, val y: Int) 9 | ``` 10 | 11 | (Some languages already have syntax for DATA classes.) 12 | 13 | > ⚡️ **Pro Tip**: Avoid primitive obsession. Minimize usage of common types for DATAs properties that you pass around. 14 | 15 | ## On construction 16 | 17 | + Create DATA using a single ctor. 18 | + You can have auxiliary static ctors (in the companion `object` or as a `static` method). 19 | + An `init` block or ctor code block may finish the data initialization. 20 | 21 | ## On naming 22 | 23 | + DATA name is a _noun_. 24 | + Avoid the use of internal types in the name. 25 | 26 | For example, don't use `BooksList` but, e.g., `Books`. Avoid using `and` to concatenate two domain names; try to find a new domain name instead. `BookId` is OK, `BookUuid` tells about the nature of the `id`, which is part of the implementation. 27 | -------------------------------------------------------------------------------- /doc/11-builder.md: -------------------------------------------------------------------------------- 1 | # 🟪 BUILDER 2 | 3 | BUILDER is a special case of DATA constructor implemented as a mutable STATE. 4 | 5 | + Exists only for building DATA, in a very short scope. 6 | + BUILDER has a fluent interface. 7 | + BUILDER produces only DATA. 8 | 9 | ```kotlin 10 | Point.Builder() 11 | .x(2) 12 | .y(5) 13 | .build() 14 | ``` 15 | 16 | BUILDER may also be a factory. 17 | -------------------------------------------------------------------------------- /doc/12-state.md: -------------------------------------------------------------------------------- 1 | # 🟥 STATE 2 | 3 | + STATE is long-lived mutable storage. 4 | + STATE is a side-effect. 5 | + STATE is contagious. 6 | 7 | ## As storage 8 | 9 | STATE **never** expose the inner implementation of the storage! 10 | 11 | The STATE may store data using collection classes. Hence, STATE never exposes the collection type (e.g., `List`, `Map`); instead, it provides ways to _operate_ on its content. 12 | 13 | ## As side-effect 14 | 15 | STATE is also a side-effect, like database, console, file system, sockets... 16 | 17 | ## On methods 18 | 19 | + STATE has methods that modify the state. 20 | + STATE exposes content. 21 | + STATE hides implementation. 22 | 23 | Do not add operational methods to STATE. 24 | 25 | > ⚡️ Pro tip: Minimize the number of STATE methods. 26 | -------------------------------------------------------------------------------- /doc/13-function.md: -------------------------------------------------------------------------------- 1 | # 🟨 FUNCTION 2 | 3 | FUNCTION is a first-class citizen. 4 | 5 | FUNCTION transforms **one** input into **one** output. FUNCTION transformations may be interpreted as _context_ mappings from one state into another. Simple as: `(A) -> B`. 6 | 7 | > Wait, one input? 8 | 9 | Sure, FUNCTION may have multiple arguments. However, we may consider multiple arguments as one argument (even have a type for it). You can find out more about this idea later. 10 | 11 | ## On naming 12 | 13 | + FUNCTION is a verb. 14 | 15 | ## On arguments 16 | 17 | FUNCTIONs arguments can be: 18 | 19 | + _explicit_ - part of functions signature. 20 | + _implicit_ - an external reference used directly in the function body (hard-coded). 21 | 22 | > ⚡️ Pro tip: Avoid _implicit_ arguments. 23 | 24 | If _implicit_ argument is an ACTION, it automatically spreads to the current FUNCTION. 25 | 26 | _Explicit_ arguments may be: 27 | 28 | + **configuration** - define the behaviour, provide external dependencies. 29 | + **input** - pure input used in functions operation. 30 | 31 | _Implicit_ arguments are always **configuration**. 32 | 33 | > ⚡️ Pro tip: Provide configuration parameters within FUNCTION ctor. 34 | 35 | It may be tricky to distinguish between configuration and input arguments. Think about the context a FUNCTION is applied to. 36 | 37 | ## Function declaration != FUNCTION 38 | 39 | This should be obvious, but let's emphasize it. FUNCTION is a reference to a function; i.e., its instance. You can define FUNCTION by, e.g.: 40 | 41 | + anonymous instance referenced by `val`, 42 | + using `object`, 43 | + having an instance of functional type, 44 | + or inline lambda that calls declared function. 45 | 46 | In other words: if you can not pass it, it is not a FUNCTION. 47 | 48 | > ⚡️ Pro tip: Minimize anonymous instances as they do not contribute to the domain vocabulary. 49 | -------------------------------------------------------------------------------- /doc/14-action.md: -------------------------------------------------------------------------------- 1 | # 🟧 Action 2 | 3 | ACTION is a function that has STATE or another ACTION as an argument (_implicit_ or _explicit_). 4 | 5 | Actions are not pure. 6 | 7 | ## Control the ACTIONs 8 | 9 | Any FUNCTION that uses STATE becomes the ACTION itself. Any FUNCTION that uses ACTION becomes the ACTION itself. STATE is contagious. 10 | 11 | You can control the spreading of the ACTIONs by having an _abstract barrier_ in the form of e.g. an interface: 12 | 13 | ```kotlin 14 | object FooAction: (String) -> Number { 15 | // ACTION 16 | } 17 | 18 | // FUNCTION, bind the implementation 19 | val fooAction: (String) -> Number = FooAction 20 | ``` 21 | 22 | You will often need ACTIONs because of 3rd party code and frameworks. That is OK. 23 | 24 | > ⚡️ Pro tip: Try hard to minimize the spread of the ACTIONs. 25 | -------------------------------------------------------------------------------- /doc/15-binding.md: -------------------------------------------------------------------------------- 1 | # FUNCTION binding 2 | 3 | If FUNCTION is an instance, there must be a reference that points to it. 4 | 5 | ## Static or Singleton 6 | 7 | FUNCTION may be a singleton (i.e. `object`) or static. However, it is not a preferable way of "holding" FUNCTION instances in your code. You can not replace its implementation (for testing purposes, for example). 8 | 9 | ## DATA class with extension method 10 | 11 | A DATA class could be an excellent way to bind FUNCTION (remember: implementations i.e. instances) in one place. Something like: 12 | 13 | ```kotlin 14 | data class ToDo( 15 | val addNewTodo: (NewToDoItem) -> ToDoItem, 16 | val findTodoByText: (String) -> List 17 | //... 18 | ) 19 | ``` 20 | 21 | This data class now collects ALL verbs of the domain. You may have the default instance: 22 | 23 | ```kotlin 24 | val toDo = ToDo( 25 | addNewTodo = AddNewTodo(saveToDoInRepo), 26 | // ... 27 | ) 28 | ``` 29 | 30 | and custom ones: 31 | 32 | ```kotlin 33 | val toDoWithLogging = toDo.copy( 34 | addNewTodo = { withLogging(toDo.addNewTodo, it) } 35 | ) 36 | ``` 37 | 38 | Extension methods may become the _end_ users of the domain verbs: 39 | 40 | ```kotlin 41 | fun ToDo.actionOne() { 42 | addNewTodo(NewToDoItem("Finish paperwork")) 43 | } 44 | with(toDoWithLogging) { 45 | actionOne() 46 | } 47 | ``` 48 | 49 | > ⚡️ Pro tip: Either way you do, maintain the list of verbs (bindings) in one place. Think about generating its source. 50 | -------------------------------------------------------------------------------- /doc/16-context.md: -------------------------------------------------------------------------------- 1 | # ⬛ Context 2 | 3 | Several times I've mentioned the _context_. We can think about the FUNCTION as operations on the context value. Therefore, context is a container that holds value. 4 | 5 | Context could be expressed: 6 | 7 | + with a custom class (see `Ctx` and `CtxD`), that serves as a value container. 8 | + function body itself is a context. 9 | 10 | ## Container context class 11 | 12 | When we explicitly use context with a container class, we put the initial content inside and define the ordered set of transformations of the initial value: 13 | 14 | ```text 15 | input -> func1 -> func2 -> func3 -> func4 -> output 16 | ``` 17 | 18 | An extreme version of the context is one that only accepts functions with a _single_ argument. 19 | 20 | While this idea seems interesting, it is hard to keep up with it in the real world. The consequence is that you are somewhat _forced_ to define input types of the function. Which is not a bad thing, after all. While I like to think like that, it does not always make sense. 21 | 22 | The cool thing about container context is that you may have different modes of execution: 23 | 24 | + _immediate_ - executes right away; 25 | + _delayed_ - constructs flow of the operations (i.e. pipeline) and gets executed anytime later. 26 | 27 | Another cool thing is that business logic becomes just sequential context execution. 28 | 29 | Context class does not have to be a monad. 30 | 31 | ## Function body as a context 32 | 33 | The function body itself is a context! This is my preferred way of thinking about the context (and function bodies). 34 | 35 | When you think in such a way, specific rules emerge: 36 | 37 | + the context is never changed - you just build it up; by adding final references to calculated values. In other words, always use `val`s. 38 | + fetch, then process - first bring all the values (build-up the context), then process it fluently. 39 | -------------------------------------------------------------------------------- /doc/17-misc.md: -------------------------------------------------------------------------------- 1 | # Misc topics 2 | 3 | ## Minimize the public function argument count 4 | 5 | + `one` - great 6 | + `two` - ok 7 | + `three` or more - avoid 8 | 9 | ## Use abstraction barriers to control ACTIONs spread 10 | 11 | It's very easy to let the ACTIONs spread out. If your code is red and orange, that is not OK. 12 | 13 | You can not avoid actions, but you can minimize the usage. By keeping actions in a module and providing them as FUNCTION implementations, you can put things back in control. 14 | 15 | ## Distinguish function configuration from function input 16 | 17 | Focus on WHAT your _verb_ operates onto. What do we change, transform? The answer gives you the _input_. Everything else is probably a configuration. 18 | 19 | Obviously, DI of the dependencies is part of the configuration. 20 | 21 | ## Use modules to organize code and divide between concerns 22 | 23 | Simple as that, yet powerful. 24 | 25 | I wish IDE and tools work more smoothly with modules. 26 | 27 | ## First split abstractions, then implementations 28 | 29 | A common approach is to group all services in one module (package), repository class in another, etc. Avoid this. Instead, separate abstractions first, and then different verbs and repo. 30 | 31 | ## API layer may have different transport implementation 32 | 33 | API invocations could be replaceable without changing the implementation. For example, API invocation could be: 34 | 35 | + direct function call (in a monolith application). 36 | + remote call, when implementation is defined in remote (micro)service. 37 | 38 | In other words, modules and API layer allows extracting a module into a separate microservice. 39 | -------------------------------------------------------------------------------- /doc/20-color-wheel.md: -------------------------------------------------------------------------------- 1 | # Code colors rules 2 | 3 | Primary colors: 4 | 5 | ``` 6 | 🟦 == calm, unchanged, stable 7 | 🟨 == pure, light, combined 8 | 🟥 == contagious, changes other colors 9 | ``` 10 | 11 | ## Green is good 12 | 13 | ``` 14 | DATA + FUNCTION -> 🟦 + 🟨 == 🟩 15 | ``` 16 | 17 | Code greens as much as possible. 18 | 19 | ## Red spreads 20 | 21 | ``` 22 | DATA + STATE -> 🟦 + 🟥 == 🟪 23 | ``` 24 | 25 | Purple means **pay attention**. STATE naturally operates with DATA (receives or emits). 26 | 27 | BUILDER, for example, is somewhere between STATE and DATA (holds a state temporarily until it creates DATA); hence it's violet. 28 | 29 | ``` 30 | FUNCTION + STATE -> 🟨 + 🟥 == ACTION 🟧 31 | ``` 32 | 33 | Orange means **warning**. 34 | 35 | ACTION is not pure. It is contaminated by the STATE (side-effect). 36 | 37 | ``` 38 | FUNCTION + ACTION -> 🟨 + 🟧 == ACTION 🟧 39 | ``` 40 | 41 | STATE is contagious, so ACTION is contagious too. FUNCTION that uses ACTION becomes an ACTION. 42 | 43 | ## Avoid white 44 | 45 | ``` 46 | OOP == 🟦 + 🟥 + 🟨 == ⬜️ 47 | ``` 48 | 49 | White means **nothing**, as a mix of everything. Avoid. Traditional OOP is white, as it mixes all the concepts in one place. 50 | -------------------------------------------------------------------------------- /doc/30-example-calculator.md: -------------------------------------------------------------------------------- 1 | # Example: Calculator 2 | 3 | The calculator provides basic math operation: `+`, `-`, `\`, `*` on input. 4 | 5 | Each math operation is a FUNCTION that maps the input pair into the result. E.g. `add` takes an input tuple and maps it into the single result value. 6 | 7 | > Domain keywords detected: input `tuple`, result `value`. 8 | 9 | We need DATA types that will represent both input and output. 10 | 11 | Obviously, we need DATA types for the value: `Value`. This type holds a `Number`, but also a flag that indicates if the value is a float-point number or not. 12 | 13 | Interesting is the input tuple naming. The first idea might be `ValueTuple`, or `ValuePair`. Now, here comes one potentially good side of having one single argument - it forces you to think about what is the actual context of a function. So, we are talking here about the `Operands`. 14 | 15 | > Focus on verbs to get FUNCTIONs. 16 | 17 | This example is trivial. FUNCTIONs are: `add`, `subtract`... 18 | 19 | ## Calculation continuation 20 | 21 | Calculation of `Operands` results with the single `Value` in the context. We need to bind it with another `Value` to form a new `Operands` instance. 22 | 23 | Therefore, we need another context transformation i.e. verb i.e. FUNCTION. 24 | 25 | Now things become interesting. The common way would be a function that takes 2 arguments: 26 | 27 | ```kotlin 28 | .map { Operands.of(it, 8) } 29 | ``` 30 | 31 | The streamlined approach is to have a FUNCTION that operates on the previous value (the context), using the configuration of the new value. 32 | 33 | ```kotlin 34 | .map(with(8)) 35 | ``` 36 | 37 | The `8` here is the _configuration_ argument, not the _input_! We use `with()` to create a FUNCTION configured with a number that works on the previous value, stored in Context. 38 | -------------------------------------------------------------------------------- /doc/31-example-todo.md: -------------------------------------------------------------------------------- 1 | # Example: ToDo App 2 | 3 | Simple TODO app with in-memory storage. 4 | 5 | See the code for the comments. 6 | -------------------------------------------------------------------------------- /doc/32-example-restaurants.md: -------------------------------------------------------------------------------- 1 | # Example: Restaurants 2 | 3 | The domain problem is lovely: table reservation in a restaurant. The domain is simple, yet it requires some effort to make it right. Use it on interviews:) 4 | 5 | > ⚠️ The example is not complete (yet) and not entirely correct. It demonstrates how to organize a project. 6 | 7 | ## Project organization 8 | 9 | The project is divided into modules: 10 | 11 | + `model` - domain model, pure DATA 12 | + `model-serializers` - serializers for `model` and various API inputs 13 | + `office-api` - thin definition of all the domain verbs, pure FUNCTIONs 14 | + `office` - implementation of `office-api` 15 | + `repo-api` - thin definition of repository API, pure FUNCTIONs 16 | + `repo-db` - implementation of `repo-api` on relational database; ACTIONs. 17 | + `boot` - bootstrapping of the application, wires everything and runs the application. 18 | 19 | ## Takeaways 20 | 21 | + Business logic consists of pure FUNCTIONs. 22 | + All ACTIONs are in implementation. 23 | + Clean separation of colors. 24 | + There are internal functions for the business logic. 25 | 26 | ## Things to do for real-world project 27 | 28 | Business logic: 29 | 30 | + `MakeReservation` must be detect concurrent reservations. One way is to add a field `last_reservation_time` to the `Restaurant` entity. 31 | + Consider that tables may be joined for larger parties. 32 | 33 | Code: 34 | 35 | + Some code fragments should be generated using Kotlin KSP. 36 | + Add tests for FUNCTIONs. 37 | + Transform exceptions to proper HTTP responses. 38 | -------------------------------------------------------------------------------- /doc/iscan.md: -------------------------------------------------------------------------------- 1 | # ISCAN 2 | 3 | This is a pen-and-paper tool for quick analysis of the code structure. The goal is to tell quickly if the code is relatively well-structured or not. 4 | 5 | The ISCAN report is a distilled version of the code structure. 6 | 7 | ## Data 8 | 9 | Data elements (DATA, but properties of the class, too) are listed. 10 | 11 | For example: 12 | 13 | ```kotlin 14 | data class Wallet(var cash: Int) 15 | data class Customer(val wallet: Wallet) 16 | ``` 17 | 18 | is represented as: 19 | 20 | ```text 21 | * 🟥 Wallet 22 | * 🟦 Customer 23 | ``` 24 | 25 | `Wallet` is STATE because it is mutable. `Customer` is DATA because it is not mutable. 26 | 27 | List all data types and determine if they are STATE or DATA (mutable or immutable). 28 | 29 | ## Functions, methods 30 | 31 | Functions, however, require more attention. The template is the following: 32 | 33 | ```text 34 | * (🟨 or 🟧) 35 | arg: 36 | - 37 | out: 38 | - 39 | inv: 40 | - [R|W|C|I]+ 41 | use: 42 | - 43 | ``` 44 | 45 | Here is each section explained. 46 | 47 | ### ARG 48 | 49 | List of arguments. _Implicit_ arguments are surrounded with brackets. 50 | 51 | ### OUT 52 | 53 | List of outputs. _Implicit_ outputs are surrounded with brackets. 54 | 55 | ### INV 56 | 57 | List of invocation subjects (without duplication). 58 | 59 | A way to locate all invocation subjects is to find all the invocation dots. Invocation subject is everything that is _left_ of the dot. 60 | 61 | Each invocation subject is marked with one of the following: 62 | 63 | + `R` - reads something from it, usually data 64 | + `W` - writes something into it, usually data 65 | + `C` - creates something, usually data 66 | + `I` - invocation of another entity 67 | 68 | Example: 69 | 70 | ```kt 71 | foo.bar(1, 2) 72 | foo.baz.qux = 2 73 | ``` 74 | 75 | We have 3 invocation dots. Everything left is: `foo` (invocation), `foo` (read), `baz`(write). 76 | 77 | ```text 78 | inv: 79 | - foo (I/R) 80 | - baz (W) 81 | ``` 82 | ### USE 83 | 84 | List of used data - or the access. 85 | 86 | A way to locate all used data is find all the invocation dots. Everything _right_ of the dot is some kind of data/access. 87 | 88 | In above example, right of the dots are: `bar`, `baz`, `qux`. 89 | 90 | ## The signs of **BAD** code 91 | 92 | + ACTION is not OK. However, sometimes you can not avoid using an ACTION - that is also OK. 93 | + _implicit_ arguments are BAD. 94 | + _implicit_ outputs are BAD. 95 | + write invocation (`W`) is BAD (it directly modifies external, mutable data). 96 | + read invocation (`R`) needs to be checked - are we accessing more than we should (abstraction leak)? The `use` section helps detects this. 97 | + invocation that is `C` may be suspicious - do we create something that is not on the same abstraction level. 98 | + abstraction leaks are BAD. 99 | 100 | (These rules need some more work, admittedly, but the gist is there.) 101 | 102 | The last rule is a bit ambiguous. With the ISCAN, there are two tricks how to determine _possible_ abstraction leaks: 103 | 104 | + too many elements in `use` section. 105 | + elements in `use` section are UNRELATED to method or input arguments _names_. Ask yourself: "does this function need to know about the ____". 106 | 107 | ## Bad Spreads 108 | 109 | This is important. If you detect a bad entity, it spreads to all places where it is used. 110 | 111 | ## Class 112 | 113 | ISCAN of the classes (in OOP) is simply a collection of ISCAN records of internal data and all its methods. Usually, class methods have implicit arguments and sometimes implicit outputs. 114 | 115 | ## Examples 116 | 117 | Super-trivial example: 118 | 119 | ```kotlin 120 | fun add(a: Int, b: Int): Int { 121 | return a + b 122 | } 123 | ``` 124 | 125 | ```text 126 | ISCAN 127 | * 🟨 add 128 | arg: 129 | - a 130 | - b 131 | out: 132 | - sum 133 | inv: 134 | use: 135 | ``` 136 | 137 | OOP example that breaks LoD: 138 | 139 | ```kotlin 140 | class Paperboy { 141 | private var collectedAmount: Int = 0 142 | fun collectMoney(customer : Customer, dueAmount: Int) { 143 | if (customer.wallet.cash < dueAmount) { 144 | throw IllegalStateException("Customer has insufficient funds") 145 | } 146 | customer.wallet.cash -= dueAmount 147 | collectedAmount += dueAmount 148 | } 149 | } 150 | ``` 151 | 152 | ```text 153 | ISCAN 154 | 155 | PAPERBOY 156 | -------- 157 | * 🟥 collectedAmount 158 | * 🟧 collectMoney 159 | arg: 160 | - [collectedAmount] 161 | - Customer 162 | - Int 163 | out: 164 | - [collectedAmount] 165 | inv: 166 | - customer (R) 167 | - wallet (R/W) 168 | - [this] (R/W) 169 | use: 170 | - wallet 171 | - cash 172 | - collectedAmount 173 | ``` 174 | 175 | Above code is BAD: 176 | 177 | - First, the internal state is mutable (STATE) 178 | - The method has implicit argument and output, `collectedAmount`. 179 | - The method changes the state of `wallet`. 180 | - Abstraction leak of `wallet` in the method. Actors are `customer` and `paperboy`. They use `Int` for the amount of money. This method DOES NOT need to know about the `wallet` i.e. how the money is stored. 181 | 182 | ## ISCAN is a work in progress 183 | 184 | Time needed. 185 | 186 | ## ISCAN is partial 187 | 188 | You do not need to perform the ISCAN of the whole code. It is enough to scan only relevant parts; feel free to ignore the rest. 189 | -------------------------------------------------------------------------------- /examples/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id('org.jetbrains.kotlin.jvm') 3 | } 4 | 5 | test { 6 | useJUnitPlatform() 7 | } 8 | 9 | dependencies { 10 | implementation project(':ctx') 11 | testImplementation 'org.jetbrains.kotlin:kotlin-test' 12 | } 13 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/calc/CalcRunner.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.calc 2 | 3 | import ac.obl.cc.ctx.Ctx 4 | 5 | fun main() { 6 | 7 | // Examples of Context usage 8 | // This approach lead to creating the `Operands` class and `with` function 9 | 10 | Ctx.of(Operands.of(2, 3)) 11 | .map { multiply(it) } 12 | .use { println(it) } 13 | 14 | Ctx.of(Operands.of(2, 3)) 15 | .map { add(it) } 16 | .use { println(it) } 17 | 18 | Ctx.of(Operands.of(2, 3)) 19 | .map(add) 20 | .use(printValue) 21 | 22 | Ctx.of(Operands.of(6, 2)) 23 | .map(divide()) 24 | .use { println(it) } 25 | 26 | Ctx.of(Operands.of(6, 2)) 27 | .map(divide()) 28 | .map(with(9)) 29 | .map(add) 30 | .use(printValue) 31 | 32 | 33 | // Alternative syntax, more user-readable 34 | 35 | (Ctx 36 | + (Value(3)) 37 | + with(5) 38 | + add 39 | + with(3) 40 | + add 41 | - printValue 42 | ) 43 | 44 | // Assigning final context value to the variable 45 | 46 | val result = ( 47 | Ctx 48 | + (Operands.of(2, 3)) 49 | + add)() 50 | 51 | println(result) 52 | 53 | } 54 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/calc/CalcRunner2.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.calc 2 | 3 | import ac.obl.cc.ctx.Ctx 4 | 5 | fun main() { 6 | 7 | // A more complex calculation that requires the joining of two contexts. 8 | // 9 | // ((4 + 8) / 3) + (6 / 2) 10 | 11 | (Ctx 12 | + (Value(4)) 13 | + with(8) 14 | + add 15 | + with(3) 16 | + divide() 17 | + with(rightSide()) 18 | + add 19 | ) 20 | } 21 | 22 | // Private function required by the need of joining two contexts. 23 | private val rightSide = 24 | (Ctx 25 | + (Value(6)) 26 | + with(2) 27 | + divide() 28 | + { it.number } 29 | ) 30 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/calc/CalcRunner3.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.calc 2 | 3 | import ac.obl.cc.ctx.CtxD 4 | 5 | fun main() { 6 | 7 | // The use of delayed context. 8 | // 9 | // ((4 + 8) / 3) + (6 / 2) 10 | 11 | (CtxD 12 | + (Value(4)) 13 | + with(8) 14 | + add 15 | - printValue 16 | + with(3) 17 | + divide() 18 | + with(rightSide()) 19 | + add 20 | - printValue 21 | )() 22 | } 23 | 24 | private val rightSide = 25 | (CtxD 26 | + (Value(6)) 27 | + with(2) 28 | + divide() 29 | + { it.number } 30 | ) 31 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/calc/Calculator.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.calc 2 | 3 | import ac.obl.cc.DATA 4 | import ac.obl.cc.FUNCTION 5 | 6 | /** 7 | * This is the domain. 8 | */ 9 | 10 | @DATA 11 | data class Value(val number: Number, val isFloat: Boolean = number is Float || number is Double) 12 | 13 | @DATA 14 | data class Operands(val left: Value, val right: Value) { 15 | val isFloat = left.isFloat || right.isFloat // aux property 16 | companion object { 17 | fun of(left: Value, right: Value): Operands { 18 | return Operands(left, right) 19 | } 20 | fun of(left: Number, right: Number): Operands { 21 | return Operands(Value(left), Value(right)) 22 | } 23 | } 24 | } 25 | 26 | /** 27 | * Example of val reference to an anonymous function. 28 | */ 29 | @FUNCTION 30 | val add: (Operands) -> Value = { 31 | if (it.isFloat) { 32 | Value(it.left.number.toDouble() + it.right.number.toDouble(), true) 33 | } else { 34 | Value(it.left.number.toLong() + it.right.number.toLong(), false) 35 | } 36 | } 37 | 38 | /** 39 | * Example of the object function. 40 | */ 41 | @FUNCTION 42 | object subtract: (Operands) -> Value { 43 | override fun invoke(operands: Operands): Value { 44 | return if (operands.isFloat) { 45 | Value(operands.left.number.toDouble() - operands.right.number.toDouble(), true) 46 | } else { 47 | Value(operands.left.number.toLong() - operands.right.number.toLong(), false) 48 | } 49 | } 50 | } 51 | 52 | @FUNCTION 53 | class divide: (Operands) -> Value { 54 | override fun invoke(operands: Operands): Value { 55 | return if (operands.isFloat) { 56 | Value(operands.left.number.toDouble() / operands.right.number.toDouble(), true) 57 | } else { 58 | if ((operands.left.number.toLong() % operands.right.number.toLong()) == 0L) { 59 | Value(operands.left.number.toLong() / operands.right.number.toLong(), false) 60 | } else { 61 | Value(operands.left.number.toDouble() / operands.right.number.toLong(), true) 62 | } 63 | 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * Example of the _declared_ function that is _not_ a FUNCTION yet. 70 | * Once when it becomes an instance, it becomes a FUNCTION. 71 | */ 72 | fun multiply(operands: Operands): Value { 73 | return if (operands.isFloat) { 74 | Value(operands.left.number.toDouble() * operands.right.number.toDouble(), true) 75 | } else { 76 | Value(operands.left.number.toLong() * operands.right.number.toLong(), false) 77 | } 78 | } 79 | 80 | /** 81 | * Function for printing a value. 82 | */ 83 | @FUNCTION 84 | val printValue: (Value) -> Unit = { 85 | println(it) 86 | } 87 | 88 | @FUNCTION 89 | class with(private val right: Number): (Value) -> Operands { 90 | override fun invoke(left: Value): Operands { 91 | return Operands.of(left, Value(right)) 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/todo/AddNewTodo.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.todo 2 | 3 | import ac.obl.cc.FUNCTION 4 | import ac.obl.cc.todo.repo.SaveToDoInRepo 5 | 6 | /** 7 | * Function, takes explicit implementation of the repo ACTION. 8 | */ 9 | @FUNCTION 10 | class AddNewTodo(private val saveToDoInRepo: SaveToDoInRepo): (NewToDoItem) -> ToDoItem { 11 | override fun invoke(newToDoItem: NewToDoItem): ToDoItem { 12 | return saveToDoInRepo(newToDoItem) // explicit argument, cool! 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/todo/FindTodoByText.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.todo 2 | 3 | import ac.obl.cc.ACTION 4 | import ac.obl.cc.todo.repo.toDoRepo 5 | 6 | // Not a FUNCTION as has na implicit (hard-coded) call to another ACTION. 7 | 8 | @ACTION 9 | object FindTodoByText: (String) -> List { 10 | override fun invoke(text: String): List { 11 | return toDoRepo.filterToDoRecords(text) // implicit argument, not cool 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/todo/ToDoItem.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.todo 2 | 3 | import java.time.Instant 4 | 5 | data class TodoId(val value: Int) 6 | 7 | data class NewToDoItem(val task: String) 8 | 9 | data class ToDoItem(val id: TodoId, val task: String, val addedOn: Instant, val completed: Boolean = false) 10 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/todo/ToDoRunner.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.todo 2 | 3 | fun main() { 4 | with(toDoWithLogging) { 5 | actionOne() 6 | } 7 | } 8 | 9 | fun ToDo.actionOne() { 10 | val newTask1 = NewToDoItem("Buy coffee") 11 | val task1 = addNewTodo(newTask1) 12 | val task2 = addNewTodo(NewToDoItem("Finish paperwork")) 13 | println(task1) 14 | println(task2) 15 | findTodoByText("coffee").forEach { println(it.task) } 16 | } 17 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/todo/_bind.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.todo 2 | 3 | import ac.obl.cc.todo.repo.toDoRepo 4 | 5 | // Binding FUNCTION instances. 6 | 7 | data class ToDo( 8 | val addNewTodo: (NewToDoItem) -> ToDoItem, 9 | val findTodoByText: (String) -> List 10 | ) 11 | 12 | // default binding, used in production 13 | val toDo = ToDo( 14 | addNewTodo = AddNewTodo(toDoRepo.saveToDoInRepo), 15 | findTodoByText = FindTodoByText 16 | ) 17 | 18 | // enhanced binding, for debugging purposes 19 | val toDoWithLogging = toDo.copy( 20 | addNewTodo = { withLogging(toDo.addNewTodo, it, "AddNewTodo") } 21 | ) 22 | 23 | private fun withLogging(fn: (A) -> B, value: A, name: String): B { 24 | println("Before $name") 25 | val result = fn(value) 26 | println("After $name") 27 | return result 28 | } 29 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/todo/repo/Db.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.todo.repo 2 | 3 | import ac.obl.cc.STATE 4 | import java.time.Instant 5 | 6 | /** 7 | * Example of a state object. 8 | */ 9 | @STATE 10 | internal object Db { 11 | 12 | private val map = mutableMapOf() 13 | 14 | private fun nextId(): Int { 15 | return map.size + 1 16 | } 17 | 18 | @Synchronized fun save(record: ToDoRecord): ToDoRecord { 19 | return if (record.id == null) { 20 | val id = nextId() 21 | val recordToSave = record.copy(id = id, createdAt = Instant.now()) 22 | map[nextId()] = recordToSave 23 | recordToSave 24 | } else { 25 | map[record.id] = record 26 | record 27 | } 28 | } 29 | 30 | // We minimize the number of operations you can do with the state with generic snapshot iterator. 31 | 32 | @Synchronized fun snapshot(filter: (ToDoRecord) -> Boolean): List { 33 | return map.values.filter(filter).toList() 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/todo/repo/FilterToDoRecords.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.todo.repo 2 | 3 | import ac.obl.cc.ACTION 4 | 5 | // Uses a STATE and becomes an ACTION. 6 | 7 | @ACTION 8 | internal val filterToDoRecordsWithDb: FilterToDoRecords = { text -> 9 | Db.snapshot { it.task.contains(text) } 10 | .map { it.toToDoItem() } 11 | .toList() 12 | } 13 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/todo/repo/SaveToDoInRepo.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.todo.repo 2 | 3 | import ac.obl.cc.ACTION 4 | 5 | // Uses a STATE and becomes an ACTION. 6 | 7 | @ACTION 8 | internal val saveToDoInRepoWithDb: SaveToDoInRepo = { newToDoItem -> 9 | newToDoItem 10 | .toToDoRecord() 11 | .let { Db.save(it) } 12 | .toToDoItem() 13 | } 14 | 15 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/todo/repo/ToDoRecord.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.todo.repo 2 | 3 | import ac.obl.cc.DATA 4 | import ac.obl.cc.todo.NewToDoItem 5 | import ac.obl.cc.todo.ToDoItem 6 | import ac.obl.cc.todo.TodoId 7 | import java.time.Instant 8 | 9 | @DATA 10 | data class ToDoRecord( 11 | val id: Int?, 12 | val task: String, 13 | val createdAt: Instant?, 14 | val completed: Boolean = false, 15 | val completedAt: Instant? = null 16 | ) 17 | 18 | fun NewToDoItem.toToDoRecord(): ToDoRecord { 19 | return ToDoRecord( 20 | id = null, 21 | task = this.task, 22 | createdAt = Instant.now() 23 | ) 24 | } 25 | 26 | fun ToDoRecord.toToDoItem(): ToDoItem { 27 | return ToDoItem( 28 | id = TodoId(this.id!!), 29 | task = this.task, 30 | addedOn = this.createdAt!!, 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /examples/src/main/kotlin/ac/obl/cc/todo/repo/_bind.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.cc.todo.repo 2 | 3 | import ac.obl.cc.todo.NewToDoItem 4 | import ac.obl.cc.todo.ToDoItem 5 | 6 | typealias SaveToDoInRepo = (NewToDoItem) -> ToDoItem 7 | typealias FilterToDoRecords = (String) -> List 8 | 9 | // Abstract barrier for the ACTIONs. 10 | // ACTIONs are internal to prevent leaking. 11 | 12 | data class ToDoRepo( 13 | val saveToDoInRepo: SaveToDoInRepo = saveToDoInRepoWithDb, 14 | val filterToDoRecords: FilterToDoRecords = filterToDoRecordsWithDb 15 | ) 16 | 17 | val toDoRepo = ToDoRepo() 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ktor_version=2.0.1 2 | kotlin_version=1.6.21 3 | gradle_version=7.4.2 4 | logback_version=1.2.3 5 | exposed_version=0.38.2 6 | serialization_version=1.3.2 7 | kotlin.code.style=official 8 | 9 | org.gradle.java.installations.auto-download=false 10 | org.gradle.java.installations.auto-detect=false 11 | org.gradle.java.installations.fromEnv=JAVA_HOME 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igr/color-code/f6da8eb458e70626a06f289109ebf65d01005c9f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /pectopah/.env: -------------------------------------------------------------------------------- 1 | POSTGRES_DB=pectopah 2 | POSTGRES_USER=puser 3 | POSTGRES_PASSWORD=ppass -------------------------------------------------------------------------------- /pectopah/api/http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "restaurantId": "d6e08151-4f18-4c42-b4d9-d29008cabbf2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pectopah/api/reservations.http: -------------------------------------------------------------------------------- 1 | ### New reservation 2 | POST http://localhost:8080/reservations 3 | Content-Type: application/json 4 | 5 | { 6 | "restaurantId": {{restaurantId}}, 7 | "people": "2", 8 | "day": "2022-04-22", 9 | "time": "12:00", 10 | "duration": "120" 11 | } 12 | -------------------------------------------------------------------------------- /pectopah/api/restaurants.http: -------------------------------------------------------------------------------- 1 | ### List of restaurants 2 | GET http://localhost:8080/restaurants 3 | Accept: application/json 4 | 5 | ### New restaurant 6 | POST http://localhost:8080/restaurants 7 | Content-Type: application/json 8 | 9 | { 10 | "name": "LeChick", 11 | "maxDuration": 180 12 | } 13 | -------------------------------------------------------------------------------- /pectopah/boot/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id('org.jetbrains.kotlin.jvm') 3 | } 4 | 5 | test { 6 | useJUnitPlatform() 7 | } 8 | 9 | dependencies { 10 | implementation project(':ctx') 11 | implementation project(':pectopah:model') 12 | implementation project(':pectopah:model-serializers') 13 | implementation project(':pectopah:office-api') 14 | implementation project(':pectopah:office') 15 | implementation project(':pectopah:repo-api') 16 | implementation project(':pectopah:repo-db') 17 | 18 | implementation "io.ktor:ktor-server-content-negotiation-jvm:$ktor_version" 19 | implementation "io.ktor:ktor-server-core-jvm:$ktor_version" 20 | implementation "io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version" 21 | implementation "io.ktor:ktor-server-call-logging-jvm:$ktor_version" 22 | implementation "io.ktor:ktor-server-default-headers-jvm:$ktor_version" 23 | implementation "io.ktor:ktor-server-compression-jvm:$ktor_version" 24 | implementation "io.ktor:ktor-server-netty-jvm:$ktor_version" 25 | implementation "ch.qos.logback:logback-classic:$logback_version" 26 | 27 | implementation "org.jetbrains.exposed:exposed-core:$exposed_version" 28 | implementation "com.zaxxer:HikariCP:4.0.3" 29 | runtimeOnly "org.postgresql:postgresql:42.3.3" 30 | 31 | testImplementation "io.ktor:ktor-server-tests-jvm:$ktor_version" 32 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" 33 | } 34 | -------------------------------------------------------------------------------- /pectopah/boot/src/main/kotlin/ac/obl/pectopah/boot/Application.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.boot 2 | 3 | import ac.obl.pectopah.boot.server.* 4 | import io.ktor.server.engine.* 5 | import io.ktor.server.netty.* 6 | import org.slf4j.Logger 7 | import org.slf4j.LoggerFactory 8 | 9 | // This is the `boot` module, that wires everything together and runs the application. 10 | 11 | fun main() { 12 | embeddedServer(Netty, port = 8080, host = "0.0.0.0") { 13 | configureRouting() 14 | configureSerialization() 15 | configureMonitoring() 16 | configureHTTP() 17 | configureDb() 18 | }.start(wait = true) 19 | } 20 | 21 | val appLogger: Logger = LoggerFactory.getLogger("App") 22 | -------------------------------------------------------------------------------- /pectopah/boot/src/main/kotlin/ac/obl/pectopah/boot/server/DB.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.boot.server 2 | 3 | import ac.obl.pectopah.boot.appLogger 4 | import com.zaxxer.hikari.HikariConfig 5 | import com.zaxxer.hikari.HikariDataSource 6 | import io.ktor.server.application.* 7 | import org.jetbrains.exposed.sql.Database 8 | 9 | fun Application.configureDb() { 10 | Database.connect(hikari()) 11 | } 12 | 13 | private fun hikari(): HikariDataSource { 14 | val config = HikariConfig("/hikari.properties") 15 | if (System.getenv("DATABASE_DB") != null) { 16 | val databaseDb = System.getenv("DATABASE_DB") 17 | config.jdbcUrl = "jdbc:postgresql://$databaseDb" 18 | } 19 | if (System.getenv("DATABASE_USER") != null) { 20 | config.username = System.getenv("DATABASE_USER") 21 | } 22 | if (System.getenv("DATABASE_PASS") != null) { 23 | config.password = System.getenv("DATABASE_PASS") 24 | } 25 | 26 | config.validate() 27 | 28 | appLogger.info("Connecting to: ${config.jdbcUrl}") 29 | 30 | return HikariDataSource(config) 31 | } 32 | -------------------------------------------------------------------------------- /pectopah/boot/src/main/kotlin/ac/obl/pectopah/boot/server/HTTP.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.boot.server 2 | 3 | import io.ktor.server.application.* 4 | import io.ktor.server.plugins.compression.* 5 | import io.ktor.server.plugins.defaultheaders.* 6 | 7 | fun Application.configureHTTP() { 8 | install(DefaultHeaders) { 9 | header("X-Engine", "Ktor") 10 | } 11 | install(Compression) { 12 | gzip { 13 | priority = 1.0 14 | } 15 | deflate { 16 | priority = 10.0 17 | minimumSize(1024) 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /pectopah/boot/src/main/kotlin/ac/obl/pectopah/boot/server/Log.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.boot.server 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory 5 | 6 | /** 7 | * Defines ac.obl.pectopah.boot.server.logger. 8 | */ 9 | inline fun T.logger(): Logger { 10 | return LoggerFactory.getLogger(T::class.java) 11 | } 12 | 13 | -------------------------------------------------------------------------------- /pectopah/boot/src/main/kotlin/ac/obl/pectopah/boot/server/Monitoring.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.boot.server 2 | 3 | import io.ktor.server.request.* 4 | import io.ktor.server.application.* 5 | import io.ktor.server.plugins.callloging.* 6 | import org.slf4j.event.* 7 | 8 | fun Application.configureMonitoring() { 9 | install(CallLogging) { 10 | level = Level.INFO 11 | filter { call -> call.request.path().startsWith("/") } 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /pectopah/boot/src/main/kotlin/ac/obl/pectopah/boot/server/Routing.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.boot.server 2 | 3 | import ac.obl.cc.ctx.CtxD 4 | import ac.obl.pectopah.boot.appLogger 5 | import ac.obl.pectopah.boot.office 6 | import ac.obl.pectopah.model.* 7 | import io.ktor.http.* 8 | import io.ktor.server.application.* 9 | import io.ktor.server.request.* 10 | import io.ktor.server.response.* 11 | import io.ktor.server.routing.* 12 | import org.jetbrains.exposed.sql.StdOutSqlLogger 13 | import org.jetbrains.exposed.sql.addLogger 14 | import org.jetbrains.exposed.sql.transactions.transaction 15 | 16 | // This class should be split into multiple classes and put in a different package 17 | // Here I use the delayed context `CtxD` just for fun - and it makes sense to be used here. 18 | 19 | fun Application.configureRouting() { 20 | 21 | routing { 22 | post("/restaurants") { 23 | ((CtxD 24 | + call.receive() 25 | + convertNewRestaurantInputToModel 26 | + office.createNewRestaurant 27 | + convertRestaurantToOutput 28 | ) 29 | * transactional() 30 | )() 31 | .also { call.respond(it) } 32 | } 33 | get("/restaurants") { 34 | ((CtxD 35 | + office.listRestaurants 36 | + { it.map(convertRestaurantToOutput) } 37 | ) 38 | * transactional() 39 | )() 40 | .also { call.respond(it) } 41 | 42 | } 43 | post("/reservations") { 44 | ((CtxD 45 | + call.receive() 46 | + convertReservationRequestInputToModel 47 | + office.makeReservation 48 | ) 49 | * transactional() 50 | )() 51 | .also { call.respond(it) } 52 | } 53 | 54 | forEachRoute { 55 | appLogger.info("|> $it") 56 | } 57 | } 58 | } 59 | 60 | // Helper method that starts transactions, without aspects. 61 | private fun transactional(): (() -> T) -> T = { 62 | transaction { 63 | addLogger(StdOutSqlLogger) 64 | it() 65 | } 66 | } 67 | 68 | /** 69 | * Iterates over all defined routes up to this point. 70 | */ 71 | private fun Routing.forEachRoute(routeConsumer: (Route) -> Unit) { 72 | val root = this 73 | val allRoutes = allRoutes(root) 74 | val allRoutesWithMethod = allRoutes.filter { it.selector is HttpMethodRouteSelector }.sortedBy { it.toString() } 75 | allRoutesWithMethod.forEach(routeConsumer) 76 | } 77 | 78 | private fun allRoutes(root: Route): List { 79 | return listOf(root) + root.children.flatMap { allRoutes(it) } 80 | } 81 | 82 | private fun Parameters.parametersToSupplier() = { name: String -> this[name].toString() } 83 | -------------------------------------------------------------------------------- /pectopah/boot/src/main/kotlin/ac/obl/pectopah/boot/server/Serialization.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.boot.server 2 | 3 | import io.ktor.serialization.kotlinx.json.* 4 | import io.ktor.server.application.* 5 | import io.ktor.server.plugins.contentnegotiation.* 6 | import kotlinx.serialization.json.Json 7 | 8 | fun Application.configureSerialization() { 9 | install(ContentNegotiation) { 10 | json(Json { 11 | prettyPrint = true 12 | isLenient = true 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pectopah/boot/src/main/kotlin/ac/obl/pectopah/boot/wire.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.boot 2 | 3 | import ac.obl.pectopah.officeApi 4 | import ac.obl.pectopah.repo.repoApiDb 5 | 6 | // application wiring, uses provided implementations 7 | 8 | private val repo = repoApiDb() 9 | val office = officeApi(repo) 10 | -------------------------------------------------------------------------------- /pectopah/boot/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | development = true 3 | 4 | deployment { 5 | port = 8080 6 | reload = true 7 | watch = [build, classes, resources] 8 | } 9 | 10 | application { 11 | modules = [ 12 | ac.obl.pectopah.server.ServerKt.module, 13 | ] 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /pectopah/boot/src/main/resources/hikari.properties: -------------------------------------------------------------------------------- 1 | driverClassName=org.postgresql.Driver 2 | jdbcUrl=jdbc:postgresql://localhost:5432/pectopah 3 | maximumPoolSize=10 4 | autoCommit=false 5 | transactionIsolation=TRANSACTION_REPEATABLE_READ 6 | username=puser 7 | password=ppass 8 | -------------------------------------------------------------------------------- /pectopah/boot/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 7 | 8 | %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /pectopah/db.sqs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 512.00 8 | 960.00 9 | 10 | 11 | 192.00 12 | 140.00 13 | 14 | 2 15 | 16 | 17 | 18 | 1 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | id 30 | restaurants 31 | 32 | 33 | 5 34 | 1 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 192.00 78 | 1088.00 79 | 80 | 81 | 191.00 82 | 80.00 83 | 84 | 1 85 | 86 | 87 | 88 | 1 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | id 98 | restaurants 99 | 100 | 101 | 5 102 | 1 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 448.00 125 | 1216.00 126 | 127 | 128 | 192.00 129 | 64.00 130 | 131 | 3 132 | 133 | 134 | 135 | id 136 | reservations 137 | 138 | 139 | 5 140 | 1 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | id 150 | tables 151 | 152 | 153 | 5 154 | 1 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 192.00 169 | 960.00 170 | 171 | 172 | 233.00 173 | 80.00 174 | 175 | 0 176 | 177 | 178 | 179 | 1 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /pectopah/db_reset.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker-compose down 4 | docker volume rm pectopah_pdb_data 5 | docker-compose up -d 6 | -------------------------------------------------------------------------------- /pectopah/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | pdb: 5 | image: postgres:14.2 6 | container_name: pdb 7 | restart: "always" 8 | ports: 9 | - "5432:5432" 10 | env_file: 11 | - .env 12 | volumes: 13 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql 14 | - pdb_data:/var/lib/postgresql/data 15 | 16 | volumes: 17 | pdb_data: 18 | -------------------------------------------------------------------------------- /pectopah/init.sql: -------------------------------------------------------------------------------- 1 | /* SQLEditor (Postgres)*/ 2 | 3 | 4 | CREATE TABLE restaurants 5 | ( 6 | id UUID NOT NULL UNIQUE , 7 | name TEXT NOT NULL UNIQUE , 8 | max_duration INTEGER NOT NULL DEFAULT 120, 9 | CONSTRAINT restaurants_pkey PRIMARY KEY (id) 10 | ); 11 | 12 | CREATE TABLE reservations 13 | ( 14 | id UUID NOT NULL UNIQUE , 15 | restaurant_id UUID NOT NULL, 16 | people INTEGER NOT NULL, 17 | date DATE NOT NULL, 18 | time INTEGER NOT NULL, 19 | duration INTEGER NOT NULL, 20 | CONSTRAINT reservations_pkey PRIMARY KEY (id) 21 | ); 22 | 23 | CREATE TABLE tables 24 | ( 25 | id UUID NOT NULL UNIQUE , 26 | restaurant_id UUID NOT NULL, 27 | places INTEGER NOT NULL, 28 | CONSTRAINT tables_pkey PRIMARY KEY (id) 29 | ); 30 | 31 | CREATE TABLE reserved_tables 32 | ( 33 | reservation_id UUID NOT NULL, 34 | table_id UUID NOT NULL 35 | ); 36 | 37 | CREATE INDEX restaurants_name_idx ON restaurants(name); 38 | 39 | CREATE INDEX reservations_restaurant_id_idx ON reservations(restaurant_id); 40 | 41 | ALTER TABLE reservations ADD FOREIGN KEY (restaurant_id) REFERENCES restaurants (id); 42 | 43 | CREATE INDEX reservations_date_idx ON reservations(date); 44 | 45 | CREATE INDEX tables_restaurant_id_idx ON tables(restaurant_id); 46 | 47 | ALTER TABLE tables ADD FOREIGN KEY (restaurant_id) REFERENCES restaurants (id); 48 | 49 | ALTER TABLE reserved_tables ADD FOREIGN KEY (reservation_id) REFERENCES reservations (id); 50 | 51 | ALTER TABLE reserved_tables ADD FOREIGN KEY (table_id) REFERENCES tables (id); 52 | -------------------------------------------------------------------------------- /pectopah/model-serializers/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' 3 | id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version" 4 | } 5 | 6 | dependencies { 7 | implementation project(":pectopah:model") 8 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version" 9 | implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2" 10 | } 11 | -------------------------------------------------------------------------------- /pectopah/model-serializers/src/main/kotlin/ac/obl/pectopah/model/NewRestaurantSerializer.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.model 2 | 3 | import kotlinx.serialization.Serializable 4 | import java.time.Duration 5 | 6 | @Serializable 7 | data class NewRestaurantInput( 8 | val name: String, 9 | val maxDuration: Int = 120 10 | ) 11 | 12 | val convertNewRestaurantInputToModel: (NewRestaurantInput) -> NewRestaurant = { 13 | NewRestaurant( 14 | name = it.name, 15 | maxDuration = Duration.ofMinutes(it.maxDuration.toLong()) 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /pectopah/model-serializers/src/main/kotlin/ac/obl/pectopah/model/ReservationRequestSerializer.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.model 2 | 3 | import kotlinx.datetime.LocalDate 4 | import kotlinx.datetime.toJavaLocalDate 5 | import kotlinx.serialization.Serializable 6 | import java.time.Duration 7 | import java.time.LocalTime 8 | import java.time.format.DateTimeFormatter 9 | import java.util.* 10 | 11 | @Serializable 12 | data class ReservationRequestInput( 13 | val restaurantId: String, 14 | val people: Int, 15 | val day: LocalDate, 16 | val time: String, 17 | val duration: Int 18 | ) 19 | 20 | val convertReservationRequestInputToModel: (ReservationRequestInput) -> ReservationRequest = { 21 | ReservationRequest( 22 | restaurantId = RestaurantId(UUID.fromString(it.restaurantId)), 23 | places = it.people, 24 | slot = ReservationSlot( 25 | day = it.day.toJavaLocalDate(), 26 | time = LocalTime.parse(it.time, DateTimeFormatter.ISO_LOCAL_TIME), 27 | duration = Duration.ofMinutes(it.duration.toLong()) 28 | ) 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /pectopah/model-serializers/src/main/kotlin/ac/obl/pectopah/model/RestaurantSerializer.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("RestaurantSerializerKt") 2 | 3 | package ac.obl.pectopah.model 4 | 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class RestaurantOutput( 9 | val id: String, 10 | val name: String, 11 | val maxDuration: Int = 120 12 | ) 13 | 14 | val convertRestaurantToOutput: (Restaurant) -> RestaurantOutput = { 15 | RestaurantOutput( 16 | id = it.id.uuid.toString(), 17 | name = it.name, 18 | maxDuration = it.maxDuration.toMinutes().toInt() 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /pectopah/model-serializers/src/main/kotlin/ac/obl/pectopah/model/serializers.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.model 2 | 3 | // TODO the preferable way doing it is not working: https://github.com/Kotlin/kotlinx.serialization/issues/532 4 | 5 | //@Serializer(forClass = Restaurant::class) 6 | //object RestaurantSerializer 7 | 8 | // Anyway, it can be generated. Alternatively, we may add serialization dependency 9 | // to `models` module and use `@Serializable` annotation. 10 | -------------------------------------------------------------------------------- /pectopah/model/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id('org.jetbrains.kotlin.jvm') 3 | } 4 | -------------------------------------------------------------------------------- /pectopah/model/src/main/kotlin/ac/obl/pectopah/model/Id.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.model 2 | 3 | import java.util.* 4 | 5 | // All IDs are UUIDs 6 | 7 | interface Id { 8 | val uuid: UUID 9 | } 10 | -------------------------------------------------------------------------------- /pectopah/model/src/main/kotlin/ac/obl/pectopah/model/Reservation.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.model 2 | 3 | import java.util.* 4 | 5 | data class ReservationId(override val uuid: UUID) : Id 6 | 7 | data class Reservation( 8 | val id: ReservationId, 9 | val restaurantId: RestaurantId, 10 | val slot: ReservationSlot, 11 | val persons: Int, 12 | ) 13 | -------------------------------------------------------------------------------- /pectopah/model/src/main/kotlin/ac/obl/pectopah/model/ReservationRequest.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.model 2 | 3 | data class ReservationRequest( 4 | val restaurantId: RestaurantId, 5 | val slot: ReservationSlot, 6 | val places: Int, 7 | ) 8 | -------------------------------------------------------------------------------- /pectopah/model/src/main/kotlin/ac/obl/pectopah/model/ReservationSlot.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.model 2 | 3 | import java.time.Duration 4 | import java.time.LocalDate 5 | import java.time.LocalTime 6 | 7 | data class ReservationSlot(val day: LocalDate, val time: LocalTime, val duration: Duration) { 8 | // example of calculated property 9 | val endTime: LocalTime = time.plus(duration) 10 | } 11 | -------------------------------------------------------------------------------- /pectopah/model/src/main/kotlin/ac/obl/pectopah/model/ReservedRestaurantTables.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.model 2 | 3 | data class ReservedRestaurantTables( 4 | val reservation: Reservation, 5 | val tables: List 6 | ) 7 | -------------------------------------------------------------------------------- /pectopah/model/src/main/kotlin/ac/obl/pectopah/model/Restaurant.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.model 2 | 3 | import java.time.Duration 4 | import java.util.* 5 | 6 | data class RestaurantId(override val uuid: UUID) : Id 7 | 8 | data class Restaurant( 9 | val id: RestaurantId, 10 | val name: String, 11 | val maxDuration: Duration, 12 | ) 13 | 14 | data class NewRestaurant( 15 | val name: String, 16 | val maxDuration: Duration, 17 | ) 18 | -------------------------------------------------------------------------------- /pectopah/model/src/main/kotlin/ac/obl/pectopah/model/RestaurantTable.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.model 2 | 3 | import java.util.* 4 | 5 | data class RestaurantTableId(override val uuid: UUID) : Id 6 | 7 | data class RestaurantTable( 8 | val id: RestaurantTableId, 9 | val restaurantId: RestaurantId, 10 | val places: Int 11 | ) 12 | 13 | data class NewRestaurantTable( 14 | val restaurantId: RestaurantId, 15 | val places: Int 16 | ) 17 | -------------------------------------------------------------------------------- /pectopah/model/src/main/kotlin/ac/obl/pectopah/model/RestaurantTopology.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.model 2 | 3 | data class RestaurantTopology( 4 | val restaurant: Restaurant, 5 | val tables: List, 6 | ) 7 | -------------------------------------------------------------------------------- /pectopah/office-api/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id('org.jetbrains.kotlin.jvm') 3 | } 4 | 5 | test { 6 | useJUnitPlatform() 7 | } 8 | 9 | dependencies { 10 | api project(":pectopah:model") 11 | 12 | testImplementation 'org.jetbrains.kotlin:kotlin-test' 13 | } 14 | -------------------------------------------------------------------------------- /pectopah/office-api/src/main/kotlin/ac/obl/pectopah/api.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah 2 | 3 | import ac.obl.pectopah.model.NewRestaurant 4 | import ac.obl.pectopah.model.Reservation 5 | import ac.obl.pectopah.model.ReservationRequest 6 | import ac.obl.pectopah.model.Restaurant 7 | 8 | // this is the business API. 9 | 10 | data class OfficeApi( 11 | val makeReservation: (ReservationRequest) -> Reservation, 12 | val createNewRestaurant: (NewRestaurant) -> Restaurant, 13 | val listRestaurants: () -> List 14 | ) 15 | -------------------------------------------------------------------------------- /pectopah/office/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id('org.jetbrains.kotlin.jvm') 3 | } 4 | 5 | test { 6 | useJUnitPlatform() 7 | } 8 | 9 | dependencies { 10 | api project(":pectopah:model") 11 | implementation project(':pectopah:office-api') 12 | implementation project(':pectopah:repo-api') 13 | 14 | testImplementation 'org.jetbrains.kotlin:kotlin-test' 15 | } 16 | -------------------------------------------------------------------------------- /pectopah/office/src/main/kotlin/ac/obl/pectopah/CreateNewRestaurant.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah 2 | 3 | import ac.obl.pectopah.model.NewRestaurant 4 | import ac.obl.pectopah.model.Restaurant 5 | 6 | class CreateNewRestaurant(private val storeNewRestaurant: (NewRestaurant) -> Restaurant): (NewRestaurant) -> Restaurant { 7 | 8 | override fun invoke(newRestaurant: NewRestaurant): Restaurant { 9 | return storeNewRestaurant(newRestaurant) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pectopah/office/src/main/kotlin/ac/obl/pectopah/ListAllRestaurants.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah 2 | 3 | import ac.obl.pectopah.model.Restaurant 4 | 5 | class ListAllRestaurants( 6 | private val fetchAllRestaurants: () -> Sequence 7 | ): () -> List { 8 | 9 | override fun invoke(): List { 10 | return fetchAllRestaurants().toList() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pectopah/office/src/main/kotlin/ac/obl/pectopah/MakeReservation.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah 2 | 3 | import ac.obl.pectopah.model.* 4 | import java.time.LocalDate 5 | 6 | class MakeReservation( 7 | private val fetchAllRestaurantTables: (RestaurantId) -> Sequence, 8 | private val findRestaurantReservationsForDay: (RestaurantId, LocalDate) -> List, 9 | private val storeReservation: (ReservationRequest, List) -> Reservation, 10 | ) : (ReservationRequest) -> Reservation { 11 | 12 | override fun invoke(newReservation: ReservationRequest): Reservation { 13 | // first fetch 14 | val tables = fetchAllRestaurantTables(newReservation.restaurantId) 15 | val reservationsForDay = findRestaurantReservationsForDay(newReservation.restaurantId, newReservation.slot.day) 16 | 17 | // then process 18 | return tables 19 | .filter { tableHasCapacity(it, newReservation.places) } 20 | .filter { tableNotOccupied(it, newReservation.slot, reservationsForDay) } 21 | .map { storeReservation(newReservation, listOf(it)) } 22 | .firstOrNull() ?: throw IllegalStateException("Reservation not available") 23 | } 24 | 25 | // Function has 3 arguments, hence it is not a public one. 26 | // To become a public one, first two arguments here may be joined in the single one, 27 | // that has a meaning in a domain. 28 | private fun tableNotOccupied( 29 | table: RestaurantTable, 30 | slot: ReservationSlot, 31 | reservations: List, 32 | ): Boolean { 33 | return reservations 34 | .filter { it.tables.contains(table) } 35 | .all { slotsDoNotOverlap(it.reservation.slot, slot) } 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /pectopah/office/src/main/kotlin/ac/obl/pectopah/RestaurantTables.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah 2 | 3 | import ac.obl.pectopah.model.RestaurantTable 4 | 5 | fun tableHasCapacity(table: RestaurantTable, places: Int): Boolean { // two arguments 6 | return table.places >= places 7 | } 8 | -------------------------------------------------------------------------------- /pectopah/office/src/main/kotlin/ac/obl/pectopah/Slots.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah 2 | 3 | import ac.obl.pectopah.model.ReservationSlot 4 | 5 | fun slotsDoNotOverlap(slotA: ReservationSlot, slotB: ReservationSlot): Boolean { 6 | if (slotA.day != slotB.day) { 7 | return true 8 | } 9 | if (slotA.time.isBefore(slotB.time) && slotA.endTime.isAfter(slotB.time)) { 10 | return false 11 | } 12 | if (slotA.time.isBefore(slotB.endTime) && slotA.endTime.isAfter(slotB.endTime)) { 13 | return false 14 | } 15 | return true 16 | } 17 | -------------------------------------------------------------------------------- /pectopah/office/src/main/kotlin/ac/obl/pectopah/api.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah 2 | 3 | import ac.obl.pectopah.repo.RepoApi 4 | 5 | // Binding done for this implementation. 6 | 7 | val officeApi: (RepoApi) -> OfficeApi = { 8 | OfficeApi( 9 | makeReservation = MakeReservation( 10 | it.fetchAllRestaurantTables, 11 | it.findRestaurantReservationsForDay, 12 | it.storeReservation 13 | ), 14 | createNewRestaurant = CreateNewRestaurant(it.storeNewRestaurant), 15 | listRestaurants = ListAllRestaurants(it.fetchAllRestaurants) 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /pectopah/repo-api/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id('org.jetbrains.kotlin.jvm') 3 | } 4 | 5 | test { 6 | useJUnitPlatform() 7 | } 8 | 9 | dependencies { 10 | api project(':pectopah:model') 11 | 12 | testImplementation 'org.jetbrains.kotlin:kotlin-test' 13 | } 14 | -------------------------------------------------------------------------------- /pectopah/repo-api/src/main/kotlin/ac/obl/pectopah/repo/RepoApi.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo 2 | 3 | import ac.obl.pectopah.model.* 4 | import java.time.LocalDate 5 | 6 | data class RepoApi( 7 | val storeNewRestaurant: (NewRestaurant) -> Restaurant, 8 | val fetchAllRestaurants: () -> Sequence, 9 | val fetchAllRestaurantTables: (RestaurantId) -> Sequence, 10 | val findRestaurantById: (RestaurantId) -> Restaurant?, 11 | val findRestaurantReservationsForDay: (RestaurantId, LocalDate) -> List, 12 | val storeReservation: (ReservationRequest, List) -> Reservation, 13 | ) 14 | -------------------------------------------------------------------------------- /pectopah/repo-db/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id('org.jetbrains.kotlin.jvm') 3 | } 4 | 5 | test { 6 | useJUnitPlatform() 7 | } 8 | 9 | dependencies { 10 | api project(':pectopah:model') 11 | implementation project(':pectopah:repo-api') 12 | 13 | implementation("org.jetbrains.exposed:exposed-core:$exposed_version") 14 | implementation("org.jetbrains.exposed:exposed-dao:$exposed_version") 15 | implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") 16 | implementation("org.jetbrains.exposed:exposed-java-time:$exposed_version") 17 | 18 | testImplementation 'org.jetbrains.kotlin:kotlin-test' 19 | } 20 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/FetchAllRestaurantTables.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo 2 | 3 | import ac.obl.pectopah.model.RestaurantId 4 | import ac.obl.pectopah.model.RestaurantTable 5 | import ac.obl.pectopah.repo.tables.Tables 6 | import ac.obl.pectopah.repo.tables.toRestaurantTable 7 | import org.jetbrains.exposed.sql.select 8 | 9 | class FetchAllRestaurantTables: (RestaurantId) -> Sequence { 10 | override fun invoke(restaurantId: RestaurantId): Sequence { 11 | return Tables 12 | .select { Tables.restaurantId eq restaurantId.uuid } 13 | .map { it.toRestaurantTable() } 14 | .asSequence() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/FetchAllRestaurants.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo 2 | 3 | import ac.obl.pectopah.model.Restaurant 4 | import ac.obl.pectopah.repo.tables.Restaurants 5 | import ac.obl.pectopah.repo.tables.Restaurants.name 6 | import ac.obl.pectopah.repo.tables.toRestaurant 7 | import org.jetbrains.exposed.sql.selectAll 8 | 9 | class FetchAllRestaurants: () -> Sequence { 10 | override fun invoke(): Sequence { 11 | return Restaurants 12 | .selectAll() 13 | .orderBy(name) 14 | .map { it.toRestaurant() } 15 | .asSequence() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/FindRestaurantById.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo 2 | 3 | import ac.obl.pectopah.model.Restaurant 4 | import ac.obl.pectopah.model.RestaurantId 5 | import ac.obl.pectopah.repo.tables.Restaurants 6 | import ac.obl.pectopah.repo.tables.toRestaurant 7 | import org.jetbrains.exposed.sql.select 8 | 9 | class FindRestaurantById: (RestaurantId) -> Restaurant? { 10 | override fun invoke(restaurantId: RestaurantId): Restaurant? { 11 | return Restaurants 12 | .select { Restaurants.id eq restaurantId } 13 | .map { it.toRestaurant() } 14 | .firstOrNull() 15 | } 16 | 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/FindRestaurantReservationsForDay.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo 2 | 3 | import ac.obl.pectopah.model.Reservation 4 | import ac.obl.pectopah.model.ReservedRestaurantTables 5 | import ac.obl.pectopah.model.RestaurantId 6 | import ac.obl.pectopah.model.RestaurantTable 7 | import ac.obl.pectopah.repo.tables.* 8 | import org.jetbrains.exposed.sql.and 9 | import org.jetbrains.exposed.sql.select 10 | import java.time.LocalDate 11 | 12 | class FindRestaurantReservationsForDay: (RestaurantId, LocalDate) -> List { 13 | override fun invoke(restaurantId: RestaurantId, localDate: LocalDate): List { 14 | return Reservations 15 | .select { 16 | (Reservations.restaurantId eq restaurantId) and (Reservations.date eq localDate) 17 | } 18 | .map { it.toReservation() } 19 | .map { 20 | ReservedRestaurantTables(it, reservedTables(it)) 21 | } 22 | } 23 | 24 | private fun reservedTables(reservation: Reservation): List { 25 | return (Reservations innerJoin ReservedTables innerJoin Tables) 26 | .select { Reservations.id eq reservation.id} 27 | .map { it.toRestaurantTable() } 28 | .toList() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/StoreNewRestaurant.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo 2 | 3 | import ac.obl.pectopah.model.NewRestaurant 4 | import ac.obl.pectopah.model.Restaurant 5 | import ac.obl.pectopah.repo.tables.Restaurants 6 | import ac.obl.pectopah.repo.tables.record 7 | import ac.obl.pectopah.repo.tables.toRestaurant 8 | import org.jetbrains.exposed.sql.insert 9 | 10 | // ACTION 11 | class StoreNewRestaurant: (NewRestaurant) -> Restaurant { 12 | override fun invoke(newRestaurant: NewRestaurant): Restaurant { 13 | return Restaurants 14 | .insert { it.record(newRestaurant) } 15 | .result() 16 | .toRestaurant() 17 | } 18 | 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/StoreReservation.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo 2 | 3 | import ac.obl.pectopah.model.Reservation 4 | import ac.obl.pectopah.model.ReservationRequest 5 | import ac.obl.pectopah.model.RestaurantTable 6 | import ac.obl.pectopah.repo.tables.Reservations 7 | import ac.obl.pectopah.repo.tables.ReservedTables 8 | import ac.obl.pectopah.repo.tables.record 9 | import ac.obl.pectopah.repo.tables.toReservation 10 | import org.jetbrains.exposed.sql.insert 11 | 12 | class StoreReservation: (ReservationRequest, List) -> Reservation { 13 | override fun invoke(reservationRequest: ReservationRequest, tables: List): Reservation { 14 | 15 | val reservation = Reservations 16 | .insert { it.record(reservationRequest) } 17 | .result() 18 | .toReservation() 19 | 20 | tables.forEach { table -> 21 | ReservedTables.insert { it.record(reservation, table) } 22 | } 23 | 24 | return reservation 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/api.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo 2 | 3 | val repoApiDb: () -> RepoApi = { 4 | RepoApi( 5 | storeNewRestaurant = StoreNewRestaurant(), 6 | fetchAllRestaurants = FetchAllRestaurants(), 7 | fetchAllRestaurantTables = FetchAllRestaurantTables(), 8 | findRestaurantById = FindRestaurantById(), 9 | findRestaurantReservationsForDay = FindRestaurantReservationsForDay(), 10 | storeReservation = StoreReservation(), 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/db.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo 2 | 3 | import ac.obl.pectopah.model.Id 4 | import org.jetbrains.exposed.dao.UUIDEntity 5 | import org.jetbrains.exposed.dao.UUIDEntityClass 6 | import org.jetbrains.exposed.dao.id.EntityID 7 | import org.jetbrains.exposed.sql.Column 8 | import org.jetbrains.exposed.sql.Op 9 | import org.jetbrains.exposed.sql.ResultRow 10 | import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq 11 | import org.jetbrains.exposed.sql.statements.InsertStatement 12 | import java.util.* 13 | 14 | // ID-related extensions 15 | 16 | /** 17 | * Equals the ID of this entity to the given id. 18 | */ 19 | infix fun Column.eq(id: Id): Op = this@eq.eq(id.uuid) 20 | @JvmName("eqEntityID") 21 | infix fun Column>.eq(id: Id): Op = this@eq.eq(id.uuid) 22 | 23 | /** 24 | * Finds the entity with the given id. 25 | */ 26 | fun UUIDEntityClass.findById(id: Id): E? { 27 | return this.findById(id.uuid) 28 | } 29 | 30 | // Insert helpers 31 | 32 | fun InsertStatement<*>.result(): ResultRow { 33 | return this.resultedValues!!.first() 34 | } 35 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/tables/Reservations.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo.tables 2 | 3 | import ac.obl.pectopah.model.* 4 | import org.jetbrains.exposed.dao.id.UUIDTable 5 | import org.jetbrains.exposed.sql.ResultRow 6 | import org.jetbrains.exposed.sql.javatime.date 7 | import org.jetbrains.exposed.sql.statements.InsertStatement 8 | import java.time.Duration 9 | import java.time.LocalTime 10 | 11 | object Reservations : UUIDTable(name = "reservations") { 12 | val restaurantId = uuid("restaurant_id").references(Restaurants.id) 13 | val people = integer("people") 14 | val date = date("date") 15 | val time = integer("time") 16 | val duration = integer("duration") 17 | } 18 | fun ResultRow.toReservation() = Reservation( 19 | id = ReservationId(this[Reservations.id].value), 20 | restaurantId = RestaurantId(this[Reservations.restaurantId]), 21 | persons = this[Reservations.people], 22 | slot = ReservationSlot( 23 | day = this[Reservations.date], 24 | time = LocalTime.ofSecondOfDay(this[Reservations.time].toLong()), 25 | duration = Duration.ofMinutes(this[Reservations.duration].toLong()) 26 | ) 27 | ) 28 | fun InsertStatement.record(dao: ReservationRequest) { 29 | val insert = this 30 | with(Reservations) { 31 | insert[restaurantId] = dao.restaurantId.uuid 32 | insert[people] = dao.places 33 | insert[date] = dao.slot.day 34 | insert[time] = dao.slot.time.toSecondOfDay() 35 | insert[duration] = dao.slot.duration.toMinutes().toInt() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/tables/ReservedTables.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo.tables 2 | 3 | import ac.obl.pectopah.model.Reservation 4 | import ac.obl.pectopah.model.RestaurantTable 5 | import org.jetbrains.exposed.sql.Table 6 | import org.jetbrains.exposed.sql.statements.InsertStatement 7 | 8 | // intermediate table 9 | object ReservedTables : Table(name = "reserved_tables") { 10 | val reservationId = uuid("reservation_id").references(Reservations.id) 11 | val tableId = uuid("table_id").references(Tables.id) 12 | } 13 | fun InsertStatement.record(reservation: Reservation, table: RestaurantTable) { 14 | val insert = this 15 | with(ReservedTables) { 16 | insert[reservationId] = reservation.id.uuid 17 | insert[tableId] = table.id.uuid 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/tables/Restaurants.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo.tables 2 | 3 | import ac.obl.pectopah.model.NewRestaurant 4 | import ac.obl.pectopah.model.Restaurant 5 | import ac.obl.pectopah.model.RestaurantId 6 | import org.jetbrains.exposed.dao.id.UUIDTable 7 | import org.jetbrains.exposed.sql.ResultRow 8 | import org.jetbrains.exposed.sql.statements.InsertStatement 9 | import java.time.Duration 10 | 11 | object Restaurants : UUIDTable(name = "restaurants") { 12 | val name = varchar("name", 50) 13 | val maxDuration = integer("max_duration") 14 | } 15 | fun ResultRow.toRestaurant() = Restaurant( 16 | id = RestaurantId(this[Restaurants.id].value), 17 | name = this[Restaurants.name], 18 | maxDuration = Duration.ofMinutes(this[Restaurants.maxDuration].toLong()) 19 | ) 20 | fun InsertStatement.record(dao: NewRestaurant) { 21 | val insert = this 22 | with(Restaurants) { 23 | insert[name] = dao.name 24 | insert[maxDuration] = dao.maxDuration.toMinutes().toInt() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pectopah/repo-db/src/main/kotlin/ac/obl/pectopah/repo/tables/Tables.kt: -------------------------------------------------------------------------------- 1 | package ac.obl.pectopah.repo.tables 2 | 3 | import ac.obl.pectopah.model.RestaurantId 4 | import ac.obl.pectopah.model.RestaurantTable 5 | import ac.obl.pectopah.model.RestaurantTableId 6 | import org.jetbrains.exposed.dao.id.UUIDTable 7 | import org.jetbrains.exposed.sql.ResultRow 8 | 9 | object Tables : UUIDTable(name = "tables") { 10 | val restaurantId = uuid("restaurant_id").references(Restaurants.id) 11 | val places = integer("places") 12 | } 13 | fun ResultRow.toRestaurantTable() = RestaurantTable( 14 | id = RestaurantTableId(this[Tables.id].value), 15 | restaurantId = RestaurantId(this[Tables.restaurantId]), 16 | places = this[Tables.places] 17 | ) 18 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | enableFeaturePreview('VERSION_CATALOGS') 2 | 3 | include 'ctx' 4 | include 'examples' 5 | include 'pectopah' 6 | include 'pectopah:model' 7 | include 'pectopah:model-serializers' 8 | include 'pectopah:office-api' 9 | include 'pectopah:office' 10 | include 'pectopah:boot' 11 | include 'pectopah:repo-api' 12 | include 'pectopah:repo-db' 13 | --------------------------------------------------------------------------------