diff --git a/src/Exchange/ExchangeRequest.php b/src/Exchange/ExchangeRequest.php new file mode 100644 index 00000000..cd1b6797 --- /dev/null +++ b/src/Exchange/ExchangeRequest.php @@ -0,0 +1,92 @@ + + * @license http://opensource.org/licenses/MIT MIT + * @link https://github.com/apiki/back-end-challenge + */ + +declare(strict_types=1); + +namespace App\Exchange; + +/** + * Exchange request value object. + * + * @category Challenge + * @package Back-end + * @author Eduardo Lopes + * @license http://opensource.org/licenses/MIT MIT + * @link https://github.com/apiki/back-end-challenge + */ +class ExchangeRequest +{ + private float $_amount; + + private string $_from; + + private string $_to; + + private float $_rate; + + /** + * Create a new exchange request. + * + * @param float $amount Exchange amount. + * @param string $from Source currency. + * @param string $to Target currency. + * @param float $rate Exchange rate. + */ + public function __construct(float $amount, string $from, string $to, float $rate) + { + $this->_amount = $amount; + $this->_from = $from; + $this->_to = $to; + $this->_rate = $rate; + } + + /** + * Return the exchange amount. + * + * @return float + */ + public function getAmount(): float + { + return $this->_amount; + } + + /** + * Return the source currency. + * + * @return string + */ + public function getFrom(): string + { + return $this->_from; + } + + /** + * Return the target currency. + * + * @return string + */ + public function getTo(): string + { + return $this->_to; + } + + /** + * Return the exchange rate. + * + * @return float + */ + public function getRate(): float + { + return $this->_rate; + } +} diff --git a/src/Exchange/ExchangeRequestFactory.php b/src/Exchange/ExchangeRequestFactory.php new file mode 100644 index 00000000..dc5c830c --- /dev/null +++ b/src/Exchange/ExchangeRequestFactory.php @@ -0,0 +1,102 @@ + + * @license http://opensource.org/licenses/MIT MIT + * @link https://github.com/apiki/back-end-challenge + */ + +declare(strict_types=1); + +namespace App\Exchange; + +use App\Http\BadRequestException; + +/** + * Build exchange requests from URIs. + * + * @category Challenge + * @package Back-end + * @author Eduardo Lopes + * @license http://opensource.org/licenses/MIT MIT + * @link https://github.com/apiki/back-end-challenge + */ +class ExchangeRequestFactory +{ + /** + * Supported currencies. + * + * @var string[] + */ + private array $_supportedCurrencies = ['BRL', 'USD', 'EUR']; + + /** + * Build a request from a URI. + * + * @param string $uri Request URI. + * + * @return ExchangeRequest + * @throws BadRequestException + */ + public function fromUri(string $uri): ExchangeRequest + { + $path = parse_url($uri, PHP_URL_PATH); + $pattern = '#^/exchange/' + . '([^/]+)/' + . '([^/]+)/' + . '([^/]+)/' + . '([^/]+)$#'; + + if (!is_string($path)) { + throw new BadRequestException(); + } + + if (preg_match($pattern, $path, $segments) !== 1) { + throw new BadRequestException(); + } + + [, $amount, $from, $to, $rate] = $segments; + + if (!$this->_isValidNumber($amount) || !$this->_isValidNumber($rate)) { + throw new BadRequestException(); + } + + if (!$this->_isValidCurrency($from) || !$this->_isValidCurrency($to)) { + throw new BadRequestException(); + } + + return new ExchangeRequest((float) $amount, $from, $to, (float) $rate); + } + + /** + * Validate a numeric input. + * + * @param string $value Numeric string. + * + * @return bool + */ + private function _isValidNumber(string $value): bool + { + $number = (float) $value; + + return is_numeric($value) && is_finite($number) && $number >= 0; + } + + /** + * Validate a supported currency. + * + * @param string $currency Currency code. + * + * @return bool + */ + private function _isValidCurrency(string $currency): bool + { + return preg_match('/^[A-Z]{3}$/', $currency) === 1 + && in_array($currency, $this->_supportedCurrencies, true); + } +} diff --git a/src/Exchange/ExchangeService.php b/src/Exchange/ExchangeService.php new file mode 100644 index 00000000..077a4333 --- /dev/null +++ b/src/Exchange/ExchangeService.php @@ -0,0 +1,64 @@ + + * @license http://opensource.org/licenses/MIT MIT + * @link https://github.com/apiki/back-end-challenge + */ + +declare(strict_types=1); + +namespace App\Exchange; + +use App\Http\BadRequestException; + +/** + * Convert exchange requests. + * + * @category Challenge + * @package Back-end + * @author Eduardo Lopes + * @license http://opensource.org/licenses/MIT MIT + * @link https://github.com/apiki/back-end-challenge + */ +class ExchangeService +{ + /** + * Supported exchange pairs. + * + * @var array + */ + private array $_supportedPairs = [ + 'BRL:USD' => '$', + 'USD:BRL' => 'R$', + 'BRL:EUR' => '€', + 'EUR:BRL' => 'R$', + ]; + + /** + * Convert an exchange request. + * + * @param ExchangeRequest $request Exchange request. + * + * @return array + * @throws BadRequestException + */ + public function convert(ExchangeRequest $request): array + { + $pair = $request->getFrom() . ':' . $request->getTo(); + + if (!array_key_exists($pair, $this->_supportedPairs)) { + throw new BadRequestException(); + } + + return [ + 'valorConvertido' => $request->getAmount() * $request->getRate(), + 'simboloMoeda' => $this->_supportedPairs[$pair], + ]; + } +} diff --git a/src/Http/BadRequestException.php b/src/Http/BadRequestException.php new file mode 100644 index 00000000..ccfcfa18 --- /dev/null +++ b/src/Http/BadRequestException.php @@ -0,0 +1,31 @@ + + * @license http://opensource.org/licenses/MIT MIT + * @link https://github.com/apiki/back-end-challenge + */ + +declare(strict_types=1); + +namespace App\Http; + +use RuntimeException; + +/** + * Signal invalid input data. + * + * @category Challenge + * @package Back-end + * @author Eduardo Lopes + * @license http://opensource.org/licenses/MIT MIT + * @link https://github.com/apiki/back-end-challenge + */ +class BadRequestException extends RuntimeException +{ +} diff --git a/src/Http/JsonResponse.php b/src/Http/JsonResponse.php new file mode 100644 index 00000000..905d39bb --- /dev/null +++ b/src/Http/JsonResponse.php @@ -0,0 +1,67 @@ + + * @license http://opensource.org/licenses/MIT MIT + * @link https://github.com/apiki/back-end-challenge + */ + +declare(strict_types=1); + +namespace App\Http; + +/** + * Send JSON responses. + * + * @category Challenge + * @package Back-end + * @author Eduardo Lopes + * @license http://opensource.org/licenses/MIT MIT + * @link https://github.com/apiki/back-end-challenge + */ +class JsonResponse +{ + /** + * Response payload. + * + * @var array + */ + private array $_data; + + /** + * Response status code. + * + * @var int + */ + private int $_statusCode; + + /** + * Create a JSON response. + * + * @param array $data Response payload. + * @param int $statusCode HTTP status code. + */ + public function __construct(array $data, int $statusCode = 200) + { + $this->_data = $data; + $this->_statusCode = $statusCode; + } + + /** + * Send the response. + * + * @return void + */ + public function send(): void + { + http_response_code($this->_statusCode); + header('Content-Type: application/json'); + + echo json_encode($this->_data); + } +} diff --git a/src/index.php b/src/index.php index 92841bc8..795fc563 100644 --- a/src/index.php +++ b/src/index.php @@ -4,15 +4,30 @@ * * PHP version 7.4 * - * Este será o arquivo chamado na execução dos testes automátizados. - * * @category Challenge * @package Back-end - * @author Seu Nome + * @author Eduardo Lopes * @license http://opensource.org/licenses/MIT MIT * @link https://github.com/apiki/back-end-challenge */ + declare(strict_types=1); require __DIR__ . '/../vendor/autoload.php'; +use App\Exchange\ExchangeRequestFactory; +use App\Exchange\ExchangeService; +use App\Http\BadRequestException; +use App\Http\JsonResponse; + +$factory = new ExchangeRequestFactory(); +$service = new ExchangeService(); + +try { + $request = $factory->fromUri($_SERVER['REQUEST_URI'] ?? '/'); + $response = new JsonResponse($service->convert($request)); +} catch (BadRequestException $exception) { + $response = new JsonResponse(['error' => 'Invalid request'], 400); +} + +$response->send();