├── .gitignore ├── Artists.postman_collection ├── LICENSE.md ├── README.md ├── SQL ├── DDL_PG.sql └── DML_PG.sql ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── resources │ └── application.conf └── scala │ └── io │ └── github │ └── alejandrome │ ├── algebra │ ├── ArtistService.scala │ └── interpreter │ │ └── ArtistServiceInterpreter.scala │ ├── api │ ├── Api.scala │ ├── exception │ │ └── ExceptionHandlerApi.scala │ ├── helpers │ │ ├── Marshalling.scala │ │ └── Response.scala │ └── rejection │ │ └── RejectionHandlerApi.scala │ ├── boot │ └── Boot.scala │ ├── config │ └── ApplicationConfig.scala │ ├── model │ ├── ApiError.scala │ ├── Artist.scala │ └── Genre.scala │ └── repository │ ├── ArtistRepository.scala │ ├── Repository.scala │ └── impl │ ├── ArtistH2Repository.scala │ └── ArtistPGRepository.scala └── test └── scala └── io └── github └── alejandrome └── repository └── impl └── ArtistPGRepositoryTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | *.class 4 | *.log 5 | *.DS_Store 6 | lib_managed/ 7 | src_managed/ 8 | project/boot/ 9 | project/plugins/project/ 10 | .history 11 | .cache 12 | .lib/ 13 | dist/ 14 | LOG_PATH_IS_UNDEFINED/ 15 | 16 | -------------------------------------------------------------------------------- /Artists.postman_collection: -------------------------------------------------------------------------------- 1 | { 2 | "variables": [], 3 | "info": { 4 | "name": "Artists", 5 | "_postman_id": "3f043644-402e-5c51-8cc4-ccb63524416e", 6 | "description": "Collection with the basic requests for Artists API", 7 | "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" 8 | }, 9 | "item": [ 10 | { 11 | "name": "List all Artists", 12 | "request": { 13 | "url": "http://localhost:7101/api/artists", 14 | "method": "GET", 15 | "header": [], 16 | "body": {}, 17 | "description": "" 18 | }, 19 | "response": [] 20 | }, 21 | { 22 | "name": "Create New Artist", 23 | "request": { 24 | "url": "http://localhost:7101/api/artists", 25 | "method": "POST", 26 | "header": [ 27 | { 28 | "key": "Content-Type", 29 | "value": "application/json", 30 | "description": "" 31 | } 32 | ], 33 | "body": { 34 | "mode": "raw", 35 | "raw": "{\n \"artistID\": 0,\n \"stageName\": \"JEAN-MICHEL JARRE\",\n \"age\": 68\n}" 36 | }, 37 | "description": "" 38 | }, 39 | "response": [] 40 | }, 41 | { 42 | "name": "Update an Artist", 43 | "request": { 44 | "url": "http://localhost:7101/api/artists", 45 | "method": "PUT", 46 | "header": [ 47 | { 48 | "key": "Content-Type", 49 | "value": "application/json", 50 | "description": "" 51 | } 52 | ], 53 | "body": { 54 | "mode": "raw", 55 | "raw": "{\n \"artistID\": 25,\n \"stageName\": \"JEAN-MICHEL JARRE MODIFIED\",\n \"age\": 99\n}" 56 | }, 57 | "description": "" 58 | }, 59 | "response": [] 60 | }, 61 | { 62 | "name": "Query an artist by ID", 63 | "request": { 64 | "url": { 65 | "raw": "http://localhost:7101/api/artists?id=1", 66 | "protocol": "http", 67 | "host": [ 68 | "localhost" 69 | ], 70 | "port": "7101", 71 | "path": [ 72 | "api", 73 | "artists" 74 | ], 75 | "query": [ 76 | { 77 | "key": "id", 78 | "value": "1", 79 | "equals": true, 80 | "description": "" 81 | } 82 | ], 83 | "variable": [] 84 | }, 85 | "method": "GET", 86 | "header": [], 87 | "body": {}, 88 | "description": "" 89 | }, 90 | "response": [] 91 | }, 92 | { 93 | "name": "Query an artist by exact name", 94 | "request": { 95 | "url": { 96 | "raw": "http://localhost:7101/api/artists?name=KIM GORDON", 97 | "protocol": "http", 98 | "host": [ 99 | "localhost" 100 | ], 101 | "port": "7101", 102 | "path": [ 103 | "api", 104 | "artists" 105 | ], 106 | "query": [ 107 | { 108 | "key": "name", 109 | "value": "KIM GORDON", 110 | "equals": true, 111 | "description": "" 112 | } 113 | ], 114 | "variable": [] 115 | }, 116 | "method": "GET", 117 | "header": [], 118 | "body": {}, 119 | "description": "" 120 | }, 121 | "response": [] 122 | } 123 | ] 124 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alejandro Marín E. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReaderM 2 | 3 | 4 | This is a _toy project_ which I made for demonstrate some concepts in [my blog](https://alejandrome.github.io). It will be evolving with new functionalities that I'm going to explore. 5 | 6 | This is a runnable _microservice_ built with the following stack of technologies: 7 | 8 | * [Akka Http](http://doc.akka.io/docs/akka-http/current/scala/http/) (10.0.9) 9 | * [cats](http://typelevel.org/cats/) (0.9.0) 10 | * [doobie](https://github.com/tpolecat/doobie) (0.4.1) 11 | * [Circe](https://github.com/circe/circe) (0.8.0) 12 | * [spray Revolver](https://github.com/spray/sbt-revolver) (0.8.0) 13 | 14 | Of course, on top of Scala (2.12.2). 15 | 16 | This MS models a **_music domain_**: it documents my favourite artists, bands (not the same as artists), albums and songs. This because of my lack of _ideas_ for a domain :sweat_smile:, but it'll be enough for demonstration purposes. 17 | 18 | You can run the API typing the following commands in your terminal (using revolver): 19 | 20 | ```shell 21 | $ sbt 22 | > reStart 23 | ``` 24 | 25 | With this command you are starting a forked JVM which can be killed or `reStart`'d without killing your `sbt` session. That means that you can type in any moment `reStart` for re-starting your application if you've made any change. 26 | 27 | Also, you can run the application in the old-fashioned way: 28 | 29 | ```shell 30 | $ sbt 31 | > run 32 | ``` 33 | 34 | The service will be listening on port **7101**. 35 | 36 | The requests (in the actual version) that you can issue are: 37 | 38 | * `[GET]` `http://localhost:7101/api/artists` **to list all the artists available in the database.** 39 | * `[GET]` `http://localhost:7101/api/artists?id=` **to query an artist by his/her ID in the database.** 40 | * `[GET]` `http://localhost:7101/api/artists?name=` **to query an artist by his/her _EXACT_ name.** 41 | * `[POST]` `http://localhost:7101/api/artists` **to create a new artist in the database** 42 | * `[PUT]` `http://localhost:7101/api/artists` **to update** 43 | 44 | All the examples and the entities for creating and updating an artists are included in the project root as a [Postman](https://www.getpostman.com/) collection if you want to take a look and test it for yourself. 45 | 46 | # Database initialization 47 | 48 | This first version assumes that you have a local installation of PostgreSQL running in your machine with the default user/schema (`postgres`). You can run the `DDL_PG.sql` script (located in the `SQL/` folder of this project) in that schema to create all the database model and then run the `DML_PG.sql` that contains a very basic dataset. 49 | This is not yet automated, but it will in future versions, so you don't have to do it by hand. 50 | 51 | If you are running macOS you can use the excellent [Postgres.app](https://postgresapp.com/) that runs a complete instance of the database in your machine without configuring or messing up with a new installation of PG. 52 | 53 | In other OS's you need to install the Database engine and execute the scripts mentioned above. 54 | 55 | **REMEMBER:** the model is created in the `postgres` default schema. 56 | 57 | # Disclaimer 58 | 59 | As noted before, this is a _toy project_, so, things like exception handling, exception messages and so on can fail abruptly. This is going to be improved in future versions of this project. Again, the code here is only for demonstration purposes and it isn't intended for production use (yet?). 60 | 61 | # License 62 | 63 | The code in this repository is licensed under the MIT license. If you don't understand what does that means, here's an excerpt from [choosealicense](https://choosealicense.com/licenses/mit/): 64 | 65 | > A short and simple permissive license with conditions only requiring preservation of copyright and license notices. Licensed works, modifications, and larger works may be distributed under different terms and without source code. 66 | 67 | And that's it :) 68 | If you have questions you can get in touch with me on [Twitter](https://twitter.com/AlejandroM_E). 69 | 70 | AME. -------------------------------------------------------------------------------- /SQL/DDL_PG.sql: -------------------------------------------------------------------------------- 1 | --DROP TABLE SONGS; 2 | --DROP TABLE ALBUMS; 3 | --DROP TABLE BANDMEMBERS; 4 | --DROP TABLE BANDS; 5 | --DROP TABLE ARTISTS; 6 | --DROP TABLE GENRES; 7 | 8 | -- DROP TABLE ARTISTS; 9 | CREATE TABLE ARTISTS( 10 | ARTISTID SERIAL, 11 | STAGENAME VARCHAR(100) NOT NULL, 12 | AGE INTEGER NOT NULL 13 | ); 14 | 15 | ALTER TABLE ARTISTS ADD CONSTRAINT PK_ARTISTS PRIMARY KEY (ARTISTID); 16 | 17 | -- DROP TABLE BANDS; 18 | CREATE TABLE BANDS( 19 | BANDID SERIAL, 20 | BANDNAME VARCHAR(100) NOT NULL 21 | ); 22 | 23 | ALTER TABLE BANDS ADD CONSTRAINT PK_BANDS PRIMARY KEY (BANDID); 24 | 25 | -- DROP TABLE BANDMEMBERS; 26 | CREATE TABLE BANDMEMBERS( 27 | BANDID INTEGER, 28 | ARTISTID INTEGER 29 | ); 30 | 31 | ALTER TABLE BANDMEMBERS ADD CONSTRAINT PK_BANDMEMBERS PRIMARY KEY (BANDID, ARTISTID); 32 | ALTER TABLE BANDMEMBERS ADD CONSTRAINT FK_ARTIST FOREIGN KEY (ARTISTID) REFERENCES ARTISTS(ARTISTID); 33 | ALTER TABLE BANDMEMBERS ADD CONSTRAINT FK_BAND FOREIGN KEY (BANDID) REFERENCES BANDS(BANDID); 34 | 35 | -- DROP TABLE GENRES; 36 | CREATE TABLE GENRES( 37 | GENREID SERIAL, 38 | GENRENAME VARCHAR(100) NOT NULL 39 | ); 40 | 41 | ALTER TABLE GENRES ADD CONSTRAINT PK_GENRES PRIMARY KEY(GENREID); 42 | 43 | -- DROP TABLE ALBUMS; 44 | CREATE TABLE ALBUMS( 45 | ALBUMID SERIAL, 46 | ALBUMNAME VARCHAR(250) NOT NULL, 47 | AUTHOR INTEGER NOT NULL, 48 | GENRE INTEGER NOT NULL, 49 | RELEASEYEAR INTEGER NOT NULL, 50 | DURATION INTEGER NOT NULL -- IN MINUTES 51 | ); 52 | 53 | ALTER TABLE ALBUMS ADD CONSTRAINT PK_ALBUMS PRIMARY KEY (ALBUMID); 54 | ALTER TABLE ALBUMS ADD CONSTRAINT FK_AUTHOR FOREIGN KEY (AUTHOR) REFERENCES BANDS(BANDID); 55 | ALTER TABLE ALBUMS ADD CONSTRAINT FK_GENRE FOREIGN KEY (GENRE) REFERENCES GENRES(GENREID); 56 | 57 | -- DROP TABLE SONGS; 58 | CREATE TABLE SONGS( 59 | SONGID SERIAL, 60 | SONGNAME VARCHAR(250) NOT NULL, 61 | LENGTH INTEGER NOT NULL, -- IN MINUTES 62 | ALBUM INTEGER NOT NULL 63 | ); 64 | 65 | ALTER TABLE SONGS ADD CONSTRAINT PK_SONGS PRIMARY KEY (SONGID); 66 | ALTER TABLE SONGS ADD CONSTRAINT FK_ALBUM FOREIGN KEY (ALBUM) REFERENCES ALBUMS(ALBUMID); 67 | -------------------------------------------------------------------------------- /SQL/DML_PG.sql: -------------------------------------------------------------------------------- 1 | -- GENRES 2 | INSERT INTO GENRES(GENRENAME) VALUES ('ROCK'); 3 | INSERT INTO GENRES(GENRENAME) VALUES ('ALTERNATIVE ROCK'); 4 | INSERT INTO GENRES(GENRENAME) VALUES ('EXPERIMENTAL ROCK'); 5 | INSERT INTO GENRES(GENRENAME) VALUES ('FOLK ROCK'); 6 | INSERT INTO GENRES(GENRENAME) VALUES ('GARAGE ROCK'); 7 | INSERT INTO GENRES(GENRENAME) VALUES ('HARD ROCK'); 8 | INSERT INTO GENRES(GENRENAME) VALUES ('GLAM ROCK'); 9 | INSERT INTO GENRES(GENRENAME) VALUES ('HEAVY METAL'); 10 | INSERT INTO GENRES(GENRENAME) VALUES ('POP ROCK'); 11 | INSERT INTO GENRES(GENRENAME) VALUES ('PUNK ROCK'); 12 | INSERT INTO GENRES(GENRENAME) VALUES ('DISCO'); 13 | INSERT INTO GENRES(GENRENAME) VALUES ('FUNK'); 14 | INSERT INTO GENRES(GENRENAME) VALUES ('JAZZ'); 15 | INSERT INTO GENRES(GENRENAME) VALUES ('SOUL'); 16 | INSERT INTO GENRES(GENRENAME) VALUES ('POP'); 17 | INSERT INTO GENRES(GENRENAME) VALUES ('TANGO'); 18 | INSERT INTO GENRES(GENRENAME) VALUES ('LATIN'); 19 | INSERT INTO GENRES(GENRENAME) VALUES ('HIP HOP'); 20 | INSERT INTO GENRES(GENRENAME) VALUES ('REGGAE'); 21 | INSERT INTO GENRES(GENRENAME) VALUES ('RAP'); 22 | INSERT INTO GENRES(GENRENAME) VALUES ('ELECTRONIC'); 23 | 24 | -- ARTISTS 25 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('KIM GORDON', 64); 26 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('MICHAEL STIPE', 57); 27 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('DAVID BOWIE', 69); 28 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('BOB DYLAN', 76); 29 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('IGGY POP', 70); 30 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('ROGER DALTREY', 73); 31 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('RITCHIE BLACKMORE', 72); 32 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('LADY GAGA', 31); 33 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('JOHN LYDON', 61); 34 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('GLORIA GAYNOR', 67); 35 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('JAMES BROWN', 73); 36 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('MILES DAVIS', 65); 37 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('ARETHA FRANKLIN', 75); 38 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('CARLOS GARDEL', 44); 39 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('SHAKIRA', 40); 40 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('RZA', 47); 41 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('BOB MARLEY', 36); 42 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('MIKE D', 51); 43 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('CALVIN HARRIS', 33); 44 | INSERT INTO ARTISTS(STAGENAME, AGE) VALUES('THURSTON MOORE', 58); 45 | 46 | -- BANDS 47 | INSERT INTO BANDS(BANDNAME) VALUES('SONIC YOUTH'); 48 | INSERT INTO BANDS(BANDNAME) VALUES('IGGY POP'); 49 | INSERT INTO BANDS(BANDNAME) VALUES('REM'); 50 | INSERT INTO BANDS(BANDNAME) VALUES('THE WHO'); 51 | INSERT INTO BANDS(BANDNAME) VALUES('BEASTIE BOYS'); 52 | INSERT INTO BANDS(BANDNAME) VALUES('THE SEX PISTOLS'); 53 | INSERT INTO BANDS(BANDNAME) VALUES('WU-TANG CLAN'); 54 | INSERT INTO BANDS(BANDNAME) VALUES('BOB DYLAN'); 55 | INSERT INTO BANDS(BANDNAME) VALUES('SHAKIRA'); 56 | INSERT INTO BANDS(BANDNAME) VALUES('DAVID BOWIE'); 57 | INSERT INTO BANDS(BANDNAME) VALUES('LADY GAGA'); 58 | INSERT INTO BANDS(BANDNAME) VALUES('MILES DAVIS'); 59 | 60 | -- BANDMEMBERS 61 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(1, 1); 62 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(2, 5); 63 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(3, 2); 64 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(4, 6); 65 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(5, 18); 66 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(6, 9); 67 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(7, 16); 68 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(8, 4); 69 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(9, 15); 70 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(10, 3); 71 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(11, 8); 72 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(12, 12); 73 | INSERT INTO BANDMEMBERS(BANDID, ARTISTID) VALUES(1, 20); 74 | 75 | -- ALBUMS 76 | INSERT INTO ALBUMS(ALBUMNAME, AUTHOR, GENRE, RELEASEYEAR, DURATION) VALUES('Green', 3, 2, 1988, 41); 77 | INSERT INTO ALBUMS(ALBUMNAME, AUTHOR, GENRE, RELEASEYEAR, DURATION) VALUES('The Rise and Fall of Ziggy Stardust and the Spiders from Mars', 10, 7, 1972, 39); 78 | INSERT INTO ALBUMS(ALBUMNAME, AUTHOR, GENRE, RELEASEYEAR, DURATION) VALUES('Lust for Life', 2, 6, 1977, 42); 79 | INSERT INTO ALBUMS(ALBUMNAME, AUTHOR, GENRE, RELEASEYEAR, DURATION) VALUES('Born This Way', 11, 15, 1977, 62); 80 | INSERT INTO ALBUMS(ALBUMNAME, AUTHOR, GENRE, RELEASEYEAR, DURATION) VALUES('Kind of Blue', 12, 13, 1959, 62); 81 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.12.2" 2 | name := "ReaderM" 3 | organization := "io.github.alejandrome" 4 | version := "1.0" 5 | 6 | val circeVersion = "0.8.0" 7 | val doobieVersion = "0.4.1" 8 | val akkaHttpVersion = "10.0.9" 9 | 10 | libraryDependencies ++= Seq( 11 | "org.typelevel" %% "cats" % "0.9.0", 12 | "org.scalatest" %% "scalatest" % "3.0.3" % "test", 13 | "com.typesafe" % "config" % "1.3.1", 14 | "org.tpolecat" %% "doobie-core-cats" % doobieVersion, 15 | "org.tpolecat" %% "doobie-h2-cats" % doobieVersion, 16 | "org.tpolecat" %% "doobie-hikari-cats" % doobieVersion, 17 | "org.tpolecat" %% "doobie-postgres-cats" % doobieVersion, 18 | "org.tpolecat" %% "doobie-scalatest-cats" % doobieVersion, 19 | "io.circe" %% "circe-core" % circeVersion, 20 | "io.circe" %% "circe-generic" % circeVersion, 21 | "io.circe" %% "circe-parser" % circeVersion, 22 | "de.heikoseeberger" %% "akka-http-circe" % "1.17.0", 23 | "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, 24 | "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % "test" 25 | ) 26 | 27 | scalacOptions ++= Seq( 28 | "-encoding", "UTF-8", 29 | "-feature", 30 | "-deprecation", 31 | "-language:existentials", 32 | "-language:higherKinds", 33 | "-language:implicitConversions", 34 | "-language:experimental.macros", 35 | "-unchecked", 36 | "-Xlint", 37 | "-Yno-adapted-args", 38 | "-Ywarn-dead-code", 39 | "-Ywarn-value-discard" 40 | ) -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.15 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.8.0") -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | postgres{ 2 | driverClassName = "org.postgresql.Driver" 3 | connectionString = "jdbc:postgresql:postgres" 4 | userName = "postgres" 5 | password = "" 6 | } 7 | 8 | h2 { 9 | # Pending implementation 10 | } 11 | 12 | activeDatabase = "postgres" 13 | 14 | akka.http{ 15 | 16 | host = "0.0.0.0" 17 | port = 7101 18 | 19 | server { 20 | server-header = "Reader REST API" 21 | request-timeout = 120s 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/algebra/ArtistService.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.algebra 2 | 3 | import cats.data.Reader 4 | import io.github.alejandrome.repository.ArtistRepository 5 | 6 | trait ArtistService[T] { 7 | 8 | def listArtists(): Reader[ArtistRepository, List[T]] 9 | def retrieveArtistById(id: Int): Reader[ArtistRepository, Option[T]] 10 | def retrieveArtistByName(name: String): Reader[ArtistRepository, Option[T]] 11 | def insertArtist(artist: T): Reader[ArtistRepository, Int] 12 | def updateArtist(artist: T): Reader[ArtistRepository, Int] 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/algebra/interpreter/ArtistServiceInterpreter.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.algebra.interpreter 2 | 3 | import cats.data.Reader 4 | import io.github.alejandrome.algebra.ArtistService 5 | import io.github.alejandrome.model.Artist 6 | import io.github.alejandrome.repository.ArtistRepository 7 | 8 | class ArtistServiceInterpreter extends ArtistService[Artist]{ 9 | 10 | override def listArtists(): Reader[ArtistRepository, List[Artist]] = Reader { 11 | repo: ArtistRepository => 12 | repo.query() 13 | } 14 | 15 | override def retrieveArtistById(id: Int): Reader[ArtistRepository, Option[Artist]] = Reader { 16 | repo: ArtistRepository => 17 | repo.query(id) 18 | } 19 | 20 | override def retrieveArtistByName(name: String): Reader[ArtistRepository, Option[Artist]] = Reader { 21 | repo: ArtistRepository => 22 | repo.query(name) 23 | } 24 | 25 | override def insertArtist(artist: Artist): Reader[ArtistRepository, Int] = Reader { 26 | repo: ArtistRepository => 27 | repo.insert(artist) 28 | } 29 | 30 | override def updateArtist(artist: Artist): Reader[ArtistRepository, Int] = Reader { 31 | repo: ArtistRepository => 32 | repo.update(artist) 33 | } 34 | 35 | } 36 | 37 | object ArtistServiceInterpreter extends ArtistServiceInterpreter 38 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/api/Api.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.api 2 | 3 | import akka.http.scaladsl.model.StatusCodes 4 | import akka.http.scaladsl.server.Route 5 | import io.github.alejandrome.api.exception.ExceptionHandlerApi 6 | import io.github.alejandrome.api.rejection.RejectionHandlerApi 7 | import io.github.alejandrome.api.helpers.Marshalling 8 | import io.github.alejandrome.repository.ArtistRepository 9 | import io.circe.syntax._ 10 | import io.github.alejandrome.model.Artist 11 | import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ 12 | 13 | trait Api extends RejectionHandlerApi with ExceptionHandlerApi with Marshalling{ 14 | 15 | import io.github.alejandrome.algebra.interpreter._ 16 | 17 | def api(repository: ArtistRepository): Route = 18 | handleExceptions(exceptionHandlerApi){ 19 | handleRejections(genericRejectionHandler){ 20 | pathPrefix("api") { 21 | pathPrefix("artists") { 22 | parameter("id".as[Int]){ id => 23 | (pathEndOrSingleSlash & get){ 24 | createHttpResponse( 25 | statusCode = StatusCodes.OK, 26 | entity = ArtistServiceInterpreter.retrieveArtistById(id).run(repository).asJson 27 | ) 28 | } 29 | } ~ 30 | parameter("name"){ artistName => 31 | (pathEndOrSingleSlash & get){ 32 | createHttpResponse( 33 | statusCode = StatusCodes.OK, 34 | entity = ArtistServiceInterpreter.retrieveArtistByName(artistName).run(repository).asJson 35 | ) 36 | } 37 | } ~ (pathEndOrSingleSlash & get) { 38 | createHttpResponse( 39 | statusCode = StatusCodes.OK, 40 | entity = ArtistServiceInterpreter.listArtists().run(repository).asJson 41 | ) 42 | } ~ (pathEndOrSingleSlash & post) { 43 | entity(as[Artist]){ artist => 44 | createHttpResponse( 45 | statusCode = StatusCodes.OK, 46 | entity = ArtistServiceInterpreter.insertArtist(artist).run(repository).asJson 47 | ) 48 | } 49 | } ~ (pathEndOrSingleSlash & put) { 50 | entity(as[Artist]){ artist => 51 | createHttpResponse( 52 | statusCode = StatusCodes.OK, 53 | entity = ArtistServiceInterpreter.updateArtist(artist).run(repository).asJson 54 | ) 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/api/exception/ExceptionHandlerApi.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.api.exception 2 | 3 | import akka.http.scaladsl.model.StatusCodes 4 | import akka.http.scaladsl.server.ExceptionHandler 5 | import io.github.alejandrome.model.ApiError 6 | import io.circe.generic.auto._ 7 | import io.circe.syntax._ 8 | import io.github.alejandrome.api.helpers.Response 9 | 10 | trait ExceptionHandlerApi extends Response{ 11 | 12 | val exceptionHandlerApi = ExceptionHandler{ 13 | case e: Exception => 14 | 15 | createHttpResponse( 16 | statusCode = StatusCodes.InternalServerError, 17 | entity = ApiError( 18 | message = "An error has ocurred while processing your request. Please see technicalMessage for more details.", 19 | technicalMessage = e.getMessage 20 | ).asJson 21 | ) 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/api/helpers/Marshalling.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.api.helpers 2 | 3 | import io.circe.{Decoder, Encoder} 4 | import io.circe.generic.semiauto._ 5 | import io.github.alejandrome.model.{Album, Artist, Band, Genre} 6 | 7 | trait Marshalling { 8 | 9 | implicit val artistEncoder: Encoder[Artist] = deriveEncoder[Artist] 10 | implicit val artistDecoder: Decoder[Artist] = deriveDecoder[Artist] 11 | 12 | implicit val bandEncoder: Encoder[Band] = deriveEncoder[Band] 13 | implicit val bandDecoder: Decoder[Band] = deriveDecoder[Band] 14 | 15 | implicit val genreEncoder: Encoder[Genre] = deriveEncoder[Genre] 16 | implicit val genreDecoder: Decoder[Genre] = deriveDecoder[Genre] 17 | 18 | implicit val albumEncoder: Encoder[Album] = deriveEncoder[Album] 19 | implicit val albumDecoder: Decoder[Album] = deriveDecoder[Album] 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/api/helpers/Response.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.api.helpers 2 | 3 | import akka.http.scaladsl.model.ContentTypes.`application/json` 4 | import akka.http.scaladsl.model.{HttpEntity, HttpResponse, StatusCode} 5 | import akka.http.scaladsl.server.{Directives, StandardRoute} 6 | import io.circe.Json 7 | 8 | trait Response extends Directives{ 9 | 10 | def createHttpResponse(statusCode: StatusCode, entity: Json): StandardRoute = 11 | complete( 12 | HttpResponse(status = statusCode, entity = HttpEntity(`application/json`, entity.toString())) 13 | ) 14 | 15 | def createHttpResponse(statusCode: StatusCode): StandardRoute = 16 | complete( 17 | HttpResponse(status = statusCode) 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/api/rejection/RejectionHandlerApi.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.api.rejection 2 | 3 | import akka.http.scaladsl.model.StatusCodes.{BadRequest, MethodNotAllowed, NotFound} 4 | import akka.http.scaladsl.server.Directives.complete 5 | import akka.http.scaladsl.server._ 6 | 7 | trait RejectionHandlerApi { 8 | 9 | val genericRejectionHandler: RejectionHandler = 10 | RejectionHandler.newBuilder() 11 | .handle { 12 | case ValidationRejection(msg, _) => 13 | complete((BadRequest, msg)) 14 | } 15 | .handle { 16 | case MissingQueryParamRejection(param) => 17 | complete((BadRequest, "One or more query params are required.")) 18 | } 19 | .handle { 20 | case MalformedQueryParamRejection(_, _, _) => 21 | complete((BadRequest, "One of the supplied query params are malformed or invalid")) 22 | } 23 | .handle { 24 | case MalformedRequestContentRejection(message, cause) => 25 | complete((BadRequest, "The request sent is malformed.")) 26 | } 27 | .handleAll[MethodRejection] { methodRejections => 28 | complete((MethodNotAllowed, s"Method not allowed")) 29 | } 30 | .handleNotFound { 31 | complete((NotFound, "The resource could not be found")) 32 | } 33 | .result() 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/boot/Boot.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.boot 2 | 3 | import akka.actor.ActorSystem 4 | import akka.event.Logging 5 | import akka.http.scaladsl.Http 6 | import akka.stream.ActorMaterializer 7 | import io.github.alejandrome.api.Api 8 | import io.github.alejandrome.config.ApplicationConfig 9 | import io.github.alejandrome.repository.ArtistRepository 10 | import io.github.alejandrome.repository.impl.{ArtistH2Repository, ArtistPGRepository} 11 | import org.slf4j.LoggerFactory 12 | 13 | object Boot extends App with Api{ 14 | 15 | implicit val system = ActorSystem("readerm") 16 | implicit val materializer = ActorMaterializer() 17 | implicit val executionContext = system.dispatcher 18 | implicit val logger = LoggerFactory.getLogger(this.getClass) 19 | 20 | val apiHost = system.settings.config.getString("akka.http.host") 21 | val apiPort = system.settings.config.getInt("akka.http.port") 22 | 23 | val akkaHttpLogger = Logging(system.eventStream, "readerm") 24 | 25 | val repo: ArtistRepository = ApplicationConfig.activeDatabase match { 26 | case "postgres" => new ArtistPGRepository 27 | case "h2" => new ArtistH2Repository 28 | } 29 | 30 | Http().bindAndHandle(handler = api(repo), interface = apiHost, port = apiPort) map { 31 | binding => 32 | akkaHttpLogger.info(s"Readerm API Bound to address ${binding.localAddress}") 33 | } recover{ 34 | case ex => 35 | akkaHttpLogger.error(ex, "Failed to bind Readerm API to {}:{}", apiHost, apiPort) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/config/ApplicationConfig.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.config 2 | 3 | import com.typesafe.config.{Config, ConfigFactory} 4 | 5 | object ApplicationConfig { 6 | 7 | val config: Config = ConfigFactory.load() 8 | 9 | val activeDatabase: String = config.getString("activeDatabase") 10 | 11 | val driverClassName: String = config.getString("postgres.driverClassName") 12 | val connectionString: String = config.getString("postgres.connectionString") 13 | val userName: String = config.getString("postgres.userName") 14 | val password: String = config.getString("postgres.password") 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/model/ApiError.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.model 2 | 3 | case class ApiError(message: String, technicalMessage: String) -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/model/Artist.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.model 2 | 3 | case class Artist(artistID: Int, stageName: String, age: Int) 4 | 5 | case class Band(bandID: Int, bandName: String, bandMembers: Option[List[Artist]]) 6 | 7 | case class Album(albumID: Int, albumName: String, performer: Band, genre: Genre, releaseYear: Int, duration: Int) -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/model/Genre.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.model 2 | 3 | case class Genre(genreID: Int, genreName: String) -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/repository/ArtistRepository.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.repository 2 | 3 | import io.github.alejandrome.model.Artist 4 | 5 | trait ArtistRepository extends Repository[Artist, Int]{ 6 | 7 | def query(): List[Artist] 8 | def query(id: Int): Option[Artist] 9 | def query(name: String): Option[Artist] 10 | def insert(obj: Artist): Int 11 | def update(obj: Artist): Int 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/repository/Repository.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.repository 2 | 3 | trait Repository[A, Id] { 4 | 5 | def query(id: Id): Option[A] 6 | def query(): List[A] 7 | def insert(obj: A): Int 8 | def update(obj: A): Int 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/repository/impl/ArtistH2Repository.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.repository.impl 2 | 3 | import io.github.alejandrome.model.Artist 4 | import io.github.alejandrome.repository.ArtistRepository 5 | 6 | class ArtistH2Repository extends ArtistRepository{ 7 | 8 | // Pending implementation 9 | 10 | override def insert(obj: Artist): Int = ??? 11 | 12 | override def query(): List[Artist] = ??? 13 | 14 | override def query(id: Int): Option[Artist] = ??? 15 | 16 | override def query(name: String): Option[Artist] = ??? 17 | 18 | override def update(obj: Artist): Int = ??? 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/io/github/alejandrome/repository/impl/ArtistPGRepository.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.repository.impl 2 | 3 | import io.github.alejandrome.model.Artist 4 | import io.github.alejandrome.repository.ArtistRepository 5 | import doobie.imports._ 6 | import doobie.hikari.imports._ 7 | 8 | class ArtistPGRepository extends ArtistRepository{ 9 | 10 | import io.github.alejandrome.config.ApplicationConfig._ 11 | 12 | def getTransactor[T](q: ConnectionIO[T]): IOLite[T] = { 13 | for { 14 | connection <- HikariTransactor[IOLite](driverClassName, connectionString, userName, password) 15 | _ <- connection.configure(hx => IOLite.primitive(hx.setAutoCommit(true))) 16 | databaseAccess <- q.transact(connection) ensuring connection.shutdown 17 | } yield databaseAccess 18 | } 19 | 20 | override def query(): List[Artist] = { 21 | val q = sql"SELECT * FROM ARTISTS".query[Artist].process.list 22 | val tx: IOLite[List[Artist]] = getTransactor(q) 23 | tx.unsafePerformIO 24 | } 25 | 26 | override def query(id: Int): Option[Artist] = { 27 | val q = sql"SELECT * FROM ARTISTS WHERE ARTISTID = $id".query[Artist].option 28 | val tx: IOLite[Option[Artist]] = getTransactor(q) 29 | tx.unsafePerformIO 30 | } 31 | 32 | override def query(name: String): Option[Artist] = { 33 | val q = sql"SELECT * FROM ARTISTS WHERE STAGENAME = ${name.toUpperCase()}".query[Artist].option 34 | val tx: IOLite[Option[Artist]] = getTransactor(q) 35 | tx.unsafePerformIO 36 | } 37 | 38 | override def insert(obj: Artist): Int = { 39 | val q = sql"INSERT INTO ARTISTS(STAGENAME, AGE) VALUES(${obj.stageName}, ${obj.age})".update.run 40 | val tx: IOLite[Int] = getTransactor(q) 41 | tx.unsafePerformIO 42 | } 43 | 44 | override def update(obj: Artist): Int = { 45 | val q = sql"UPDATE ARTISTS SET STAGENAME = ${obj.stageName}, AGE = ${obj.age} WHERE ARTISTID = ${obj.artistID}".update.run 46 | val tx: IOLite[Int] = getTransactor(q) 47 | tx.unsafePerformIO 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/test/scala/io/github/alejandrome/repository/impl/ArtistPGRepositoryTest.scala: -------------------------------------------------------------------------------- 1 | package io.github.alejandrome.repository.impl 2 | 3 | import io.github.alejandrome.algebra.interpreter.ArtistServiceInterpreter 4 | import io.github.alejandrome.model.Artist 5 | import org.scalatest.{FlatSpec, Matchers} 6 | 7 | class ArtistPGRepositoryTest extends FlatSpec with Matchers{ 8 | 9 | val repo = new ArtistPGRepository() 10 | 11 | "PG Repository" should "list all the artists" in { 12 | 13 | // If you pass the type parameter the compiler will discard it because of erasure. 14 | ArtistServiceInterpreter.listArtists().run(repo) shouldBe a [List[_]] 15 | } 16 | 17 | it should "list an artist by its ID" in { 18 | ArtistServiceInterpreter.retrieveArtistById(1).run(repo) shouldBe an [Option[Artist]] 19 | } 20 | 21 | it should "return None in case of a non-existent artist ID" in { 22 | ArtistServiceInterpreter.retrieveArtistById(-3).run(repo) shouldBe None 23 | } 24 | 25 | it should "find 'DAVID BOWIE' in artists" in { 26 | ArtistServiceInterpreter.retrieveArtistByName("DAVID BOWIE").run(repo) shouldBe an [Option[Artist]] 27 | } 28 | 29 | it should "not find 'BILLY CORGAN' in artists" in { 30 | ArtistServiceInterpreter.retrieveArtistByName("BILLY CORGAN").run(repo) shouldBe None 31 | } 32 | 33 | it should "insert artist 'JOSEPH MOUNT' in artists table" in { 34 | val artist = Artist(0, "JOSEPH MOUNT", 35) 35 | ArtistServiceInterpreter.insertArtist(artist).run(repo) shouldBe 1 36 | } 37 | 38 | it should "update artist 'KIM GORDON' age" in { 39 | val artist = Artist(1, "KIM GORDON", 22) 40 | ArtistServiceInterpreter.updateArtist(artist).run(repo) shouldBe 1 41 | } 42 | 43 | } 44 | --------------------------------------------------------------------------------