TypeScriptでモデルの型定義を良い感じに管理したい。

効率良くTypeScriptでドメインモデルの型定義を管理したい。

例えばここに、以下のようなツイッターのような投稿を模したモデルがあるとする。

type Post = {
  id: number,
  content: string
};

ふむ。特筆すべきものはない。 ではこれをモジュールとして型だけexportすることにする。 一文増えた。

type Post = {
  id: number,
  content: string
};

export = Post

ではこの型定義を利用した利用シーンを考える。 addPostという投稿を追加する関数を例で上げてみる。

import * as Post from "./model/post"

const addPost=(post:Post)=>{/* some implementation */}

...

ふむ。良さそうに見える。

ではaddPostを利用するシーンに移ってみる。 ここではテキストエリアに書かれた内容をonClick時に投稿するシーンを想定してみる。 一緒に連番でIdを生成するような関数を想定する。

import {addPost} from "./addPost"

let idCounter=0
const generateId = () => ++idCounter

const onClick=(content:stirng) => {
  const id=generateId()
  addPost({id, content})
}

はて、contentはstringになってしまい、無味乾燥な型になっている。 このstringはなんのstringだっけ?どんな意味を本来持っているんだっけ?ということになりうる。

ここでPostの型定義をしたモジュールに対して色を加えてみようと思う。 次のようなコードだ。

type Post = {
  id: number,
  content: string
};

declare namespace Post{
  export type Id = Post["id"] 
  export type Content = Post["content"] 
}

export = Post

ちょっと面倒な感じがする。 では、先ほどの無味乾燥なソースに手を加えてみようと思う。

import {addPost} from "./addPost"
import * as Post from "./model/post"

let idCounter=0
const generateId:(() => Post.Id) = () => ++idCounter

const onClick=(content: Post.Content) => {
  const id=generateId()
  addPost({id, content})
}

IdやContentの型がコードに表れており、カラフルなコードになったように思う。

さて、ここでリファクタリングのことを考える。 VSCodeのF2で出来るただのRenameのことである。

ここでPostのidの名前変えたくなった。screenIdにしたい。 F2でコードを変えるとどうなるか。

次のようになった。(まとめて書く)

type Post = {
  screenId: number,
  content: string
};

declare namespace Post{
  export type Id = Post["id"] // ここにエラーが発生する。
  export type Content = Post["content"] 
}

export = Post

// ....

import {addPost} from "./addPost"
import * as Post from "./model/post"

let idCounter=0
const generateId:(() => Post.Id) = () => ++idCounter

const onClick=(content: Post.Content) => {
  const id=generateId() // ここにエラーが発生する。
  addPost({screenId, content})
}

コメントで示したところはエラーが発生しており 手で編集する必要がある箇所が2か所発生する。

ここでは修正方法についてはそこまで難しいわけではないので解説しないが Id用の型があるおかげで、ある程度、楽に型を柔軟にメンテしやすくなるように思う。 他にいい方法があれば誰か教えていただければと思う。

Happy TypeScripting!

curlでログインしてAPIを叩く

Spring Bootで作った認証で保護されているAPIを叩く。

ログインページに入って、セッションを確立する

-c オプションでクッキーを保存する。

$ > curl -c my.cookie http://localhost:8080/login

以下みたいなクッキー(のファイル)ができるのでcatで確認

$ > cat my.cookie

# Netscape HTTP Cookie File
# http://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

localhost       FALSE   /       FALSE   0       XSRF-TOKEN      5f0854a9-e3ba-480c-91ed-e19505aecb08
#HttpOnly_localhost     FALSE   /       FALSE   0       JSESSIONID      A4D102840F355F72123DEACCDCD64941

-bオプションでできたクッキーを使ってログイン また、クッキーに書かれたXSRF-TOKENをヘッダに付与する

$ > curl -XPOST -b my.cookie http://localhost:8080/login/post -c my.cookie -H "X-XSRF-TOKEN:e76acbd1-234e-456e-970d-751e5a21c3c9"

認証で保護されているAPIを叩いてみる。

$ > curl -b my.cookie http://localhost:8080/user
// APIのレスポンス
{....}

割と泥臭い。 初めてcurlでログインからAPIまで叩いた気がする。

Spring Security入門した

はじめに

今回の記事では以下の実装を行ったのでメモ書きとして残しておきます。

  • EclipseでのSpring Loadedを使ったHot Swap
  • Thymeleaf3とSpring Securityによるフォーム認証

EclipseでのSpring Loadedを使ったHot Swap

公式に見にいくとIDEAしか書いてません。禿げた。

build.gradleに以下の設定を追加します。

apply plugin: 'eclipse'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.4.2.RELEASE'
        classpath 'org.springframework:springloaded:1.2.6.RELEASE'
    }
}

eclipse{
    classpath {
        defaultOutputDir = file("${project.buildDir}/classes/main/")
    }
}

できました。
ここで1つ問題があります。

eclipseのbuildshipプラグインを使って、gradleのプロジェクトをリフレッシュすると
なぜかクラスの生成先がbinフォルダに固定されてしまいます。

Gradle IDEを使うとgradleのeclipseコマンドが動いているようなので
期待どおりに.classpathファイルが生成されます。

ホットスワップサイコー!!!

Thymeleaf 3とSpring Securityを使う設定を追加します。

build.gradleに以下の記述を追加します。

dependencies {
    // ....
    compile 'org.springframework.boot:spring-boot-starter-security'
    compile 'org.thymeleaf:thymeleaf:3.0.0.RELEASE'
    compile 'org.thymeleaf:thymeleaf-spring4:3.0.0.RELEASE'
    compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4:3.0.0.RELEASE'
}

Spring Securityによるフォーム

まずは画面のファイルです。

src/main/resources/templates/login.htmlというパスに配置します。

<!DOCTYPE html>
<html lang="ja">

<head>
  <title></title>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  hello out friend.
  <form id="login_form" method="post" action="/login/post">
    <label>ログインID</label>
    <input type="text" id="login_id" name="login_id" placeholder="ログインIDを入力してください" autofocus="" required="" />
    <label>パスワード</label>
    <input type="password" id="login_password" name="login_password" placeholder="パスワードを入力してください" required="" />
    <input id="login_button" type="submit" value="ログイン" />
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
  </form>
</body>

</html>

th:actionとか使うと勝手にtokenが入るみたいですが
とりあえず今回は自分で書きました。
後で試します。

ここでハマったのはth:nameじゃなくてname属性で記述して
アルェーオキカワラナイゾーって一人ハマってました。

loginフォームの表示のためにtemplateを使うので
なんかアホらしいですが、Controllerを書きます。

@Controller
public class AuthController {
  @GetMapping("/login")
  public String login(Model model) {
    // テンプレート名
    return "login";
  }
}

Security周りの設定をJavaで記述します。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.csrf()
      .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    // ログインページは誰でも見れないといけない
    http.authorizeRequests()
      .antMatchers("/login")
      .permitAll()
      .anyRequest()
      .authenticated();
    // ログインはフォーム認証, ログイン成功後topに戻る
    http.formLogin()
      .loginProcessingUrl("/login/auth")
      .loginPage("/login")
      .defaultSuccessUrl("/")
      .usernameParameter("login_id")
      .passwordParameter("login_password");

    // ログアウト処理ページとその後の遷移
    http.logout()
      .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
      .logoutSuccessUrl("/login")
      .permitAll();
  }

}

application.ymlに以下の設定を追加しておきます。テスト用です。

security:
  user:
    name: test
    password: test

http://localhost:8080/にアクセスすると
http://localhost:8080/loginにリダイレクトされると思います。
loginした後はhttp://localhost:8080/に飛ばされ
http://localhost:8080/logoutにアクセスすると
もとのログインページに戻される動きが確認できると思います。

まとめ

サクッとSpring Securityに入門してみました。

この上になんか建ててみる予定です。

最後にめちゃくちゃ省略してしまったbuild.gradleを晒しておきます。

apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'com.diffplug.gradle.spotless'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

sourceCompatibility = 1.8
targetCompatibility = 1.8

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.diffplug.spotless:spotless-plugin-gradle:3.0.0'
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.4.2.RELEASE'
        classpath 'org.springframework:springloaded:1.2.6.RELEASE'
    }
}

repositories {
    mavenCentral()
}

// alias
task format(dependsOn: 'spotlessApply')
spotless {
    def headerFile = "/** "+project.file('../LICENSE.md').text+"*/"

    java {
        licenseHeader headerFile, '(package|import) '
        eclipseFormatFile project.file('eclipse-format-setting.xml')

        trimTrailingWhitespace()
        endWithNewline()
    }
}

eclipse{
    classpath {
        defaultOutputDir = file("${project.buildDir}/classes/main/")
    }
}


dependencies {
    compile 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile 'org.springframework.boot:spring-boot-starter-data-rest'
    compile 'org.springframework.boot:spring-boot-starter-security'
    
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.thymeleaf:thymeleaf:3.0.0.RELEASE'
    compile 'org.thymeleaf:thymeleaf-spring4:3.0.0.RELEASE'
    compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4:3.0.0.RELEASE'
    
    compile 'com.h2database:h2'

    testCompile 'org.springframework.boot:spring-boot-starter-test'
    testCompile 'org.jmockit:jmockit:1.21'
    testCompile 'junit:junit:4.12'
    testCompile 'org.assertj:assertj-core:3.2.0'
    
    compileOnly  'org.projectlombok:lombok:1.16.14'
    testCompileOnly   'org.projectlombok:lombok:1.16.14'
}

追記:

今回の記事ではSpringloadedを使っていますが、Spring Boot Dev Toolが強いそうです。 Application.ymlを変更したら自動でアプリが再起動しました。びっくりします。

spring-boot-devtoolsで開発効率上げようぜ、的な。 (Spring Boot 1.3) - Qiita