Conversation
* change http port to 3000
* update executionContext to `ExecutionContext.global`
…e API calls and improve performance * include automatic expiration based on configurable TTL and stale rate detection
…based on exchange rate and provided amount
with descriptive error messages to better handle invalid inputs
| bid: Price, | ||
| ask: Price, |
There was a problem hiding this comment.
added bid and ask fields to better match with response from OneFrame
| ParseFailure( | ||
| s"Invalid currency: '$s'", | ||
| s"Currency must be one of: AUD, CAD, CHF, EUR, GBP, NZD, JPY, SGD, USD" |
There was a problem hiding this comment.
added ParseFailure to query param decoder to check if currency is valid, returns error message otherwise
| // Validate 'from' parameter | ||
| queryParams.get("from") match { | ||
| case Some(values) if values.nonEmpty => | ||
| val fromValue = values.head | ||
| if (fromOpt.isEmpty) { | ||
| errors += s"Invalid 'from' currency: '$fromValue'. Must be one of: AUD, CAD, CHF, EUR, GBP, NZD, JPY, SGD, USD" | ||
| } | ||
| case _ => errors += "Missing required query parameter: 'from'" | ||
| } | ||
|
|
||
| // Validate 'to' parameter | ||
| queryParams.get("to") match { | ||
| case Some(values) if values.nonEmpty => | ||
| val toValue = values.head | ||
| if (toOpt.isEmpty) { | ||
| errors += s"Invalid 'to' currency: '$toValue'. Must be one of: AUD, CAD, CHF, EUR, GBP, NZD, JPY, SGD, USD" | ||
| } | ||
| case _ => errors += "Missing required query parameter: 'to'" | ||
| } | ||
|
|
||
| // If there are validation errors, return BadRequest | ||
| if (errors.nonEmpty) { | ||
| BadRequest(ErrorResponse(errors.mkString("; "))) | ||
| } else { | ||
| // Both parameters are valid, proceed with the request | ||
| (fromOpt, toOpt) match { | ||
| case (Some((from, _)), Some((to, _))) => | ||
| rates | ||
| .get(RatesProgramProtocol.GetRatesRequest(from, to)) | ||
| .flatMap { | ||
| case Right(rate) => Ok(rate.asGetApiResponse) | ||
| case Left(error) => mapProgramError(error) | ||
| } | ||
| case _ => | ||
| BadRequest( | ||
| ErrorResponse( | ||
| "Missing required query parameters. Both 'from' and 'to' parameters are required. " + | ||
| "Example: /rates?from=USD&to=EUR" | ||
| ) |
There was a problem hiding this comment.
validation to ensure only supported currencies are parsed to parameter. Also checks for missing or empty parameters
| implicit val currencyDecoder: Decoder[Currency] = | ||
| Decoder.decodeString.emap { s => |
There was a problem hiding this comment.
added custom Decoder that validates currency strings against supported values in One Frame
| // POST /rates | ||
| case req @ POST -> Root => | ||
| req | ||
| .as[PostApiRequest] | ||
| .flatMap { request => | ||
| rates |
There was a problem hiding this comment.
this is an additional POST API endpoint that accepts user input to set the amount they want to convert; http://localhost:{port}/rates
Sample request Body;
{ "from": "JPY", "to": "USD", "amount": 1 }
Response:
{ "from": "JPY", "to": "USD", "amount": 1, "exchangeRate": 0.15538797046424008, "convertedAmount": 0.15538797046424008, "timestamp": "2026-01-29T15:08:35.341Z" }
| override def get(request: Protocol.GetRatesRequest): F[Error Either Rate] = | ||
| EitherT(ratesService.get(Rate.Pair(request.from, request.to))).leftMap(toProgramError(_)).value | ||
|
|
||
| override def compare(request: Protocol.CompareRatesRequest): F[Error Either Rate] = |
There was a problem hiding this comment.
for comparing rates from 2 different currency pairs using the from and to fields
| class InMemoryCache[F[_]: Sync] private ( | ||
| cache: ConcurrentHashMap[Rate.Pair, CacheEntry] | ||
| ) extends Algebra[F] { | ||
|
|
||
| override def get(pair: Rate.Pair): F[Option[Rate]] = { |
There was a problem hiding this comment.
this is a simple in-memory cache for rates. it stores rates in a concurrent hash map and checks for expiration when retrieving rates using a specified TTL
Overview
This PR implements a robust local proxy for the Forex currency exchange rate service. It addresses the 1,000 requests/day limitation of the One-Frame upstream API to support 10,000+ requests/day while maintaining a 5-minute data freshness guarantee.
Key Requirements Addressed
Technical Implementation
InMemoryCache.scalawith TTL support.OneFrameLive.scalato verify timestamps against the 5-minute window.RateStaleand improved descriptive feedback for upstream failures.application.conf.API Endpoints
http://localhost:3000/rates?from=USD&to=JPY- for getting currency exchange rateshttp://localhost:3000/rates(add-on) - for users to set the amount they want to convertTesting
curl "http://localhost:3000/rates?from=USD&to=JPY".