Diamondのチュートリアルを触ってみた

# はじめに
> Diamond is a powerful MVC / Template Framework written in the D Programming Language, inspired by ASP.NET using vibe.d as backend.

[Diamond](https://diamondmvc.github.io/Diamond/)はパワフルなASP.NETインスパイア系MVC/テンプレートです。バックエンドはvibe.dが使われています。
MVCという響きが良いですね。早速チュートリアルを触ってみます。

# Features
まず公式ページの機能一覧を眺めます。
Web Application Frameworkとして必要な機能が整っていてとても良い感じです。

* General Features
  * Low Memory & CPU Consumption
  * MVC & HMVC
  * RESTful
  * Advanced Routing
  * ACL
  * Cross-platform
  * Website/Webapi Support
  * i18n
* Data & Storage
  * ORM
  * MySql ORM
  * Caching
  * Mongo
  * Redis
  * Request-context
  * Cookies
  * Sessions
  * Transactions
* Views & Frontend
  * Compile-time Parsing
  * Partial Views
  * Layouts
  * Fast & Performant Rendering
  * Dynamic
  * Any D Code Can Be Executed
  * Sections
  * Flash-messages
* Controllers
  * Auto-mapping
  * View-integration
  * Mandatory Actions
  * Version-control
* More
  * Authentication
  * CSRF Protection
  * Cryptography
  * JSON/BSON
  * Asynchronous
  * Fibers/Tasks
  * Sharding
  * Network Security & Restrictions
  * Unittesting
  * Logging

# [Tutorials](https://diamondmvc.github.io/Diamond/tutorials/)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/98084/fd210995-7e8d-03af-d70a-76963e20924a.png)
2017年12月14日現在、下記3つ以外のリンク先は`Coming soon …`となっています。

* [Createing website](https://diamondmvc.github.io/Diamond/tutorials/website)
* [Creating a web api](https://diamondmvc.github.io/Diamond/tutorials/webapi)
* [Using Diamond without Web](https://diamondmvc.github.io/Diamond/tutorials/stand-alone)

## [Using Diamond without Web](https://diamondmvc.github.io/Diamond/tutorials/stand-alone/)

このチュートリアルだけリポジトリがないので写経をしてビルドします。

```
../../../.dub/packages/diamond-2.5.3/diamond/core/webconfig.d-mixin-85(88,28): Error: file "test.dd" cannot be found or not in a path specified with -J
../../../.dub/packages/diamond-2.5.3/diamond/views/viewgenerator.d(34,7): Error: mixin diamondapp.GenerateViews!().generateViews.LoadViewData!false error instantiating
../../../.dub/packages/diamond-2.5.3/diamond/init/diamondapp.d(18,3): Error: mixin diamondapp.GenerateViews!() error instantiating
dmd failed with exit code 1.
```

まずは明らかに誤植と思しきファイル名を修正します。

```
renamed:    views/home.dd -> views/test.dd
```

```
../../../.dub/packages/diamond-2.4.8/diamond/data/i18n/loader.d(56,9): Error: undefined identifier addMessage
../../../.dub/packages/diamond-2.4.8/diamond/data/i18n/loader.d(82,5): Error: undefined identifier addMessage
dmd failed with exit code 1.
```

ファイルを確認します。

```
../../../.dub/packages/diamond-2.4.8/diamond/data/i18n/messages.d
79:  package(diamond) void addMessage(string language, string key, string message)
```

バグっぽいのでdiamondのバージョンを上げます。

```diff
--- a/dub.json
+++ b/dub.json
@@ -5,7 +5,7 @@
        "homepage": "http://mydiamondwebsiteproject.com/",
        "license": "http://mydiamondwebsiteproject.com/license",
        "dependencies": {
-               "diamond": "~>2.4.5"
+               "diamond": "~>2.5.3"
        },
        "sourcePaths": ["src", "models"],
        "stringImportPaths": ["views", "config"],
```

```
../../../.dub/packages/diamond-2.5.3/diamond/http/websockets.d(300,47): Error: mutable method vibe.http.server.HTTPServerRequest.path is not callable using a const object
../../../.dub/packages/diamond-2.5.3/diamond/http/websockets.d(298,64): Error: none of the overloads of 'handleWebSockets' are callable using argument types (void), candidates are:
../../../.dub/packages/vibe-d-0.8.2/vibe-d/http/vibe/http/websockets.d(267,28):        vibe.http.websockets.handleWebSockets(void function(scope WebSocket) nothrow @safe on_handshake)
../../../.dub/packages/vibe-d-0.8.2/vibe-d/http/vibe/http/websockets.d(272,28):        vibe.http.websockets.handleWebSockets(void delegate(scope WebSocket) nothrow @safe on_handshake)
../../../.dub/packages/vibe-d-0.8.2/vibe-d/http/vibe/http/websockets.d(320,28):        vibe.http.websockets.handleWebSockets(void delegate(scope WebSocket) nothrow @system on_handshake)
../../../.dub/packages/vibe-d-0.8.2/vibe-d/http/vibe/http/websockets.d(325,28):        vibe.http.websockets.handleWebSockets(void function(scope WebSocket) nothrow @system on_handshake)
../../../.dub/packages/vibe-d-0.8.2/vibe-d/http/vibe/http/websockets.d(330,28):        vibe.http.websockets.handleWebSockets(void delegate(scope WebSocket) @safe on_handshake)
../../../.dub/packages/diamond-2.5.3/diamond/http/websockets.d(298,64):        ... (3 more, -v to show) ...
dmd failed with exit code 1.
```


Diamondが対応しているvibe.dのバージョンは現在`0.8.1`までなので、明示的に指定します。

```diff
--- a/dub.json
+++ b/dub.json
@@ -5,7 +5,8 @@
        "homepage": "http://mydiamondwebsiteproject.com/",
        "license": "http://mydiamondwebsiteproject.com/license",
        "dependencies": {
-               "diamond": "~>2.4.5"
+               "vibe-d": "0.8.1",
+               "diamond": "~>2.5.3"
        },
        "sourcePaths": ["src", "models"],
        "stringImportPaths": ["views", "config"],
```

これでようやく実行できます。

```shell-session
$ ./diamondproject

<p> Hello World!</p>

```

中を見ていきます。

```shell-session
$ tree
.
|-- config
|   `-- views.config
|-- models
|   `-- package.d
|-- src
|   `-- main.d
|-- views
|   `-- test.dd
|-- diamondproject
|-- dub.json
`-- dub.selections.json
```

### config/views.conifg

```
test|test.dd
```

ビューの名前とファイル名を`{name}|{file}`の形式で書きます。

### models/package.d

```d
module models;

public
{
	// TODO: Import models here ...
}
```

モデルをインポートします。今回は空です。

### views/home.dd

```
@*The test view*
<p> Hello World!</p>
```

1行目はメタデータのブロックです。ビューの設定を記述します。
今回はコメントのみです。

### src/main.d

```d
module main;

import diamondapp : getView;

void main()
{
	import std.stdio;

	auto view = getView("test");

	writeln(view.generate());
	readln();
}
```

ビューを取得してその内容を標準出力に出力します。
テンプレートエンジンとしての挙動が確認できます。

## [Creating a web site](https://diamondmvc.github.io/Diamond/tutorials/website)

[こちら](https://github.com/DiamondMVC/Diamond-Template-WebServer)にソースがあるのでcloneします。
前のチュートリアルと同様に、vibe.dのバージョンを`0.8.1`に固定してビルドします。

```shell-session
$ ./diamond-webserver-empty
Listening for requests on http://[::1]:8181/
Listening for requests on http://127.0.0.1:8181/
Listening for requests on http://[::1]:8080/
Listening for requests on http://127.0.0.1:8080/
The web-server Empty Diamond WebServer is now running.
```


```shell-session
$ curl 127.0.0.1:8181
<!DOCTYPE html>
<html>
<head>
  <title>Home</title>
</head>
<body>


<p>Hello Diamond!</p>

</body>
</html>
```

```shell-session
$ curl 127.0.0.1:8080
<!DOCTYPE html>
<html>
<head>
  <title>Home</title>
</head>
<body>


<p>Hello Diamond!</p>

</body>
</html>
```

良いでしょう。中身を見ていきます。

```shell-session
$ tree
.
|-- config
|   |-- views.config
|   `-- web.json
|-- controllers
|   |-- homecontroller.d
|   `-- package.d
|-- core
|   `-- websettings.d
|-- models
|   `-- package.d
|-- views
|   |-- home.dd
|   `-- layout.dd
`-- dub.json
```

### dub.json

```json
{
    "name": "diamond-webserver-empty",
    "description": "Empty project template for a web-server",
    "authors": ["Jacob Jensen"],
    "homepage": "https://github.com/DiamondMVC/Diamond-Template-WebServer",
    "dependencies": {
        "vibe-d": "~>0.8.1",
        "diamond": "~>2.4.5",
        "mysql-native": "~>1.1.2"
    },
    "versions": ["VibeDefaultMain", "Diamond_Debug", "Diamond_WebServer"],
    "sourcePaths": ["core", "models", "controllers"],
    "stringImportPaths": ["views", "config"],
    "targetType": "executable"
}
```



### config/web.json

```json
{
  "name": "Empty Diamond WebServer",
  "staticFileRoutes": ["public"],
  "homeRoute": "home",
  "allowFileRoute": false,
  "accessLogToConsole": false,
  "addresses": [{
    "ipAddresses": ["::1", "127.0.0.1"],
    "port": 8181
  }, {
    "ipAddresses": ["::1", "127.0.0.1"],
    "port": 8080
  }],
  "defaultHeaders": {
    "general": {
      "Content-Type": "text/html; charset=UTF-8",
      "Server": "vibe.d - Diamond MVC/Template Framework"
    },
    "staticFiles": {
      "Server": "vibe.d - Diamond MVC/Template Framework"
    },
    "notFound": {
      "Content-Type": "text/html; charset=UTF-8",
      "Server": "vibe.d - Diamond MVC/Template Framework"
    },
    "error": {
      "Content-Type": "text/html; charset=UTF-8",
      "Server": "vibe.d - Diamond MVC/Template Framework"
    }
  }
}
```

* `staticFileRoutes`静的ファイル置き場を指定します
* `homeRoute`でデフォルトルートを指定します
* `allowFileRoute`はビューをファイル名でルーティングするか否かのフラグです
* `accessLogToConsole`はログを標準出力にリダイレクトするか否かのフラグです
* `addresses`で応答するIPアドレスを指定します
* `defaultHeaders`でデフォルトで使うヘッダーを指定できます
  * `general`は通常のレスポンスに使用されます
  * `stticFiles`は静的ファイルに使用されます
  * `notFound`は404応答時に使用されます
  * `error`はエラー時に使用されます

### config/views.config

```
layout|layout.dd
home|home.dd
```

ホーム用のビューとレイアウトファイルを指定しています。


### controllers/package.d

```d
module controllers;

public
{
  import controllers.homecontroller;
}
```

必要なコントローラをインポートします。MVCっぽくなってきました。

### controllers/homecontroller.d

```d
module controllers.homecontroller;

import diamond.controllers;

/// The home controller.
final class HomeController(TView) : Controller!TView
{
  public:
  final:
  /**
  * Creates a new instance of the home controller.
  * Params:
  *   view =  The view assocaited with the controller.
  */
  this(TView view)
  {
    super(view);
  }

  /// Route: / | /home
  @HttpDefault Status home()
  {
    return Status.success;
  }
}
```

コントローラの実装です。ロジックがないので200応答を返すだけになっています。
コントローラのメソッドは必ず`Status`を返す必要があります。

### core/websettings.d

```d
module websettings;

import diamond.core.websettings;

class DiamondWebSettings : WebSettings
{
  import vibe.d : HTTPServerRequest, HTTPServerResponse, HTTPServerErrorInfo;

  import diamond.http;

  private:
  this()
  {
    super();
  }

  public:
  override void onApplicationStart()
  {
  }

  override bool onBeforeRequest(HttpClient client)
  {
    return true;
  }

  override void onAfterRequest(HttpClient client)
  {

  }

  override void onHttpError(Throwable thrownError, HTTPServerRequest request,
    HTTPServerResponse response, HTTPServerErrorInfo error)
  {
    response.bodyWriter.write(thrownError.toString());
  }

  override void onNotFound(HTTPServerRequest request, HTTPServerResponse response)
  {
    import std.string : format;

    response.bodyWriter.write(format("The path '%s' wasn't found.'", request.path));
  }

  override void onStaticFile(HttpClient client)
  {

  }
}

void initializeWebSettings()
{
  webSettings = new DiamondWebSettings;
}
```

* `onBeforeRequest`リクエストが処理される前処理をハンドリングできます
* `onAferRequest`リクエストが処理される前処後をハンドリングできます
* `onHttpError`エラー時のハンドリングをします
* `onNotFound`404エラー時のハンドリングをします
* `onStaticFile`静的ファイルが呼ばれた時のハンドリングをします

### models/package.d

```d
module models;

public
{
    // TODO: Import models here ...
}
```

モデルは今回も利用しません。

### views/layout.dd

```
@<doctype>
<html>
<head>
  <title>@<title></title>
</head>
<body>
  @<view>
</body>
</html>
```

レイアウトを記述します。`doctype`はDiamondが提供するデフォルトのドキュメントタイプのプレイスホルダです。
`title`ろ`view`はそれぞれ、このページのタイトルと、このレイアウトを利用するページのビューのプレイスホルダです。

## views/home.dd

```
@[
  layout:
    layout
---
  route:
    home
---
  controller:
    HomeController
---
  placeHolders:
    [
	  "title": "Home"
    ]
]

<p>Hello Diamond!</p>
```

下記の値はそれぞれの各値に対応します。

* `layout` レイアウト名
* `model` モデル名
* `controller` コントローラ名
* `route` パス

`placeHolders`はプレイスホルダを連想配列で記述します。
ここではページのタイトルのみです。

## [Creating a web site](https://diamondmvc.github.io/Diamond/tutorials/webapi)

[こちら](https://github.com/DiamondMVC/Diamond-Template-WebApi)からcloneし、同様にvibe.dのバージョンを`0.8.1`に固定してからビルドします。

```
controllers/homecontroller.d(11,30): Error: template instance Controller!HomeController Controller is not a template declaration, it is a class
```

```diff
--- a/src/controllers/homecontroller.d
+++ b/src/controllers/homecontroller.d
@@ -8,7 +8,7 @@ module controllers.homecontroller;
 import diamond.controllers;

 /// The home controller.
-final class HomeController : Controller!HomeController
+final class HomeController : Controller
 {
   public:
   final:
```

これで一旦ビルドできたので実行します。

```shell-session
$ ./diamond-webapi-empty
unhandledError: object.Exception@../../../../../.dub/packages/vibe-d-0.8.1/vibe-d/data/vibe/data/serialization.d(782): Missing non-optional field 'staticFileRoutes' of type 'WebConfig' (DefaultPolicy(T)).
object.Exception@../../../../../.dub/packages/vibe-d-0.8.1/vibe-d/data/vibe/data/serialization.d(782): Missing non-optional field 'staticFileRoutes' of type 'WebConfig' (DefaultPolicy(T)).
```

`staticFileRoutes`が何かおかしいようです。`web.json`に記述が足りていないようなので追記します。

```diff
--- a/src/config/web.json
+++ b/src/config/web.json
@@ -1,5 +1,6 @@
 {
   "name": "Empty Diamond WebApi",
+  "staticFileRoutes": ["public"],
   "homeRoute": "home",
   "allowFileRoute": false,
   "accessLogToConsole": false,
```

これでビルドできました。実行してみます。

```shell-session
$ ./diamond-webapi-empty
Listening for requests on http://[::1]:8181/
Listening for requests on http://127.0.0.1:8181/
Listening for requests on http://[::1]:8080/
Listening for requests on http://127.0.0.1:8080/
The web-api Empty Diamond WebApi is now running.
```

```shell-session
$ curl 127.0.0.1:8080 -i
HTTP/1.1 200 OK
Server: vibe.d - Diamond MVC/Template Framework
Date: Wed, 13 Dec 2017 14:37:46 GMT
Keep-Alive: timeout=10
Content-Type: text/json; charset=UTF-8
Transfer-Encoding: chunked

{
      "message": "Hello Diamond!"
    }
```

インデントが大分おかしいですがJSONが返ってきました。
中身を見ていきます。

```
.
|-- config
|   |-- controllers.config
|   `-- web.json
|-- controllers
|   |-- homecontroller.d
|   `-- package.d
|-- core
|   `-- websettings.d
|-- models
|   `-- package.d
`-- dub.json
```

前のチュートリアルと違うところはコントローラです。


### config/controllers.config

```
Home
```
`Controller`を抜いたコントローラ名を記述します。

### controllers/package.d

```d
module controllers;

public
{
  import controllers.homecontroller;
}
```

利用するコントローラをインポートします。

### controllers/homecontroller.d

前のチュートリアルとコントローラの差を見てみます。

```diff
$ diff Diamond-Template-Web{Server,Api}/src/controllers/homecontroller.d
--- Diamond-Template-WebServer/src/controllers/homecontroller.d 2017-12-13 19:56:08.858793540 +0900
+++ Diamond-Template-WebApi/src/controllers/homecontroller.d    2017-12-13 23:27:14.073648179 +0900
@@ -1,6 +1,6 @@
 /**
 * Copyright c DiamondMVC 2016-2017
-* License: MIT (https://github.com/DiamondMVC/Diamond-Template-WebServer/blob/master/LICENSE)
+* License: MIT (https://github.com/DiamondMVC/Diamond-Template-WebApi/blob/master/LICENSE)
 * Author: Jacob Jensen (bausshf)
 */
 module controllers.homecontroller;
@@ -8,23 +8,25 @@
 import diamond.controllers;

 /// The home controller.
-final class HomeController(TView) : Controller!TView
+final class HomeController : Controller
 {
   public:
   final:
   /**
   * Creates a new instance of the home controller.
   * Params:
-  *   view =  The view assocaited with the controller.
+  *   client =  The client passed to the controller.
   */
-  this(TView view)
+  this(HttpClient client)
   {
-    super(view);
+    super(client);
   }

   /// Route: / | /home
   @HttpDefault Status home()
   {
-    return Status.success;
+    return jsonString(`{
+      "message": "Hello Diamond!"
+    }`);
   }
 }
```
主な違いはビューの有無と[`jsonString()`](https://github.com/DiamondMVC/Diamond/blob/bb9b9333b94c61517765c708ea593e9c094575a6/controllers/controller.d#L537)です。ちょっと気持ち悪いですが`Status`を返します。

# まとめ

チュートリアルで簡単な動きを確認できました。
各機能についても今後触っていく予定です。