Skip to content

Commit

Permalink
And some documentation in Readme.
Browse files Browse the repository at this point in the history
Update to http4s 1.0.0-M3.
Update to Scala 2.13.3.
Update to scalatest 3.2.0
Remove http4s-dsl dependency.
Minor fixes.
  • Loading branch information
Lasering committed Jul 30, 2020
1 parent e0a1f41 commit 5a3c3bf
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 21 deletions.
110 changes: 110 additions & 0 deletions README.md
Expand Up @@ -17,5 +17,115 @@ libraryDependencies += "org.fenixedu" %% "fenixedu-scala-sdk" % "0.1.0"
```
We use [semantic versioning](http://semver.org).

## Usage
### Creating the HTTP Client
In a **production** environment we recommend using [IOApp](https://typelevel.org/cats-effect/datatypes/ioapp.html)
(or [TaskApp](https://monix.io/api/3.0/monix/eval/TaskApp.html), [zio.App](https://zio.dev/docs/getting_started.html#main)):

```scala
import scala.concurrent.ExecutionContext.Implicits.global
import cats.effect._
import org.http4s.client.blaze.BlazeClientBuilder
import org.http4s.syntax.all._
import org.fenixedu.sdk.FenixEduClient

object Example extends IOApp {
override def run(args: List[String]): IO[ExitCode] = {
BlazeClientBuilder[IO](global).resource.use { implicit httpClient =>
val client: FenixEduClient[IO] = new FenixEduClient[IO](uri"https://fenix.tecnico.ulisboa.pt/api/fenix/")
// your code, which eventually must return ExitCode.Success inside an IO
}
}
}
```

In a **testing** environment we recommend:

```scala
import scala.concurrent.ExecutionContext
import java.util.concurrent.Executors
import cats.effect.{Blocker, ContextShift, IO}
import org.http4s.client.{Client, JavaNetClientBuilder}
import org.http4s.syntax.all._
import org.fenixedu.sdk.FenixEduClient
import org.fenixedu.sdk.services._

val ec = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(5))
implicit val cs: ContextShift[IO] = IO.contextShift(ec)
implicit val httpClient: Client[IO] = JavaNetClientBuilder[IO](Blocker.liftExecutionContext(ec)).create
val client: FenixEduClient[IO] = new FenixEduClient[IO](uri"https://fenix.tecnico.ulisboa.pt/api/fenix/")
```

[Try it in Scastie](https://scastie.scala-lang.org/cDgS42JbTsGppvKZlbf4mQ)

### Examples
#### Degrees
```scala
// List degrees for the current academic term
client.degrees.list() // returns IO[List[Degree]]
// List degrees for the academic term 2021/2022
client.degrees.list(Some("2021/2022"))

// Get a specific degree
client.degrees.get("2761663971474") // returns IO[Degree]
```

#### Courses
```scala
// List the courses of a degree
val degreeId = "2761663971474" // LEIC-A 2019/2020
client.degrees.courses(degreeId) // returns IO[List[CourseRef]]
// CourseRef is a reference to a course, it does not contain all the information about a Course, but
// contains the course id, acronym, name, and academicTerm. If a FenixEduClient is in the implicit scope
// the course can be easily fetched by invoking `course` of the CourseRef.

val courseId = "564560566180817" // Análise e Síntese de Algoritmos 2º Semestre 2019/2020
val course: Course[IO] = client.course(courseId)

// Get the course
course.get() // returns IO[Course]

// Get the course evaluations such as exams, tests, projects, etc
course.evaluations // returns IO[List[Evaluation]]

// Get the course groups such teams of students for laboratories or projects
course.groups // returns IO[List[Group]]

// Get the course schedule such as the lesson periods or laboratories shifts.
course.schedule // returns IO[Schedule]

// Lists all the students attending the specified course
course.students // returns IO[AttendingStudents]
```

#### Parking
```scala
// the number of free parking slots for each parking location.
client.parking // returns IO[Map[String, Parking]]
```

#### Shuttle
```scala
// the shuttle information, including stops and timetables.
client.shuttle // returns IO[Shuttle]
```

#### Spaces
```scala
// List spaces
client.spaces.list() // returns IO[List[SpaceRef]]
// Just like CourseRef, SpaceRef is a reference to a Space, it does not contain all the information about a Space, but
// contains the space type, id, acronym, and name. If a FenixEduClient is in the implicit scope
// the Space can be easily fetched by invoking `space` of the SpaceRef.

// Get a space
val alameda = "2448131360897"
client.spaces.get(alameda) // returns IO[Space]

// Get the blueprint of a space
// 2448131361063 = Pavilhão Central Floor 0
client.spaces.blueprint("2448131361063") // returns Stream[IO, Byte]
```

## License
This library is open source and available under the [MIT license](LICENSE).
22 changes: 18 additions & 4 deletions build.sbt
Expand Up @@ -5,7 +5,7 @@ name := "fenixedu-scala-sdk"
// ==== Compile Options =================================================================================================
// ======================================================================================================================
javacOptions ++= Seq("-Xlint", "-encoding", "UTF-8", "-Dfile.encoding=utf-8")
scalaVersion := "2.13.2"
scalaVersion := "2.13.3"

scalacOptions ++= Seq(
"-encoding", "utf-8", // Specify character encoding used by source files.
Expand Down Expand Up @@ -36,17 +36,31 @@ scalacOptions in (Compile, console) ~= (_.filterNot { option =>
})
scalacOptions in (Test, console) := (scalacOptions in (Compile, console)).value

initialCommands in console :=
"""import scala.concurrent.ExecutionContext
|import java.util.concurrent.Executors
|import cats.effect.{Blocker, ContextShift, IO}
|import org.http4s.client.{Client, JavaNetClientBuilder}
|import org.http4s.syntax.all._
|import org.fenixedu.sdk.FenixEduClient
|import org.fenixedu.sdk.services._
|
|val ec = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(5))
|implicit val cs: ContextShift[IO] = IO.contextShift(ec)
|implicit val httpClient: Client[IO] = JavaNetClientBuilder[IO](Blocker.liftExecutionContext(ec)).create
|val client: FenixEduClient[IO] = new FenixEduClient[IO](uri"https://fenix.tecnico.ulisboa.pt/api/fenix/")""".stripMargin

// ======================================================================================================================
// ==== Dependencies ====================================================================================================
// ======================================================================================================================
libraryDependencies ++= Seq("blaze-client", "dsl", "circe").map { module =>
"org.http4s" %% s"http4s-$module" % "0.21.4"
libraryDependencies ++= Seq("blaze-client", "circe").map { module =>
"org.http4s" %% s"http4s-$module" % "1.0.0-M3"
} ++ Seq(
"io.circe" %% "circe-derivation" % "0.13.0-M4",
"io.circe" %% "circe-parser" % "0.13.0",
"com.beachape" %% "enumeratum-circe" % "1.6.1",
"ch.qos.logback" % "logback-classic" % "1.2.3" % Test,
"org.scalatest" %% "scalatest" % "3.1.2" % Test,
"org.scalatest" %% "scalatest" % "3.2.0" % Test,
)
addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1")

Expand Down
4 changes: 1 addition & 3 deletions src/main/scala/org/fenixedu/sdk/models/Course.scala
@@ -1,7 +1,5 @@
package org.fenixedu.sdk.models

import cats.effect.Sync
import io.circe.Decoder.Result
import io.circe.{Decoder, HCursor}
import io.circe.derivation.deriveDecoder
import org.fenixedu.sdk.FenixEduClient
Expand Down Expand Up @@ -42,5 +40,5 @@ object CourseRef {
implicit val decoder: Decoder[CourseRef] = deriveDecoder(identity)
}
case class CourseRef(id: String, acronym: String, name: String, academicTerm: String, url: Option[Uri]) {
def course[F[_]: Sync](implicit client: FenixEduClient[F]): F[Course] = client.course(id).get()
def course[F[_]](implicit client: FenixEduClient[F]): F[Course] = client.course(id).get()
}
3 changes: 1 addition & 2 deletions src/main/scala/org/fenixedu/sdk/models/Degree.scala
@@ -1,6 +1,5 @@
package org.fenixedu.sdk.models

import cats.effect.Sync
import io.circe.Decoder
import io.circe.derivation.deriveDecoder
import org.fenixedu.sdk.FenixEduClient
Expand Down Expand Up @@ -43,5 +42,5 @@ object DegreeRef {
implicit val decoder: Decoder[DegreeRef] = deriveDecoder(identity)
}
case class DegreeRef(id: String, name: String, acronym: String) {
def degree[F[_]: Sync](implicit client: FenixEduClient[F]): F[Degree] = client.degrees.get(id)
def degree[F[_]](implicit client: FenixEduClient[F]): F[Degree] = client.degrees.get(id)
}
3 changes: 1 addition & 2 deletions src/main/scala/org/fenixedu/sdk/models/Space.scala
@@ -1,6 +1,5 @@
package org.fenixedu.sdk.models

import cats.effect.Sync
import io.circe.derivation.deriveDecoder
import io.circe.{Decoder, DecodingFailure, HCursor, Json}
import org.fenixedu.sdk.FenixEduClient
Expand All @@ -13,7 +12,7 @@ object SpaceRef {
}))
}
case class SpaceRef(`type`: String, id: String, name: String) {
def space[F[_]: Sync](implicit client: FenixEduClient[F]): F[Space] = client.spaces.get(id)
def space[F[_]](implicit client: FenixEduClient[F]): F[Space] = client.spaces.get(id)
}

object Capacity {
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/org/fenixedu/sdk/services/BaseService.scala
Expand Up @@ -5,14 +5,14 @@ import cats.syntax.flatMap._
import cats.syntax.functor._
import fs2.{Chunk, Stream}
import io.circe.{Decoder, HCursor}
import org.http4s.Method.GET
import org.http4s.circe.decodeUri
import org.http4s.client.Client
import org.http4s.client.dsl.Http4sClientDsl
import org.http4s.dsl.impl.Methods
import org.http4s.{Query, Request, Uri}

abstract class BaseService[F[_], T: Decoder](baseUri: Uri, val name: String)(implicit client: Client[F], F: Sync[F]) {
protected val dsl = new Http4sClientDsl[F] with Methods
protected val dsl = new Http4sClientDsl[F] {}
import dsl._

val pluralName = s"${name}s"
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/org/fenixedu/sdk/services/Degrees.scala
Expand Up @@ -9,11 +9,11 @@ final class Degrees[F[_]: Sync](baseUri: Uri)(implicit client: Client[F]) {
val uri: Uri = baseUri / "degrees"

/** @return the information for all degrees. If no academicTerm is defined it returns the degree information for the currentAcademicTerm. */
def list(academicTerm: Option[String] = None): F[List[Degree]] = client.expect(uri +??("academicTerm", academicTerm))
def list(academicTerm: Option[String] = None): F[List[Degree]] = client.expect(uri.+??("academicTerm", academicTerm))

/** @return the information for the `id` degree. If no academicTerm is defined it returns the degree information for the currentAcademicTerm. */
def get(id: String, academicTerm: Option[String] = None): F[Degree] = client.expect(uri / id +??("academicTerm", academicTerm))
def get(id: String, academicTerm: Option[String] = None): F[Degree] = client.expect((uri / id).+??("academicTerm", academicTerm))

/** @return the information for a degree’s courses. If no academicTerm is defined it returns the degree information for the currentAcademicTerm. */
def courses(id: String, academicTerm: Option[String] = None): F[List[CourseRef]] = client.expect(uri / id / "courses" +??("academicTerm", academicTerm))
def courses(id: String, academicTerm: Option[String] = None): F[List[CourseRef]] = client.expect((uri / id / "courses").+??("academicTerm", academicTerm))
}
8 changes: 4 additions & 4 deletions src/main/scala/org/fenixedu/sdk/services/Spaces.scala
Expand Up @@ -5,15 +5,15 @@ import java.time.format.DateTimeFormatter
import cats.effect.Sync
import fs2.Stream
import org.fenixedu.sdk.models.{Space, SpaceRef}
import org.http4s.Method.GET
import org.http4s.Uri
import org.http4s.client.Client
import org.http4s.client.dsl.Http4sClientDsl
import org.http4s.dsl.impl.Methods

final class Spaces[F[_]: Sync](baseUri: Uri)(implicit client: Client[F]) {
val uri: Uri = baseUri / "spaces"

protected val dsl = new Http4sClientDsl[F] with Methods
protected val dsl = new Http4sClientDsl[F] {}
import dsl._

/** @return returns the information about the campi. */
Expand All @@ -22,10 +22,10 @@ final class Spaces[F[_]: Sync](baseUri: Uri)(implicit client: Client[F]) {
/** @return information about the space for a given `id`. The `id` can be for a Campus, Building, Floor or Room. */
def get(id: String, day: Option[LocalDate] = None): F[Space] = {
val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
client.expect(uri / id +??("day", day.map(d => dateTimeFormatter.format(d))))
client.expect((uri / id).+??("day", day.map(d => dateTimeFormatter.format(d))))
}

/** @return the space’s blueprint in the required format.*/
def blueprint(id: String, format: Option[String] = Some("jpeg")): Stream[F, Byte] =
Stream.eval(GET(uri / id / "blueprint" +??("format", format))).flatMap(client.stream).flatMap(_.body)
Stream.eval(GET((uri / id / "blueprint").+??("format", format))).flatMap(client.stream).flatMap(_.body)
}
1 change: 0 additions & 1 deletion src/test/scala/org/fenixedu/sdk/CoursesSpec.scala
Expand Up @@ -2,7 +2,6 @@ package org.fenixedu.sdk

import cats.instances.list._
import cats.syntax.parallel._
import cats.syntax.applicativeError._
import org.http4s.Status.InternalServerError
import org.http4s.client.UnexpectedStatus

Expand Down

0 comments on commit 5a3c3bf

Please sign in to comment.