Browse Source

Begin migration to Rust; Add actix-web, diesel as main dependencies

tags/v0.2.0
Kevin J Hoerr 2 years ago
committed by Kevin Hoerr
parent
commit
0965d62be0
22 changed files with 105 additions and 664 deletions
  1. +2
    -0
      .env.sample
  2. +10
    -0
      .gitignore
  3. +0
    -3
      .htaccess
  4. +15
    -4
      .travis.yml
  5. +18
    -0
      Cargo.toml
  6. +10
    -0
      Dockerfile
  7. +0
    -19
      autoload.php
  8. +5
    -0
      diesel.toml
  9. +0
    -30
      index.php
  10. +0
    -92
      private/Controller/Controller.php
  11. +0
    -42
      private/Model/Connection.php
  12. +0
    -71
      private/Model/Query.php
  13. +0
    -52
      private/Model/Result.php
  14. +0
    -10
      private/Model/creds.php
  15. +0
    -104
      private/View/Main.php
  16. +0
    -28
      private/View/Output.php
  17. +45
    -0
      src/bin/august-offensive.rs
  18. +0
    -0
      src/util/fresh.sql
  19. +0
    -97
      test/Controller/ControllerTest.php
  20. +0
    -42
      test/Model/QueryTest.php
  21. +0
    -37
      test/Model/ResultTest.php
  22. +0
    -33
      test/View/OutputTest.php

+ 2
- 0
.env.sample View File

@@ -0,0 +1,2 @@
DATABASE_URL=postgres://username:password@localhost/august_offensive
BIND_ADDRESS=127.0.0.1:8080

+ 10
- 0
.gitignore View File

@@ -1 +1,11 @@
.idea
.env*
!.env.sample
Cargo.lock

migrations/
target/

*.out
*~
**/*.rs.bk

+ 0
- 3
.htaccess View File

@@ -1,3 +0,0 @@
RewriteEngine On
RewriteRule ^api/(.*)$ index.php/api/$1
RedirectMatch 403 ^/private/.*$

+ 15
- 4
.travis.yml View File

@@ -1,4 +1,15 @@
language: php
php:
- '7.0'
script: phpunit --bootstrap autoload.php test
language: rust
rust:
- stable
- beta
- nightly
cache: cargo

matrix:
allow_failures:
- rust: nightly
fast_finish: true

script:
- cargo build --verbose --all
- cargo test --verbose --all

+ 18
- 0
Cargo.toml View File

@@ -0,0 +1,18 @@
[package]
name = "august-offensive"
version = "0.1.0"
authors = ["Kevin J Hoerr <kjhoerr@submelon.tech>"]
description = "Turn-based strategy game as a web service"
license = "ISC"
readme = "README.md"
repository = "https://gitlab.com/kjhoerr/august-offensive.git"

[dependencies]
dotenv = "0.10"
env_logger = "0.5"
error-chain = "^0.12"

diesel = { version = "1.3.0", features = ["postgres"] }

actix = "0.7"
actix-web = "^0.7"

+ 10
- 0
Dockerfile View File

@@ -0,0 +1,10 @@
FROM rust:latest

WORKDIR /usr/src/august

COPY . .

RUN cargo install
EXPOSE 8080

CMD ["august-offensive"]

+ 0
- 19
autoload.php View File

@@ -1,19 +0,0 @@
<?php

declare(strict_types=1);

// Borrowed and modified from PSR-4 Closure Example
spl_autoload_register(
function ($class) {
$prefix = 'AugustOffensive\\';
$relative_class = substr($class, strlen($prefix));

// find file in /private/ in respective namespace path
$file = __DIR__ . '/private/' . str_replace('\\', '/', $relative_class) . '.php';

// if the file exists, require it
if (file_exists($file)) {
require $file;
}
}
);

+ 5
- 0
diesel.toml View File

@@ -0,0 +1,5 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli

[print_schema]
file = "src/schema.rs"

+ 0
- 30
index.php View File

@@ -1,30 +0,0 @@
<?php

declare(strict_types=1);

namespace AugustOffensive;

require_once "autoload.php";

use AugustOffensive\View;
use AugustOffensive\Controller;

// configure content type before anything is output
header("Content-Type: application/" . View\Main::TYPE);

try {
// initiate connection and build front-end
$connection = Controller\Controller::initiateConnection();
$view = new View\Main($connection);

// get results of query from front-end
$result = $view->generateResult();

echo $result;
} catch (\Exception $err) {
// catch all exceptions and let the controller generate the error
$error = Controller\Controller::errorResult($err);

// pass generated error result to output
echo View\Main::generateOutput($error);
}

+ 0
- 92
private/Controller/Controller.php View File

@@ -1,92 +0,0 @@
<?php

declare(strict_types=1);

namespace AugustOffensive\Controller;

use AugustOffensive\Model;

/**
* Static controller class for interfacing between the view and the model.
*/
class Controller
{
/**
* Initiates connection with the database.
*
* While errors are ideally handled by the controller, this instantiation
* will likely throw a \PDOException which should be handled by the front-
* end due to this being a fatal error and generally unrecoverable.
*
* @return Model\Connection
*/
public static function initiateConnection (): Model\Connection
{
return new Model\Connection();
}

/**
* Creates and returns a Query object.
*
* If the creation results in an error, a different query object is
* returned with the error message.
*
* @param array $path The array that holds the original request structure.
* @param string $request The request method made to the server.
* @param array $content The content object sent by the request.
*
* @return Model\Query
*/
public static function createQuery (
array $path,
string $request,
array $content
): Model\Query {
try {
return new Model\Query($path, $request, $content);
} catch (\Exception $err) {
return new Model\Query(array(), "", array("ERROR" => $err->getMessage()));
}
}

/**
* Creates and returns a Result object.
*
* @param string $resultType The type of result to send back to the client.
* @param array $result The result object to send back to the client.
*
* @return Model\Result
*/
public static function createResult (
string $resultType,
array $result
): Model\Result {
try {
return new Model\Result($resultType, $result);
} catch (\Exception $err) {
return new Model\Result("ERROR", array($err->getMessage()));
}
}

/**
* Obtain the error result based on the exception that was thrown.
*
* @param \Exception $err the error that was thrown.
*
* @return Model\Result
*/
public static function errorResult (\Exception $err): Model\Result
{
$errorType = "";
// Juggle error: objective is to sort error type
try {
throw $err;
} catch (\PDOException $e) {
$errorType = "DATABASE_ERROR";
} catch (\Exception $e) {
$errorType = "ERROR";
}

return new Model\Result($errorType, array("error" => $err->getMessage()));
}
}

+ 0
- 42
private/Model/Connection.php View File

@@ -1,42 +0,0 @@
<?php

declare(strict_types=1);

namespace AugustOffensive\Model;

/**
* Model connection class for connecting to database via PDO.
*/
class Connection
{
/** @var \PDO $_conn PDO connection to database. */
private $conn;
/**
* Initiates connection to PostGreSQL database.
*
* @return Connection
*/
public function __construct ()
{
// Establish connection to db
// breaks side-effect rule
include 'creds.php';

try {
$conn = new \PDO(
"pgsql: host=" . $cred->host .
(($cred->port !== '') ? ";port=" . $cred->port : '') .
";dbname=" . $cred->dbName,
$cred->login,
$cred->password
);
// we destroy $cred as quickly as possible
$cred = null;
} catch (\PDOException $err) {
// we destroy $cred as quickly as possible
$cred = null;
throw $err; // throw for Controller to catch
}
return $this;
}
}

+ 0
- 71
private/Model/Query.php View File

@@ -1,71 +0,0 @@
<?php

declare(strict_types=1);

namespace AugustOffensive\Model;

/**
* Query object for storing relevant query information.
*/
class Query
{
/** @var array $path array of request structure. */
private $path;

/** @var string $request type of request made to the server. */
private $request;

/** @var array $content structure of information sent to the server. */
private $content;

/**
* Store query information.
*
* @param array $path The array that holds the original request structure.
* @param string $request The request method made to the server.
* @param array $content The content object sent by the request.
*
* @return Query
*/
public function __construct (
array $path,
string $request,
array $content
) {
$this->path = $path;
$this->request = $request;
$this->content = $content;

return $this;
}

/**
* Returns the request path made by the client.
*
* @return array
*/
public function getPath (): array
{
return $this->path;
}

/**
* Returns the request type made by the client.
*
* @return string
*/
public function getRequest (): string
{
return $this->request;
}

/**
* Returns the information that is built from outside the request path.
*
* @return array
*/
public function getContent (): array
{
return $this->content;
}
}

+ 0
- 52
private/Model/Result.php View File

@@ -1,52 +0,0 @@
<?php

declare(strict_types=1);

namespace AugustOffensive\Model;
/**
* Result object for storing information to send back to the client.
*/
class Result
{
/** @var string $resultType the type of result to return to the client. */
private $resultType;

/** @var array $result */
private $result;

/**
* Store result information.
*
* @param string $resultType The type of result to send back to the client.
* @param array $result The result object to send back to the client.
*
* @return Result
*/
public function __construct (string $resultType, array $result)
{
$this->resultType = $resultType;
$this->result = $result;

return $this;
}

/**
* Returns the result type of the Result.
*
* @return string
*/
public function getResultType (): string
{
return $this->resultType;
}

/**
* Returns the result array of the Result.
*
* @return array
*/
public function getResult (): array
{
return $this->result;
}
}

+ 0
- 10
private/Model/creds.php View File

@@ -1,10 +0,0 @@
<?php

declare(strict_types=1);

$cred = new \stdClass();
$cred->host = "localhost";
$cred->port = "5432";
$cred->dbName = "";
$cred->login = "";
$cred->password = "";

+ 0
- 104
private/View/Main.php View File

@@ -1,104 +0,0 @@
<?php

declare(strict_types=1);

namespace AugustOffensive\View;

use AugustOffensive\Controller;
use AugustOffensive\Model;

/**
* Outputs the JSON result by communicating with the controller.
*/
class Main
{
/** @var Model\Query $query the Query object that generated the request. */
private $query;

/** @var Model\Result $result the Result object to be sent to the client. */
private $result;

/** @var string TYPE The type of output the client should receive. */
const TYPE = "json";

/**
* Prepares the output and environment for the front end of the service.
*
* @param Model\Connection $connection View "Needs to know" model exists.
*
* @return Result
*/
public function __construct (Model\Connection $connection)
{
$this->query = Controller\Controller::createQuery(
explode('/', trim($_SERVER['PATH_INFO'] ?? '/api', '/')),
$_SERVER['REQUEST_METHOD'],
$this->generateContent($_SERVER['REQUEST_METHOD'])
);
return $this;
}

/**
* Generates the content of the query based on the request type.
*
* @param string $request The request method on which to base the content.
*
* @return array
*/
public function generateContent (string $request): array
{
$content;
switch ($request) {
case "GET": // GET should always be empty
case "POST": // POST contains moves, account info, etc.
default:
$content = $_REQUEST;
}
return $content;
}

/**
* Communicates with the controller to generate the output.
*
* @return string
*/
public function generateResult (): string
{
$path = $this->query->getPath();
if (count($path) >= 1 && $path[0] === "api" && count($path) >= 2) {
switch (strtolower($path[1])) {
case "callback":
$this->result = Controller\Controller::createResult(
"CALLBACK",
array(
"path" => $path,
"request" => $this->query->getRequest(),
"content" => $this->query->getContent()
)
);
break;
default:
$this->result = Controller\Controller::createResult("NOT_UNDERSTOOD", array($path));
break;
}
} else {
$this->result = Controller\Controller::createResult("NOT_UNDERSTOOD", array($path));
}

return self::generateOutput($this->result);
}

/**
* Creates output of the result based on the defined constant TYPE.
*
* @param Model\Result $result The result to be sent to the client.
*
* @return string
*/
public static function generateOutput (Model\Result $result): string
{
$type = self::TYPE;
return Output::$type($result);
}
}

+ 0
- 28
private/View/Output.php View File

@@ -1,28 +0,0 @@
<?php

declare(strict_types=1);

namespace AugustOffensive\View;

use AugustOffensive\Model;

/**
* Output formats for Results that are returned to the client.
*/
class Output
{
/**
* Formats the result into JSON.
*
* @param Model\Result $result The result to return to the client.
*
* @return string
*/
public static function json (Model\Result $result): string
{
return json_encode(array(
"Result-Type" => $result->getResultType(),
"Content" => $result->getResult()
));
}
}

+ 45
- 0
src/bin/august-offensive.rs View File

@@ -0,0 +1,45 @@
extern crate actix;
extern crate actix_web;
extern crate env_logger;
extern crate dotenv;
extern crate diesel;

use actix_web::{middleware, server, App, HttpRequest};
use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
use std::env;

fn index(_req: &HttpRequest) -> &'static str {
"Hello, world!"
}

// TODO describe change of direction in README
// TODO implement error-chain
// TODO match 0.1.0 functionality
fn main() {
dotenv().ok();
let db_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let bind_address = env::var("BIND_ADDRESS")
.expect("BIND_ADDRESS must be set");

env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
let sys = actix::System::new("hello-world");

PgConnection::establish(&db_url)
.expect(&format!("Error connecting to {}", db_url));

server::new(|| {
App::new()
.middleware(middleware::Logger::default())
.resource("/index.html", |r| r.f(index))
.resource("/", |r| r.f(index))
}).bind(&bind_address)
.unwrap()
.start();

println!("Started http server: {}", bind_address);
let _ = sys.run();
}

private/Model/fresh.sql → src/util/fresh.sql View File


+ 0
- 97
test/Controller/ControllerTest.php View File

@@ -1,97 +0,0 @@
<?php

declare(strict_types=1);

namespace AugustOffensive\Controller;

use PHPUnit\Framework\TestCase;
use AugustOffensive\Model;

/**
* Integration test: requires DB connection. Expect side effects (use test db if possible).
*
* @covers Controller
*/
final class ControllerTest extends \PHPUnit\Framework\TestCase
{
public function testDBConnection()
{
/* Disable integration test, for now
try {
$this->assertInstanceOf(
Model\Connection::class,
Controller::initiateConnection()
);
} catch (\PDOException $err) {
$this->fail("Database not initialized correctly: " . $err->getMessage());
}*/
}

public function testCreateQuery()
{
$path = array("api", "create", "query");
$request = "DELETE";
$content = array("c" => "cherry", "d" => "dike");
$query = Controller::createQuery($path, $request, $content);

$this->assertInstanceOf(
Model\Query::class,
$query
);

$this->assertEquals(
$path,
$query->getPath()
);
$this->assertEquals(
$request,
$query->getRequest()
);
$this->assertEquals(
$content,
$query->getContent()
);
}

public function testCreateResult()
{

$resultType = "TYPE";
$result = array("no", "values");
$resultObject = Controller::createResult($resultType, $result);

$this->assertInstanceOf(
Model\Result::class,
$resultObject
);

$this->assertEquals(
$resultType,
$resultObject->getResultType()
);
$this->assertEquals(
$result,
$resultObject->getResult()
);
}

public function testErrorResult()
{
$message = "Oh no! Oops!";
$errorResult = Controller::errorResult(new \Exception($message));

$this->assertInstanceOf(
Model\Result::class,
$errorResult
);

$this->assertEquals(
"ERROR",
$errorResult->getResultType()
);
$this->assertEquals(
array("error" => $message),
$errorResult->getResult()
);
}
}

+ 0
- 42
test/Model/QueryTest.php View File

@@ -1,42 +0,0 @@
<?php

declare(strict_types=1);

namespace AugustOffensive\Model;

use PHPUnit\Framework\TestCase;

/**
* @covers Query
*/
final class QueryTest extends \PHPUnit\Framework\TestCase
{
public function testCanBeCreated()
{
$this->assertInstanceOf(
Query::class,
new Query(array("api", "path"), "GET", array())
);
}

public function testHasAccessibleValues()
{
$path = array("api", "query", "path");
$request = "PUT";
$content = array("a" => "apple", "b" => "bearing");
$query = new Query($path, $request, $content);

$this->assertEquals(
$path,
$query->getPath()
);
$this->assertEquals(
$request,
$query->getRequest()
);
$this->assertEquals(
$content,
$query->getContent()
);
}
}

+ 0
- 37
test/Model/ResultTest.php View File

@@ -1,37 +0,0 @@
<?php

declare(strict_types=1);

namespace AugustOffensive\Model;

use PHPUnit\Framework\TestCase;

/**
* @covers Result
*/
final class ResultTest extends \PHPUnit\Framework\TestCase
{
public function testCanBeCreated()
{
$this->assertInstanceOf(
Result::class,
new Result("TEST_SUCCESS", array("it worked"))
);
}

public function testHasAccessibleValues()
{
$resultType = "VISIBLE";
$result = array("array", "of", "values");
$resultObject = new Result($resultType, $result);

$this->assertEquals(
$resultType,
$resultObject->getResultType()
);
$this->assertEquals(
$result,
$resultObject->getResult()
);
}
}

+ 0
- 33
test/View/OutputTest.php View File

@@ -1,33 +0,0 @@
<?php

declare(strict_types=1);

namespace AugustOffensive\View;

use PHPUnit\Framework\TestCase;
use AugustOffensive\Model;

/**
* @covers Output
*/
final class OutputTest extends \PHPUnit\Framework\TestCase
{
public function testJSONOutput()
{
$resultType = "JSON_ENCODED";
$result = array("json", "1.6");
$resultObject = new Model\Result($resultType, $result);

// If JSON is properly formatted, we can decode and test the values
$output = json_decode(Output::json($resultObject), true);

$this->assertEquals(
$resultType,
$output["Result-Type"]
);
$this->assertEquals(
$result,
$output["Content"]
);
}
}

Loading…
Cancel
Save