carloでHTTP通信してみる

carloとは

github.com

githubの説明文をgoogleで翻訳しました。

Carloは、ノードアプリケーションにGoogle Chromeレンダリング機能を提供し、Puppeteerプロジェクトを使用してローカルにインストールされたブラウザインスタンスと通信し、ノードとブラウザ間の通信のためのリモート呼び出しインフラストラクチャを実装します。

ポイントは「ローカルにインストールされたブラウザインスタンスと通信」の部分。 ローカルのchromeを使うから必然的にバイナリサイズが小さくなる。

疑問 クロスドメインなサイトにajaxでHTTP通信できるの?

結論としては「ajaxではできない」
UI(html)はchrome上で動いてるだけだから。

↑すいません、これ嘘でした。 やり方は追記で。

ただnode.jsとの間で通信できるから

UI => node => HTTP

のような構図でnodeを間に挟めばいけそう。

1. hello world

とりあえず表示するまで

setup

npm init
npm install carlo --save

これでpackage.jsonが作られる。

htmlを作る

www/index.html

<body>Hello</body>

index.jsでhtmlを読み込む

const carlo = require('carlo');

carlo.launch().then(async app => {
  app.serveFolder(__dirname + '/www');
  app.on('exit', () => process.exit());
  await app.load('index.html');
});

起動

node .

これでウィンドウにHelloが表示される

2. UIからnodeを呼ぶ

carloにrpcというクラスがあるのでそれを使います。

index.jsに呼ばれる側を作る

const carlo = require('carlo');
const { rpc } = require('carlo/rpc');// 追加

carlo.launch().then(async app => {
  app.serveFolder(__dirname + '/www');
  app.on('exit', () => process.exit());
  await app.load('index.html', rpc.handle(new Backend) /* 追加 */);
});

// htmlから扱うクラス追加
class Backend {
  hello(name) {
    console.log(`Hello ${name}`);
    return 'Backend is happy';
  }
}

htmlからnodeを呼ぶ

www/index.html

<script>
// 追加
async function run() {
  console.log("run");
  const [backend] = await carlo.loadParams();
  
  console.log(await backend.hello('from frontend'));
}
</script>
    
<body onload="run()">Hello</body>

carlo.loadParams()でnode側のインスタンスを取り出してhello()を実行します。
するとnode側の実装でconsole.log()が実行され、ターミナル上にfrom frontendと表示されます。

実行

ターミナル上にHello from frontendが表示されました。 UI上のconsoleにはBackend is happyが表示されました。

3. node-fetchで通信する

chromeで使えるfetch関数はnodeにはないので、それをインストールしてhtmlから利用します。

node-fetchのインストール

npm install node-fetch --save

nodeに通信部分を実装する

index.js

const carlo = require('carlo');
const { rpc } = require('carlo/rpc');
const nodeFetch = require('node-fetch');// 追加

carlo.launch().then(async app => {
  console.log(rpc);
  app.serveFolder(__dirname + '/www');
  app.on('exit', () => process.exit());
  await app.load('index.html', rpc.handle(new Backend));
});

class Backend {
  hello(name) {
    console.log(`Hello ${name}`);
    return 'Backend is happy';
  }

  // 追加: 通信して結果を返す
  async fetch(url) {
    const res = await nodeFetch(url);
    return res.text()
  }
}

UIから呼び出す

<script>
async function run() {
  console.log("run");
  const [backend] = await carlo.loadParams();

  console.log(await backend.hello('from frontend'));

  // 追加
  const text = await backend.fetch('https://www.google.com/')
  console.log(text);
}
</script>
    
<body onload="run()">Hello</body>

実行

これでUIのconsole上にgoogleのソースが表示されました

まとめ

carloはchromeのラッパーみたいなものなのでUI上からクロスドメインなHTTP通信はできないが、nodeを介せばできることがわかった。
気軽に使えそうな予感。

追記

「UI側でクロスドメインな通信は無理」と書きましたが、できました。 方法としては起動オプションでセキュリティをoffにすればOK。
こんな感じです。

index.js

const carlo = require('carlo');

carlo.launch({
    args:['--disable-web-security']// 追加
  }).then(async app => {
  app.serveFolder(__dirname + '/www');
  app.on('exit', () => process.exit());
  await app.load('index.html');
});

ポイント launchのオプションに--disable-web-securityを渡すところ。これでクロスドメインの制限が外れる

inde.html

<script>
async function run() {
  console.log("run");
  const res = await fetch('https://www.yahoo.co.jp');
  const text = await res.text();
  console.log(text);
}
</script>
    
<body onload="run()">Hello</body>

fetchで普通に取得可能になる

セキュリティには気をつけろ

これで目標は達成したけど、webセキュリティを外すと何が起こるのかわからないので気をつけようw
node側でApp.serveHandler(handler)を使えばHttpRequestにフィルターがかけれるのでそれでアクセス制限かけるのもアリだと思う

ドメイン駆動設計なエンジニアの育成プログラムを作った

この記事は Engineering Manager vol.2 Advent Calendar 2018 - Qiita の16日目の記事です。

今の会社にはチームにジョインした方に対してドメイン駆動設計での開発ができるように育成するプログラムがあります。
「プログラムがある」と言っても有志で持ち回りでやってるちょっと真面目な勉強会のようなものです。
それを私がやることになったので、今日はその時に考えたことを書きます。
身の回りに勉強会等開いてくれる人がいたら「あの人はこんな感じのことを考えてるのかー」と思ってもらえたら幸いです。

誰?

  • 名前: なおしむ
  • ISPでエンジニアをしている
  • 最近は新しく来た人の育成もしている

背景

DDDスタート塾とは?

  • 新しく来た人が最初に入る塾
  • 塾の講師は「塾長」と呼ばれる
  • チャットルームのアイコンはこれ
    • f:id:naosim:20181215011116p:plain
  • 塾の期間はだいたい4週間
  • 塾の受講者はだいたい4人くらい
  • 塾自体はずっと前からあったが、私は塾長をやったことがなかったので今回引き受けてみた

計画

塾名の改名

過去の塾は塾長から名前をとって、例えば「鈴木塾」とか「佐藤塾」とか呼ばれてました。
塾に名前が入ってると属人化しそうだったので(してたので)今回「DDDスタート塾」に改名しました。

ゴールの定義「DDDスタート塾のゴールはなんだろう?」

まずはチームメンバーに相談しつつ塾のゴールを定義しました。
塾の期間だけでドメイン駆動設計がカンペキにわかるわけがないので、塾のゴールを「チームに入ってDDDなコードを読み、理解し、書くことができる」としました。

育成コンセプト「箸の持ち方から教える」

ジョインされる方はだいたい事前に面談をしています。そこでjava経験アリ・Spring経験アリと言ってても、実際はできない方もいます。
たとえばjavaはできるが設計はできないとか。
ただ面談時に「この人はいける!」と思って採用した方なので「できないのは経験がないだけ、教えればできるようになる」と考えています。 なので必要なスキルがすべて身につくようにプログラムを設計しました。

ゴールに対して必要なスキルに細分化する

DDDは色々は技術を積み上げたようなものなので、DDDを細分化しました。

余談ですがPlantUMLがあるのはExcelじゃない感を出したいだけです。

ざっくりスケジュールと学ぶこと

全体の進め方

毎日1時間は勉強会形式の集合研修をやり、そのほかの時間は研修で出た宿題をやってもらいました。
集合研修の内容はメンバーの進捗に各種説明、ハンズオン、宿題のレビューです。

計画を練りすぎてもしょうがない

ここから先は、行き当たりばったりです。都度メンバーの状態を見ながら説明したりハンズオンをしたりして進めました。

塾を2回やった結果

上記の計画でプログラムを2回やりました。
期間でいうと4週間x2回で約2ヶ月です。 FAN DONE LEARNでまとめてみます。(←最近流行ってる)

DONE

4週間になんとか収まった

ギリギリ間に合った感が否めない。 SQL文とか細かいところが伝えきれてない。

LEARN

2ヶ月間、毎日勉強会をやり続けるのは大変

毎日どんな質問がくるかわからない中で塾を進めるのは大変でした。質問内容とかは形式知にしないとなーと思った。(やってない)
とりあえず説明するときに使った資料はこちらです。 www.itsenka.com

www.slideshare.net naosim.hatenablog.jp www.slideshare.net

「なぜDDDをやるのか」をうまく伝えることができない

オブジェクト指向とかイベントソーシングとかいろんな要素から生まれていたり、そもそもDDDを実験的にやってみてるところもあるので、DDDをやる理由をシンプルに説明するのが難しかった。

ハンズオンをたくさんやるべき

HelloWorldでも意外にみんなハマる。原因は、サイトの内容を理解せずにコピペしてたり、3ステップくらい必要な機能を一気に実装しようとしてわけわかんなくなったりいろいろ。そうゆうのをひとつひとつ指摘していくことに意義があるとおもった。

FAN

中間テストをやると緊張感と達成感があって良い

オニオンアーキテクチャのお題が終わったタイミングで中間テストをやってみました。
もちろん事前に告知して。
そしたら程よい緊張感で結構楽しかったです。
ちなみに問題はこん感じ。

  • intelljでプロジェクトを作ってspockでテストを動かすまでをググらずにやりなさい ※ただしdependencesに書く内容とか暗記できないようなものは提示する
  • 真っ白な紙にオニオンアーキテクチャの図を書きなさい

既に業務をやってる人が「入門したい!」と言ってきた

既に案件をガッツリやってる方なので「チームで時間が取れるように調整できるならイイよー」ってことにした。 私としてもある程度知識がある人がいた方が質問が活発になってやりやすかった。
こうゆう広がりすごく嬉しい。

今後のトライ

形式知化する

行き当たりばったりで資料を作成したりネットの資料を使ったりしてたので、そうゆうのはまとめようと思う。最悪資料読めば講師がいなくてもメンバーが進めれるようにしたい。(緊急の用事が入ったりもするし)

講師役を増やす

形式知を増やせばある程度誰でも講師役ができるようになると思う。講師役をやることで理解が深まることもあるので、入門→業務遂行→講師→より深い知識って感じのパスを作りたい。

まとめ

ダラダラとたくさん書きましたが、講師をすることでみんながつまずくポイントが分かって勉強になった。課題はたくさんあるけど、いろんなやり方を試しつつ続けていきたいと思いました。

【問題】キャメルケースの英語を日本語に変える

仕様書が日本語でプログラムは英語だと単語のマッピング表が欲しくなりますね。
今日はそんな問題です。

TODOの部分を実装し、下部の期待する振る舞いをするようにせよ。

<!DOCTYPE html>
<script>
function convertToJpName(camelCaseText) {
    // TODO 実装
}

// 期待する振る舞い
console.log(convertToJpName("engagementId"));        // => 契約ID
console.log(convertToJpName("engagementStartDate")); // => 契約開始日
console.log(convertToJpName("engagementEndDate"));   // => 契約終了日
console.log(convertToJpName("updateDateTime"));      // => 更新日時
</script>

とりあえずif文を4つ書けば解けますね。
けどもっと汎用的にしたいですねー。
例えば"engagementUpdateDate"が入力されたらプログラムを修正しなくても"契約更新日"になってくれるとか。

SpringBoot + h2 + mybatisでHelloWorld

ハンズオン用のメモです
開発環境はIntellij、ビルドツールはgradleです

今回のゴール

  • SpringBoot + h2 + mybatisなアプリケーションをIntellij + gradleで作成すること
  • APIは以下の3本を作成すること
API 説明 METHOD URL RESPONSE
HELLO ただhelloを返す GET http://localhost:8080/hello {"response":"hello"}
LIST DBのデータをリストで返す GET http://localhost:8080/list {"response":["2018-11-28T07:29:19.596"]}
INSERT DBに挿入する GET http://localhost:8080/insert {"response":{"id":2,"event_date_time":"2018-11-28T07:30:35.264"}}

プロジェクトを新規作成する

File -> New -> Projectで作ります

f:id:naosim:20181128074202p:plain
Gradleを選択し、Javaにチェックを入れてNext

f:id:naosim:20181128074231p:plain
GroupIdに自分のパッケージ名をいれ、ArtifactIdにプロジェクト名をいれてNext

f:id:naosim:20181128074253p:plain
Use auto-importにチェックを入れます。そうするとimport文を自動的に最適化してくれます
あとはそのままでNext

次もNext

これで完了
いろいろロードされるのでしばし待つ

SpringBootでHello

build.gradleを編集する

buildscriptを追加する

springのプラグインを使いたいので、pluginsの上にbuildscriptを書く

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE")
    }
}

pluginにspringを追加する

先ほどのbuildscriptを書いた上で以下を追加

plugins {
    id 'java'
    id 'eclipse'
    id 'idea'
}

apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

pluginsに追加したらうまく動かなかったのでapply pluginにした
pluginsがダメな理由はよくわからない

dependenciesにspringを追加する

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.16.20'

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

最小構成としてはspring-boot-starter-webだけで良い
個人的にlombokも使いたいので追加

build.gradle全体

全体はこんな感じ

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE")
    }
}

plugins {
    id 'java'
    id 'eclipse'
    id 'idea'
}

apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group 'com.naosim'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.16.20'

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

Applicationにmain関数を書く

要するにメイン関数を記述します
パス: ./src/main/java/com/naosim/dddspringh2/Application.java

package com.naosim.dddspringh2;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Applicationクラスのパッケージ名がとても重要です
なぜならこのパッケージ配下は後記述する@Autowiredが自動的に聞きますがパッケージ配下以外の場合は設定をしないと効かないから
なので何も考えないのであればApplicationクラスは出来るだけ上位のパッケージに作った方がいいです

ControllerにhelloのAPIを作成する

helloを返すだけのAPIを作ります

package com.naosim.dddspringh2;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@AllArgsConstructor
public class Controller {
    
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public Map hello() {
        Map<String, Object> res = new HashMap<>();
        res.put("response", "hello");
        return res;
    }
}

@RequestMappingでURLのパスやメソッドを設定します
hello()の戻り値をMapにすると自動的にJSON形式に変換してくれます
なのでこのAPIの挙動としては

GET http://localhost:8080/hello

を投げると

{"response":"hello"}

を返すカタチになります

アプリケーションを実行してみる

実行の仕方もたくさんあるので紹介します
intelljがたまにおかしくなるので、やり方は全部知っておいた方がいいです

intellijのgradleから実行する

intellijの右側のバーからgradleを選択し、Tasks->application->bootRunをダブルクリックすると実行されます
intellijの停止ボタンを押すと停止できます
停止せずに別のアプリを起動するとポートが被るために起動できないことがあるので、実行が終わったら必ず停止しましょう

Applicationクラスから実行する

Applicationクラスのソースコードを表示すると、クラス名とメソッド名の左に再生ボタンが表示されています
これをクリックし、Run'Application.main()'を選択すると実行できます

ターミナルから実行する

ターミナルから

./gradlew bootRun

で実行できます
停止はcontrol + cです
intellijがおかしくなったらこのやり方が最強です たまにintellijだとエラーだけどターミナルからなら実行出来ることがありますが この場合、ソースコードは間違ってないです
intellijの何かがうまくいってないだけです
intellijを再起動するなり、ネットワーク周りの設定を見直すなりしましょう

動作確認

前置きが長くなりましたが動作確認です アプリケーションを実行した状態で

http://localhost:8080/hello

にアクセスすると想定通りのjsonが表示されます

{"response":"hello"}

ここまででSpringBootでhelloを返すAPIができました

h2 + mybatis

ここから先はDBを扱う設定の話です

build.gradleにh2とmybatisを追加する

まずはbuild.gradleにライブラリを追加します

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.16.20'

    compile group: 'com.h2database', name: 'h2', version: '1.4.197'// 追加
    compile group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '1.3.2'// 追加

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

application.ymlにh2の設定を書く

リソースにh2の設定を記述します パス: ./src/main/resources/application.yml

spring.datasource.url: jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE
spring.datasource.driverClassName: org.h2.Driver
spring.datasource.username: sa
spring.datasource.password:
spring.h2.console.enabled: true

今回はインメモリの設定で書きました
もしもアプリケーションが終了してもデータが消えないようにしたいなら spring.datasource.urlの値をこんな感じにすると良いです

spring.datasource.url=jdbc:h2:file:./target/db/testdb

最後の./target/db/testdbがファイルの出力先になるので適宜変更してください

DBのセットアップ処理を書く

リソースにCreateTable文や初期状態のinsert文を書きます

schema.sql

schema.sql起動時に最初に呼んでくれるsqlです
ここにCreateTable文を書きます

DROP TABLE IF EXISTS time_event;
CREATE TABLE time_event (
    id INT PRIMARY KEY AUTO_INCREMENT,
    event_date_time DATETIME
);

DROP TABLE IF EXISTSはテーブルがあったら一旦消すコマンドです インメモリだから2回目の起動を意識する必要はないけど念のため決しておきます

data.sql

schema.sqlの次に呼んでくれるsqlです
ここにDBの初期状態を書いておきます

INSERT INTO time_event (event_date_time) VALUES (sysdate);

マッパーにDB操作のメソッドを書く

Event

後に記述するマッパーで利用するためのEventクラスを作ります 作成したテーブルのカラム名と本クラスのフィールド名を一致させるのがミソです パス: ./src/main/java/com/naosim/dddspringh2/datasource/Event.java

package com.naosim.dddspringh2.datasource;

import java.time.LocalDateTime;

public class Event {
    public int id;
    public LocalDateTime event_date_time;
}

DbMapper

マッパーでDBの操作を書きます パス: ./src/main/java/com/naosim/dddspringh2/datasource/DbMapper.java

package com.naosim.dddspringh2.datasource;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.time.LocalDateTime;
import java.util.List;

@Mapper
public interface DbMapper {
    @Select("SELECT * FROM time_event ORDER BY id")
    List<Event> select();

    @Insert("INSERT INTO time_event (event_date_time) VALUES (#{datetime});")
    void insert(@Param("datetime")LocalDateTime localDateTime);
}

interfaceで作ります interfaceに@Mapperを付けるとmybatisとして処理されます

select()の解説
@Select("SELECT * FROM time_event ORDER BY id")
List<Event> select();

@Selectの中にSELECT文を書きます
戻り値は今回は複数返るのでListにしています
ジェネリクスで型を指定すると、mybatisがそのクラスをnewしてカラム名と同じ名前のフィールドに値を入れるところまで自動でやってくれます
ちなみに戻り値の数が1つだけの場合はListではなく戻りの型を指定します
この状態でSELECTが複数ヒットした場合は例外になります
必ず1つとわかっているならその記述の方が良いと思います

insert()の解説
@Insert("INSERT INTO time_event (event_date_time) VALUES (#{datetime});")
void insert(@Param("datetime")LocalDateTime localDateTime);

@Insertの中にInsert文を書きます
変数を使う場合は#{datetime}のように書くとSQLインジェクション等をイイ感じに回避してくれます
変数名はメソッド引数の@Paramで指定した文字列と同じものを指定します

ControllerからMapperを使う

Controller.javaAPIを追加します

マッパーをDIする

@RestController
@AllArgsConstructor
public class Controller {
    @Autowired
    private final DbMapper dbMapper;
...

フィールドにDbMapperを追加し、@Autowiredを付与します
これでController生成時にマッパーを生成してくれます
注意点としては、@AutowiredはDIによって生成されたものの中でだけ効くということ たとえばControllerクラスを自分で

Controller c = new Controller();
System.out.print(c.dbMapper);

として場合、Controllerは自分で生成していてAutowiredが聴いていないためdbMapperはnullになります それを防ぐ方法としては 「@Autowiredを付けるフィールドは必ずfinalを付け、コンストラクタインジェクションにすること」 です
DIの方法にはコンストラクタインジェクションとフィールドインジェクションの二種類がありますが、前者の方がいいです 理由は...良い記事を見つけたのでリンクを貼っておきます

pppurple.hatenablog.com

今回の例ではDbMapperにfinalを付けて、Controllerクラスに@AllArgsConstructorを書くことでコンストラクタインジェクションにしています

DB内のデータをリストで取得するAPIを作成する

上記のAutowiredでマッパーが使えるようになったのでそれを使ってデータ取得APIを作ります マッパーからデータを取得し、日付だけにして返します

@RequestMapping(value = "/list", method = RequestMethod.GET)
public Map list() {
    Map<String, Object> res = new HashMap<>();
    res.put("response", dbMapper.select().stream().map(v -> v.event_date_time).collect(Collectors.toList()));
    return res;
}

この状態でhttp://localhost:8080/listにアクセスすると

{"response":["2018-11-28T07:29:19.596"]}

のようなデータが取得できます

DBにデータを投入するAPIを作成する

現在日時をinsertするAPIを作成します インサートしたら、インサートした値をレスポンスで返します

@RequestMapping(value = "/insert", method = RequestMethod.GET)
public Map insert() {
    dbMapper.insert(LocalDateTime.now());

    // 返却値
    List l = dbMapper.select();
    Map<String, Object> res = new HashMap<>();
    res.put("response", l.get(l.size() - 1));
    return res;

}

この状態でhttp://localhost:8080/insertにアクセスすると

{"response":{"id":2,"event_date_time":"2018-11-28T07:30:35.264"}}

のようなデータが返ってきて、挿入できたことが確認できます

Controllerの全体はこんな感じ

package com.naosim.dddspringh2;

import com.naosim.dddspringh2.datasource.DbMapper;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@RestController
@AllArgsConstructor
public class Controller {
    @Autowired
    private final DbMapper dbMapper;

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public Map hello() {
        Map<String, Object> res = new HashMap<>();
        res.put("response", "hello");
        return res;
    }

    @RequestMapping(value = "/list", method = RequestMethod.GET)
    public Map list() {
        Map<String, Object> res = new HashMap<>();
        res.put("response", dbMapper.select().stream().map(v -> v.event_date_time).collect(Collectors.toList()));
        return res;
    }

    @RequestMapping(value = "/insert", method = RequestMethod.GET)
    public Map insert() {
        dbMapper.insert(LocalDateTime.now());

        // 返却値
        List l = dbMapper.select();
        Map<String, Object> res = new HashMap<>();
        res.put("response", l.get(l.size() - 1));
        return res;

    }
}

以上で、ゴール達成です
お疲れ様でした!

RAMLの導入を考える

夜中に眠れなくなってしまったので、なんとなくRAMLの導入について考える

RAMLとは

YAMLでRestAPIのAPI仕様を作成できるツール

今私がいる環境

  • java
  • spring boot
  • ddd

何が生成できるの?

YAMLから以下を生成できる

  • API仕様書
  • バリデーションチェック(可能性)
  • スタブ作成
  • テスト仕様書作成(可能性)

疑問: RAML => ソースソース => RAMLか?

RAML => ソースの場合

RAMLは設計書、ソースは製造物と考える

pros

  • RAMLをしっかりレビューして、そこから差異なく製造することにより品質を担保できる
  • コンポ図との相性が良い

cons

  • コードを手で入力したら結局差異が出てバグる (Excel仕様書と差異なし)
  • RAMLからコードを自動的に生成するのは大変そう
  • RAMLをjavaに食わせたり出来んの?
  • テストコードの自動生成ができればいいのかなぁ

ソース => RAMLの場合

ソースは設計書兼製造物と考える
RAMLはそこから出力される何か

pros

  • RAMLとソースの差異がない

cons

  • RAML生成までに時間がかかる(実装が必要なため)
    • 既存APIへの変更の場合は内部実装も修正しないとRAMLを生成できない
    • Requestクラスを毎回新規作成すれば良さそうだが差分がわかりづらくなる
    • 仕様書早くくれ〜って他チームから言われると辛い

感想

メンテまで考えるとソース => RAMLは現実的ではなさそう
ただRAML => ソースも自動化をしておかないと旨味を活かせない

疑問: スタブ作成?既存APIはどうすんの?

APIのバージョン管理をしてないと無理
優先低い

こんなこといいな、できたらいいな

spockのテストパタン自動生成

RAMLに記述したテストからspockのwhereとwhenを自動生成する
setupとthenは自力で書く
設計書(RAML)、テストコード、製造の流れが作りやすくなりそう

特に結論もなく以上です

【SpringBoot】Formクラスのフィールドを値オブジェクトにする

真面目に調べたことがなかったのでメモ

@RestController
public class Api {
    @RequestMapping("/")
    public String index(Form form) {
        return "Greetings from Spring Boot! " + form.name.value;
    }

    // privateでもok
    private static class Form {
        private NameForm name;


        // フォームではセッターで渡す
        public void setName(NameForm nameForm) {
            this.name = nameForm;
        }
    }

    // privateでもok
    private static class NameForm {
        private final String value;

        // 値オブジェクトへはコンストラクタで渡す
        public NameForm(String value) {
            this.value = value;
        }
    }
}

ポイント

Formクラス

  • メソッド名をAPIのキー名と同じにする
  • セッターへは値オブジェクトを渡す

値オブジェクト (NameForm)

  • 値はコンストラクタで渡す

その他

Formや値オブジェクトはプライベートクラスにしてもOK

googleドキュメントにmermaid.jsを書く

googleドキュメントはオンライン上で編集できてとても便利
そこにmermaid.jsも書きたい!ってことでやってみた

ドキュメントを書く

こんな感じ
f:id:naosim:20181101171058p:plain
これを読み込んでHTMLのページ上に図を表示します

スクリプトエディタでプログラムを書く

ツール > スクリプトエディタでエディタを開いてプログラムを書きます

//ドキュメントのID
var DOC_ID = 'YOUR_DOCUMENT_ID';

// HTMLを表示する
function doGet() {
  return HtmlService.createTemplateFromFile("index").evaluate();
}

// ドキュメントの内容を取得する
function getDocs() {
  var docs=DocumentApp.openById(DOC_ID);
  return {
    title: docs.getName(),
    text: docs.getBody().getText()
  }
}

YOUR_DOCUMENT_IDのところにドキュメントのIDを入れます
具体的にはドキュメントのURLの↓この部分です https://docs.google.com/document/d/!!ココ!!/edit

HTMLを作る

ファイル > 新規作成 > HTMLファイルを選択しindex.htmlというファイルを作ります
ファイルの中身はこんな感じ

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script src="https://unpkg.com/mermaid@7.1.2/dist/mermaid.js"></script>
  </head>
  <body>
    <h1 id="title"></h1>
    <div id="mermaid" class="mermaid"></div>
    <script>
    function update() {
      google.script.run
        .withSuccessHandler(function(result) {// 戻り値に対する処理
          console.log(result);  
          document.querySelector('#title').innerHTML = result.title;
          document.querySelector('.mermaid').innerHTML = result.text;
          mermaid.init();// mermaidを更新
        })
        .getDocs();
    }
    
    update();// 初回実行
    
    mermaid.initialize({startOnLoad:false});
    </script>
  </body>
</html>

公開する

公開 > ウェブアプリケーションとして導入を選択して必要事項を埋める
アプリケーションにアクセスできるユーザ自分だけにすると自分以外見れないので安心です

作成したサイトのURLにアクセスすると図が表示される

キターー!

f:id:naosim:20181101171300p:plain

まとめ

mermaid.jsが手軽にかけるようになった
markdown + mermaid.jsが表示できるようにしたくなってきた!
開発は続く