Github から Azure DevOps にRepositoryを移行する

1. はじめに

最近、再び自分の中のAzure熱が高まったので Github に置いているPrivate Repository(C#)をAzure DevOpsに引っ越ししてみました。

ソース管理としてGithubは素晴らしいのですが、Azure DevOpsもCI/CD含めた統合環境として(特に.NETプロジェクトでは)素晴らしいようなのでちょっと遊びとして使ってみたいなぁ、というのが移行の動機です(あ、勿論Githubでも外部サービス含め、同様のCI/CD環境は得られるけど)。

という事で、ここではサンプルとして用意したGithub RepositoryをAzure DevOpsに移行してみます。

2. 移行元のGithub

移行元のGithub Repositoryは、Private の https://github.com/ryuichi111/ToAzureDevOpsMigrateTest.git というHelloWorldな.NET Coreコンソールアプリケーションを想定します。
SourceTreeで確認した状態は以下の通り。

f:id:daigo-knowlbo:20190201003756p:plain

当該Repositoryに対しては、以下のような操作が行われた状態です。

  • message="init commit" として最初のソースをmasterにコミット/プッシュ
  • message="modified message" としてHelloWorldのコンソール出力文字を変更したソースをmasterにコミット/プッシュ
  • feature/interactiveブランチを切ってソース修正コミット/プッシュし、masterへのPRによりmerge

3. Github -> Azure DevOpsに移行する

3.1. プロジェクト作成

Azure DevOpsのホーム画面を表示します。
右上の「Create project」をクリック。

f:id:daigo-knowlbo:20190201002357p:plain

Project nameに「ToAzureDevOpsMigrateTest」と入力、Visibilityは「Private」、Version Controlは「Git」、Work item processは「Agile(まぁ、これはお好みで)」にします。
「Create」ボタンをクリックします。

f:id:daigo-knowlbo:20190201002457p:plain

3.2. Githubからインポート

プロジェクトが作成出来たら、左サイドバーから「Repos」をクリックします。
さらに、画面中央少し下の「import」をクリックします。

f:id:daigo-knowlbo:20190201002609p:plain

「Import a Git repository」ウィンドウが表示されるので、移行元のURLを「Clone URL」に、また、Username / Passwordを入力します。

f:id:daigo-knowlbo:20190201011125p:plain

3.3. GithubからPersonal Access Tokenを取得(二要素認証有効時のみ)

が・・・Githubで二要素認証を有効化している場合はパスワードで認証が通らないので、Github上で以下の操作を行って Personal Access Token を取得し、それを「Pasword / PAT」のテキストボックスに入力します。

Githubサイトに行き、「setting -> Developer settings -> Personal access tokens -> Generate new token」を選択します。

f:id:daigo-knowlbo:20190201003246p:plain

任意の説明を「Toke Description」に付け、repoへのスコープを付けて、画面下にある「Generate token」ボタンをクリックします(以下のキャプチャでは下のボタンは映っていませんが)。

f:id:daigo-knowlbo:20190201002946p:plain

トークンが生成されたのでコピーします。

f:id:daigo-knowlbo:20190201003429p:plain

Azure DevOps側の「Password / PAT」にトークンをペーストして「Import」ボタンをクリックします。

3.3. クローン中。。。

f:id:daigo-knowlbo:20190201003553p:plain

Github -> Azure DevOpsへのCloneが走って・・・

f:id:daigo-knowlbo:20190201003611p:plain

3.4. Azure DevOps上のRepositoryを確認

Azure DevOpsのRepo上でHistoryを確認してみましょう。

f:id:daigo-knowlbo:20190201003707p:plain

Commit / PR mergeの履歴含めてきれいに移行出来ました。

4. まとめ

GithubがMS傘下になり、これまでGithubに置いていた資産をAzure DevOpsに移行しても、なんだか変な安心感を覚えるように個人的にはなりました^^;;;
まぁ、少なくとも.NETプロジェクトに関してはAzure DevOpsに乗っかっていた方がCI/CDなど含め諸々のメリットが享受できそうな気がしています。
一昔前のMSであればお金を払わないと何もできなかった感じですが、最近のMSならこのAzure DevOpsに関しても個人としての入り口部分では無料でいろいろできますし。

ということで、僕はプライベートではAzure DevOpsを楽しんでいこうかと思います!(仕事はGithubだからいい感じにバランス取れてるでしょ)

ASP.NET Core 2.2 + JWTのサンプル(AccessToken/RefreshToken利用)

ASP.NET Core 2.2(WebAPI) と JWTを使った、認証付きWebAPIの実装を↓↓↓に置きました。

github.com

雑実装ですがAccessTokenとRefreshTokenに対応しています。

※説明ブログは後程。。。書く予定。。。

EntityFramework Core 2.2 + Cosmos DB ~ ファーストステップ

1. はじめに

2018年10月(?)あたりからPreview版とはいえ、EntityFramework CoreからCosmos DBにアクセスするプロバイダが提供されていたという事で試してみました。
データの保存と読み込みを行うだけの超基本となるファーストステップの記事になります。

使用した環境

2. こんな事をするよ

すごく単純に EF Core Cosmos DB Provider を使って単純なモデルクラスの保存と読み込みを行います。

3. Cosmos DBの作成

Azureポータルの「+リソースの作成」からCosmos DBを作成します。

f:id:daigo-knowlbo:20190108015649p:plain

f:id:daigo-knowlbo:20190108015756p:plain

4. 実装

以下順に。

4.1. プロジェクト作成

新規プロジェクトを作成します。
コンソール アプリ(.NET Core)

f:id:daigo-knowlbo:20190108012847p:plain

プロジェクト名は「EfCoreCosmosExamConsole」

f:id:daigo-knowlbo:20190108012915p:plain

4.2. Nuget追加

Nugetパッケージで「microsoft.EntityFrameworkcore.Cosmos」を追加します。
プレリリース版を含めるにして検索してインストールします。

f:id:daigo-knowlbo:20190108013743p:plain

4.3. モデルクラス / DbContextクラス追加

Cosmos DBに保存するデータモデルクラスを追加します。
FamilyクラスとPersonクラスとします(家族情報を保存するイメージ)。
※手抜きして Models/Family.cs の1ファイルに2つのクラスを定義しています。

// Models/Family.cs
using System;
using System.Collections.Generic;

namespace EFCoreCosmosExamConsole.Models
{
  public class Family
  {
    public Guid FamilyId { get; set; }

    public Person HeadOfHousehold { get; set; }

    public Person Partner { get; set; }

    public List<Person> Children { get; set; }
  }

  public class Person
  {
    public Guid PersonId { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public DateTime Birth { get; set; }
  }
}

FamilyクラスがPersonクラスを世帯主(HeadOfHousehold )、パートナー(Partner)、子供(Children)として保有しています。

DbContextクラスを作成します。
RDBに対するEF Coreの時とほぼ同様の感じです。

// Models/PeopleContext.cs
using Microsoft.EntityFrameworkCore;

namespace EFCoreCosmosExamConsole.Models
{
  public class PeopleContext : DbContext
  {
    public DbSet<Family> Families { get; set; }

    public DbSet<Person> Persons { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
      optionsBuilder.UseCosmos(
        "https://ryuichi111cosmos.documents.azure.com:443/",
        "ひみつひみつひみつひみつひみつひみつひみつひみつ",
        "PeopleDatabase"
      );
    }
  }
}

Cosmos DBプロバイダ固有の設定は「OnConfiguring()」での「UseCosmos()」呼び出しになります。
UseCosmos()メソッドは、Microsoft.EntityFrameworkCore.CosmosアセンブリMicrosoft.EntityFrameworkCore.CosmosDbContextOptionsの拡張メソッドとして定義/実装されています。
第1引数には接続先Cosmos DBのURL、第2引数には接続キー、第3引数には(任意の)データベース名を指定します。
「第1引数 接続先Cosmos DBのURL」「第2引数 接続キー」はAzureポータルの対象Cosmos DBのKeysで確認することができます。

f:id:daigo-knowlbo:20190108014227p:plain

ここまでで、ソリューションエクスプローラ的には↓↓↓こんな感じになる。

f:id:daigo-knowlbo:20190108013030p:plain

4.4. main()からDbContext呼び出し

Program.cs main()にCosmos DBへのデータ保存と読み込みを記述します。
RDBに対するEF Core実装とほぼ同じです。

// Program.cs
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using EFCoreCosmosExamConsole.Models;

namespace EFCoreCosmosExamConsole
{
  class Program
  {
    static void Main(string[] args)
    {
      // テスト用の家族オブジェクトを作成(世帯主+パートナー+子供×2)
      Person takashi = new Person() { PersonId = Guid.NewGuid(), FirstName = "Takashi", LastName = "Tanaka", Birth = new DateTime(1985, 4, 5) };
      Person sawako = new Person() { PersonId = Guid.NewGuid(), FirstName = "Sawako", LastName = "Tanaka", Birth = new DateTime(1983, 7, 12) };
      Person chiyori = new Person() { PersonId = Guid.NewGuid(), FirstName = "Chiyori", LastName = "Tanaka", Birth = new DateTime(2001, 10, 4) };
      Person mamoru = new Person() { PersonId = Guid.NewGuid(), FirstName = "Mamoru", LastName = "Tanaka", Birth = new DateTime(2002, 11, 20) };
      Family family = new Family()
      {
        FamilyId = Guid.NewGuid(),
        HeadOfHousehold = takashi,
        Partner = sawako,
      };
      family.Children = new List<Person>();
      family.Children.Add(chiyori);
      family.Children.Add(mamoru);

      // CosmosDBに保存
      using (var context = new PeopleContext())
      {
        // Database / Collectionを(無ければ)作成
        context.Database.EnsureCreated();

        // Familyをコンテキストに追加
        context.Families.Add(family);

        // SaveChanges()の裏側でオブジェクトをJSON変換、シャドウプロパティの追加、CosmosDBへの保存、が行われる
        context.SaveChanges();
      }

      // CosmosDBから読み込み
      using (var context = new PeopleContext())
      {
        // Familyを関連オブジェクトごと取得
        var loladedFamily = context.Families
          .Include(f => f.HeadOfHousehold)
          .Include(f => f.Partner)
          .Include(f => f.Children)
          .Where(f => f.FamilyId == family.FamilyId).FirstOrDefault();

        // Personを単独で取得
        var loadedSawako = context.Persons
          .Where(p => p.PersonId == sawako.PersonId).FirstOrDefault();
      }
    }
  }
}

テスト用に Family / Person モデルクラスの作成

Cosmos DBに保存するテスト用のモデルクラスはべた書きで作成しています。

Cosmos DBの初期化

AzureポータルからCosmos DBの入れ物は作成済みですが、それに紐づく「データベース」「コレクション」がまだ作成されていません。
以下の呼び出しを行うと「データベース」「コレクション」が存在しなければ作成してくれます。

context.Database.EnsureCreated();

ちなみにEnsureCreated()呼び出し直後に、Azureポータル Data Explorer で確認した状態は以下です。

f:id:daigo-knowlbo:20190108020029p:plain

データベース名=PeopleDatabase(PeopleContextにおいてUseCosmos()の第3引数で指定した名称)
コレクション名=PeopleContext(PeopleContextのクラス名)
Throughput(RU) = 400

Familyオブジェクトの保存

以下の呼び出しでCosmos DBにデータを保存することができます(RDBに対するEF Coreと全く同じ)。

context.Families.Add(family);
context.SaveChanges();

以下のように、同一のコレクション(PeopleContext)内に Family / Person オブジェクトが保存されます。

↓↓↓Family f:id:daigo-knowlbo:20190108020510p:plain

↓↓↓Person f:id:daigo-knowlbo:20190108020536p:plain

↓↓↓Person f:id:daigo-knowlbo:20190108020601p:plain

EF Core側の実装における DbContextの括りでコレクションが作成され、そこに対象DbContextが扱うオブジェクトが保存されます。
Cosmos DBにおいては「コレクション=RUの括り(=コスト/パフォーマンス)」であるため、実運用上はコストとパフォーマンスの兼ね合いでDbContextのくくりを検討することになると思います。

それから Family / Person クラスで定義していないプロパティが多数Cosmos DB側のJsonデータには含まれていますが、これらはCosmos DB側で保持するシャドウプロパティになります。

Familyオブジェクト / Personオブジェクトの読み込み

データの読み込みもRDBに対するEF Coreと全く同様です。

// Familyを関連オブジェクトごと取得
var loladedFamily = context.Families
  .Include(f => f.HeadOfHousehold)
  .Include(f => f.Partner)
  .Include(f => f.Children)
  .Where(f => f.FamilyId == family.FamilyId).FirstOrDefault();

// Personを単独で取得
var loadedSawako = context.Persons
  .Where(p => p.PersonId == sawako.PersonId).FirstOrDefault();

まとめ

EF Core Cosmos DB Providerは、RDB操作と非常に類似した(同様の)コードでCosmos DBへのアクセスが可能でいい感じですね。
かつてLINQがデータソースに依存しない(オンメモリオブジェクトであろうがRDBデータであろうが)プログラミングモデルを目指し、実現しましたが、そんなテイストで 相手がRDBであろうがCosmos DBであろうが同様のプログラムコードが書けるのはうれしいです。
もちろんアーキテクチャ的にはバックエンドの技術知識を持つ必要がありますが、すごく期待できるデータプロバイダな気がしました^^

サンプルコードは一応↓↓↓↓↓です。 github.com

ASP.NET Core 2.2 WebAPI で Pagination対応 する

1. はじめに

ASP.NET Core WebAPIにおいて、pagination(ページング)でJSONデータを返す実装のメモです。
(ググれば既出だけど、意外に情報少なめだったので、自分メモの意味も込めて)

開発環境

※ VS2017でもCore2.xでも同じだと思う。

2. こんな実装をする

pagination対応するときは、主に以下の2つがあります。

  • body要素のjsonにページ番号や全体件数を含ませる
  • body要素には本来のデータのみを含ませ、HTTP Response Headerにページ情報や全体件数を含ませる

ここでは後者の実装を行います。

社員情報をpaginationで5件ずつ取得することができるAPIを想定します。

GET /api/employee?page=10

↑↑↑とすると
↓↓↓が帰るみたいな

[
  {
    "id": 45,
    "firstName": "しゃいん",
    "lastName": "45号"
  },
  {
    "id": 46,
    "firstName": "しゃいん",
    "lastName": "46号"
  },
  {
    "id": 47,
    "firstName": "しゃいん",
    "lastName": "47号"
  },
  {
    "id": 48,
    "firstName": "しゃいん",
    "lastName": "48号"
  },
  {
    "id": 49,
    "firstName": "しゃいん",
    "lastName": "49号"
  }
]

3. 実装コード

注意)以下のコードは、paginationに関する部分のみに集中したコードです。DBもEFもクラスモデリングも無視でなるべく簡易な実装にしているので、クラス構成・メソッドの抽出等々は無視したコードです。

3.1. モデルクラス Employee。

// Models/Employee.cs
namespace PaginationExam.Models
{
  public class Employee
  {
    public int ID { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }
  }
}

3.2. Daoクラス

Employeeを取得するDaoクラス。(ダミーリストデータをメモリ内に作ってそれをLINQでpaginationして返すだけの実装)

// Dao/EmployeeDao.cs
using System.Linq;
using System.Collections.Generic;
using PaginationExam.Models;
using System;

namespace PaginationExam.Dao
{
  public class EmployeeDao
  {
    private List<Employee> _dummyEmployeeData = new List<Employee>();

    public EmployeeDao()
    {
      for (int i = 0; i < 100; i++)
      {
        this._dummyEmployeeData.Add(
          new Employee()
          {
            ID = i,
            FirstName = "しゃいん",
            LastName = i.ToString() + "号"
          });
      }
    }

    public (int totalItemCount, int lastPage, List<Employee>) GetEmployees(int page, int countPerPage)
    {
      int totalItemCount = 0;
      int lastPage = 0;
      List<Employee> employees = null;

      employees = this._dummyEmployeeData.Skip(countPerPage * (page - 1)).Take(countPerPage).ToList();
      totalItemCount = this._dummyEmployeeData.Count();

      lastPage = (int)Math.Floor((decimal)totalItemCount / countPerPage);
      if (totalItemCount % countPerPage > 0)
        lastPage++;

      return (totalItemCount, lastPage, employees);
    }
  }
}

3.3. コントローラクラス

Web API のコントローラクラス。 「GET api/Employee?page=xx」を受け付けます。
1ページ当たり5件は固定です。
pagination用のHTTP Response Headerは以下の3つを追加しています。

Links

以下の4つのリンク先を示します。
first - 1ページ目のURI prev - 前のページのURI
next - 次のページのURI
last - 最後のページのURI

X-TotalItemCount

「X-」なのでカスタムヘッダです。
Employeeの全件数を「X-TotalItemCount」に設定しています。

X-CurrentPage

現在のページを設定しています。

※「X-」カスタムヘッダは非推奨とされましたが、個人的にはその経緯から使ってOKじゃね?との認識。

// Controllers/EmployeeController.cs
using Microsoft.AspNetCore.Mvc;
using PaginationExam.Dao;
using PaginationExam.Models;
using System.Collections.Generic;

namespace PaginationExam.Controllers
{
  [Route("api/[controller]")]
  [ApiController]
  public class EmployeeController : Controller
  {
    // 1ページに5件
    const int CountPerPage = 5;

    [HttpGet]
    public IEnumerable<Employee> GetList([FromQuery] int page)
    {
      if (page < 1) // 1ページスタートなので、1未満は1に簡単に補正
        page = 1;

      // データ取得
      // 全件数、該当ページのEmployeeリスト
      var dao = new EmployeeDao();
      (int totalItemCount, int lastPage, List<Employee> employees) = 
        dao.GetEmployees(page, EmployeeController.CountPerPage);

      // Response Header追加
      this.Response.Headers.Add("Links", this.CreateLinksHeader("Employee", page, lastPage));
      this.Response.Headers.Add("X-TotalItemCount", totalItemCount.ToString());
      this.Response.Headers.Add("X-CurrentPage", page.ToString());

      // Body Jsonは本来のデータのみ
      return employees;
    }

    /// <summary>
    /// Pagination用のLinkヘッダ値を作成
    /// </summary>
    /// <param name="controller"></param>
    /// <param name="currentPage"></param>
    /// <param name="lastPage"></param>
    /// <returns></returns>
    protected string CreateLinksHeader(string controller, int currentPage, int lastPage)
    {
      List<string> links = new List<string>();

      links.Add(string.Format("<{0}>; rel=\"first\"", this.Url.Link("", new { Controller = controller, page = 1 })));
      if (currentPage > 1)
      {
        links.Add(string.Format("<{0}>; rel=\"prev\"", this.Url.Link("", new { Controller = controller, page = currentPage - 1 })));
      }
      if (currentPage < lastPage)
      {
        links.Add(string.Format("<{0}>; rel=\"next\"", this.Url.Link("", new { Controller = controller, page = currentPage + 1 })));
      }
      links.Add(string.Format("<{0}>; rel=\"last\"", this.Url.Link("", new { Controller = controller, page = lastPage })));

      return string.Join(", ", links);
    }
  }
}

4. 実行

実行します。
Postmanを起動して「GET https://localhost:44332/api/employee?page=10」をSendした結果は以下です。

BodyのJSONには本来のデータ(Employeeリスト)のみ。

f:id:daigo-knowlbo:20190106124027p:plain

Response Headerには Link / X-TotalItemCount / X-CurrentPage が返却されている。

f:id:daigo-knowlbo:20190106124106p:plain

5. まとめ

ASP.NET Core 2.2 でのpaginationについての一実装例でしたm( )m
ソースは以下に置いてあります。

github.com

あと、paginationに関する説明や検討事項は以下なんかが結構参考になると思います。

  1. 翻訳: WebAPI 設計のベストプラクティス - Qiita

  2. qiita.com

  3. RFC 5988 - Web Linking

OpenAPI Generator + golang + Flutter でアプリ開発

1. はじめに

以下のような構成のスマホアプリを作ってみようと思います。

f:id:daigo-knowlbo:20181103213543p:plain

一般的な、HTTP経由でサーバと通信するタイプのスマホアプリです。

で、タイトルの通り「OpenAPI 3.0でWebAPIを定義」し「golangでWebAPIを実装」し「Flutterでスマホアプリを実装」する、ということをしたいと思います。
また、OpenAPI 3.0定義からサーバ及びクライアントコードを自動生成するために OpenAPI Generator を使用することとします。

github.com

今回利用した環境  

macOS Mojave 10.14
Flutter 0.8.2
go 1.10.3
openapi-generator 3.3.2

2. 準備

Flutter / go 環境は構築済みの前提とします。
openapi-generatorは以下のbrewコマンドでインストールします。

$ brew install openapi-generator

3. 作る!

大まかな流れは以下になります。

  • OpenAPI 3.0 specでAPI仕様を定義
  • OpenAPI Generatorでgolangのserverコードを自動生成
  • OpenAPI Generatorでdartのclientコードを自動生成
  • Flutterアプリを作成(dart clientを利用してgolang serverを呼び出す)

では順番に進めていこうと思います。
まずは、アプリ開発用のディレクトリを作成します。

$ mkdir oas3_go_flutter_exam
$ cd oas3_go_flutter_exam

3.1. OpenAPI 3.0 specを作成

OpenAPI定義yamlを作成します。

ファイル名は「employee_api.yml」とします。
idを指定してemployee情報を取得するWebAPIの定義になります。
Employeeは id / firstName / lastName / salary の属性を持ったオブジェクトとします。

openapi: "3.0.0"
info:
  version: 1.0.0
  title: oas3_go_flutter_exam Employee
  license:
    name: MIT
servers:
  - url: http://localhost:8080/api/
paths:
  /employee:
    get:
      summary: get employee information
      operationId: getEmployee
      tags:
        - employee
      parameters:
        - name: employeeId
          in: query
          description: query target employee id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: return employee information
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Employee"
components:
  schemas:
    Employee:
      required:
        - id
        - firstName
        - lastName
        - salary
      properties:
        id:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        salary:
          type: integer
          format: int64

3.2. yamlの妥当性をチェック

定義した employee_api.yml にエラーがないか、以下のコマンドでチェックします。

$ openapi-generator validate -i employee_api.yml

Validating spec (employee_api.yml)
No validation issues detected.

3.3. golangのserverコードを自動生成

以下のコマンドでgolangのserverコードを自動生成します。

$ openapi-generator generate -i employee_api.yml -g go-server -o ./server

[main] WARN  o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
[main] INFO  o.o.c.languages.AbstractGoCodegen - Environment variable GO_POST_PROCESS_FILE not defined so Go code may not be properly formatted. To define it, try `export GO_POST_PROCESS_FILE="/usr/local/bin/gofmt -w"` (Linux/Mac)
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/model_employee.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/api_employee.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/api/openapi.yaml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/main.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/Dockerfile
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/routers.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/logger.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/README.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/.openapi-generator-ignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/.openapi-generator/VERSION

モデルクラス(model_employee.go)やルート定義(routers.go)、APIのカラ定義(api_employee.go)など一通りのgo実装が自動生成されました。

3.4. goのWebAPIを実装

自動生成されたコードにオリジナルの実装を追加します。

api_employee.goファイル の GetEmployee() に Employeeオブジェクト を返却する実装を以下のように追加します。
(本来はDBアクセスなどを行った結果を返すと思います)

package openapi

import (
    "encoding/json"
    "net/http"
)

// GetEmployee - get employee information
func GetEmployee(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    w.WriteHeader(http.StatusOK)

    employeeID := r.URL.Query()["employeeId"]

    employee := Employee{
        Id:        employeeID[0],
        FirstName: "ryuichi " + employeeID[0],
        LastName:  "daigo " + employeeID[0],
        Salary:    12000000}
    json.NewEncoder(w).Encode(employee)
}

3.5. go serverを実行

WebAPIがうまく動作するか実行してみましょう。

$ go run main.go

curlでもブラウザでも何でも良いですが、以下のURLにGETを投げてみます。

http://localhost:8080/api/employee?employeeId=10

{"id":"10","firstName":"ryuichi 10","lastName":"daigo 10","salary":12000000}

golang serverのWebAPIが正しく動作していることを確認できました。

3.6. dartのclientコードを自動生成

以下のコマンドでdartのclientコードを自動生成します。

$ openapi-generator generate -i employee_api.yml -g dart -DbrowserClient=false -o ./client

[main] WARN  o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
[main] INFO  o.o.c.languages.DartClientCodegen - Environment variable DART_POST_PROCESS_FILE not defined so the Dart code may not be properly formatted. To define it, try `export DART_POST_PROCESS_FILE="/usr/local/bin/dartfmt -w"` (Linux/Mac)
[main] INFO  o.o.c.languages.DartClientCodegen - Dart version: 2.x
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/lib/model/employee.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/docs/Employee.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/lib/api/employee_api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/docs/EmployeeApi.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/pubspec.yaml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_client.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_exception.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_helper.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/authentication.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/http_basic_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/api_key_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/oauth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/git_push.sh
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.gitignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/README.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.openapi-generator-ignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.openapi-generator/VERSION

※ 上記のように「-DbrowserClient=false」オプションを付けないとFlutterでコンパイルできない dart:html に依存したコードが生成されてしまうので注意。

dartによりWebAPI呼び出しを行うコード、また送受信するデータのモデルクラスなど一通りの実装が自動生成されました。

3.7. Flutterアプリを作成

以下のコマンドでFlutterアプリを作成します。

 $ flutter create flutter_app

先程自動生成した dart client を利用して golang server にアクセスするために pubspec.yamldart client への依存定義を追加します。

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2

  openapi:
    path: ../client/

※ 上記定義の下2行

main.dartを修正します。
IDを入力し、ボタンを押すとWebAPI呼び出しが行われ、結果を画面にテキスト表示するようにします。

// main.dart

import 'package:flutter/material.dart';
import 'package:openapi/api.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter with OpenAPI Generator'),
    );
  }
}
 
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  String _employeeName = '';
  String _employeeId = '';

  void _callWebApi() {    
    var client = new EmployeeApi();
    var result = client.getEmployee(this._employeeId);
    result.then(
      (employee) => setState(() { this._employeeName = employee.firstName + ' ' + employee.lastName; } )
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text('pleas input employee id'),
            new TextField(
              onChanged: (v) => this._employeeId = v,
            ),
            new RaisedButton(
              child: new Text('call WebAPI'),
              onPressed: _callWebApi,
            ),
            new Text(
              this._employeeName,
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
    );
  }
}

3.8. 実行

では実行してみます。

./serverディレクトリで以下のコマンドによりgolang serverを起動しておきます。

go run main.go

そしてFlutterアプリを起動します。

f:id:daigo-knowlbo:20181103211906p:plain

IDを入力し、call WebAPIボタンをタップします。

f:id:daigo-knowlbo:20181103212648p:plain

WebAPI呼び出しが行われEmployeeオブジェクトの取得&表示が行われました!

4. まとめ

OpenAPI Generator を使うとモデルを含めたテンプレート実装が一瞬のうちに生成されかなりいい感じに開発が進められるんじゃないかと思いました。サクサクっと必要な定形コードが自動生成されるので、ドメイン領域への集中力アップにも勿論効果的ですね。
OpenAPI 3.0によるAPI定義を明確に行った上で、サービスを実装していくスタイルも(まあSwagger時代から何年も行われていることではありますが)クリーンにプロジェクトが保たれていいと思います。

今回作ったコードは以下に置いておきました。

github.com

Flutter - json_serializableを理解するメモ

1. はじめに

Flutterでアプリ作ろうと思ったら多くの場合、サーバ側のWebAPIと通信することが想定されます。

HTTP越しの通信でデータの受け渡しをする、つまりjsonデータのやり取りが必要→アプリではjson文字列ではなくモデルクラスのようなオブジェクトでデータを扱いたい→サーバに送り返すときにはモデルオブジェクトを再びjsonに変換して送りたい。。。

つまり json <-> object の serialize / deserialize ですね。
公式ページだと「json_serializable」と「built_value」が紹介されていて、特別に新しい技術でもないので、既に色々な方がブログっております。

が、まあ自分用という意味でもメモブログしておきます。

今回使った環境は、、、
OS: Windows 10 x64
Flutterバージョンは以下の通り。

$ flutter --version

Flutter 0.9.4 • channel beta • https://github.com/flutter/flutter.git
Framework • revision f37c235c32 (5 weeks ago) • 2018-09-25 17:45:40 -0400
Engine • revision 74625aed32
Tools • Dart 2.1.0-dev.5.0.flutter-a2eb050044

2. json_serializableとは

Flutter(というかDart)で json文字列<->オブジェクト の serialize / deserialize を楽に実装するためのライブラリです。
では早速、使ってみようかと。

3. 使ってみよう

以下のようなロジックを実装してみます。

上記ロジックをFlutterアプリ内で実装し、print()関数でコンソールに出力します。
(なのでFlutterアプリですが、UI無視のサンプルとします)

3.1. flutterプロジェクトを作成

以下のコマンドで「json_example」という名前のFlutterプロジェクトを作成します。

$ flutter create json_example

3.2. 依存パッケージ追加

json_serializableを使うために「json_serializable」「json_annotation」「build_runner」の3つのパッケージを「pubspec.yaml」定義に追加します。

...省略...

dependencies:
  flutter:
    sdk: flutter
  json_annotation: ^2.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  json_serializable: ^2.0.0
  build_runner: ^1.0.0

...省略...

各パッケージの概要は以下の通り:
json_serializable : json serialize / deserializeを行うパッケージ
json_annotation: json_serializableで使用されるアノテーションを定義するパッケージ
build_runner: 「json serialize / deserializeを行うためのコード」の自動生成を行うためのビルドランナー

以下のコマンドでパッケージを取得します。(VS Code+pluginやandroid Studio環境を使っていればIDEが自動で実行)

$ flutter packages get

3.3. モデルクラス作成

Employeeクラスを作成します。

// ./lib/employee.dart ファイル

import 'package:json_annotation/json_annotation.dart';

part 'employee.g.dart';

@JsonSerializable()
class Employee {
  Employee(this.id, this.firstName, this.lastName);

  int id;
  String firstName;
  String lastName;
  
  factory Employee.fromJson(Map<String, dynamic> json) => _$EmployeeFromJson(json);
  Map<String, dynamic> toJson() => _$EmployeeToJson(this);
}

モデルクラスは以下のような定型フォーマットで記述します。上記Employeeもこのフォーマットに従っています。
※xyzおよび【】部分が作成するモデルクラス名。
※別途メソッド定義を追加するのは自由。
※サンプルでは扱っていないが、アノテーションなども適時付けられる。

// ./lib/【xyz】.dart ファイル

import 'package:json_annotation/json_annotation.dart';

part '【xyz】.g.dart';

@JsonSerializable()
class 【Xyz】 {
  【Xyz】(【プロパティ定義を並べる】);

  【プロパティ定義を並べる】
  ...
  
  factory 【Xyz】.fromJson(Map<String, dynamic> json) => _$【Xyz】FromJson(json);
  Map<String, dynamic> toJson() => _$【Xyz】ToJson(this);
}

3.4. serialize / deserializeコードを自動生成

以下のコマンドを実行するとモデルクラスに対応した自動生成コードが生成されます。

$ flutter packages pub run build_runner build

※ 「flutter packages pub run build_runner watch」とすると
モデルクラスの変更を監視してファイル編集の度に自動生成が実行されます。

本サンプルでは「employee.dart」に対応した「employee.g.dart」ファイルが自動生成されます。
何に対して自動生成コードが作成されるかというと「@JsonSerializable()」アノテーションが付与されているクラスに対して生成されます。
自動生成された ./lib/employee.g.dart は以下の通りです。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'employee.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Employee _$EmployeeFromJson(Map<String, dynamic> json) {
  return Employee(json['id'] as int, json['firstName'] as String,
      json['lastName'] as String);
}

Map<String, dynamic> _$EmployeeToJson(Employee instance) => <String, dynamic>{
      'id': instance.id,
      'firstName': instance.firstName,
      'lastName': instance.lastName
    };

ポイント①$EmployeeFromJson() / $EmployeeToJson()

自動生成されたコードには、$EmployeeFromJson() / $EmployeeToJson() の2つのメソッドが定義されています。
中身をよく見ると、、、
$EmployeeFromJson()は、Mapオブジェクトから「id」「firstName」「lastName」の各要素を取り出してEmployeeオブジェクトを作成する実装(つまり、json文字列 -> Employeeの処理)。
$EmployeeToJson()は、Employeeオブジェクトから各プロパティ要素「id」「firstName」「lastName」を取り出してMapオブジェクトを作成する実装(つまり、Employee→Mapの処理)。
になっています。

ポイント②part of 'employee.dart';

自動生成コードの先頭部分を見てみると「part of 'employee.dart';」と記述されています。
「part of」はdart言語仕様のキーワードです。
簡単に言うと「自分は employee.dart の一部ですよ」と定義しています。

ポイント③もう一度 employee.dart を振り返る

前述の employee.dart を以下に再掲します。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'employee.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Employee _$EmployeeFromJson(Map<String, dynamic> json) {
  return Employee(json['id'] as int, json['firstName'] as String,
      json['lastName'] as String);
}

Map<String, dynamic> _$EmployeeToJson(Employee instance) => <String, dynamic>{
      'id': instance.id,
      'firstName': instance.firstName,
      'lastName': instance.lastName
    };
part 'employee.g.dart'; 定義

employee.dart では定型記述として「part 'employee.g.dart';」と定義していました。
dart言語の「part」キーワード定義であり、つまり「employee.g.dartは自分の子(パート)ですよ」という意味になります。

$EmployeeFromJson(json) / $Employee_ToJson(this)の利用

さらに、「Employee.fromJson()メソッドの定義実装で$EmployeeFromJson(json)」を呼び出し、「toJson()メソッド定義実装で$Employee_ToJson(this)」を呼び出していました。

つまり、モデルクラスは「自動生成されるコードに合わせた形」で定義していたわけです。
上記を理解するとコードのロジックがすべてクリアに見渡せるようになると思います。

3.5. Employeeクラスをserialize / deserializeしてみる

では main.dart からEmployeeクラスのserialize / deserializeを行ってみます。

// ./lib/main.dart

import 'package:flutter/material.dart';
import 'dart:convert';
import 'employee.dart';

void main() {
  String orgJson = '{"id":1000,"firstName":"ryuichi","lastName":"daigo"}';
  print('オリジナルJSON文字列: $orgJson');
  
  var mapEmployee = json.decode(orgJson);
  var employee = Employee.fromJson(mapEmployee);
  print('JSON→Employeeデシリアライズしたオブジェクト: id: ${employee.id}, firstName: ${employee.firstName}, lastName: ${employee.lastName}');

  var serializedJson = json.encode(employee.toJson());
  print('再度Employee→JSONシリアライズした文字列: $serializedJson');

  runApp(new MyApp());
}

...省略...

実行結果(コンソール出力)は以下の通り。

$ flutter run

...省略...
I/flutter (28581): オリジナルJSON文字列: {"id":1000,"firstName":"ryuichi","lastName":"daigo"}
I/flutter (28581): JSON→Employeeデシリアライズしたオブジェクト: id: 1000, firstName: ryuichi, lastName: daigo
I/flutter (28581): 再度Employee→JSONシリアライズした文字列: {"id":1000,"firstName":"ryuichi","lastName":"daigo"}
...省略...

3. まとめ

json_serializableについては、初見では、なんか良く分からん定型的な形式でモデルクラスを定義し、ジェネレータを使うといい感じに json serialize / deserialize 可能なパッケージというモヤモヤ感を持っていました。
ただ上述ように、ジェネレートされた僅かなコードを覗くことで、すべてが一本の線につながって気持ちよくなるかと思います。

最後に公式のリンクを4つ貼っておきます。

↓ Flutter公式のjson serialize説明 flutter.io

json_serializableパッケージ pub.dartlang.org

json_annotationパッケージ pub.dartlang.org

↓ build_runnerパッケージ pub.dartlang.org

FlutterのHello Worldを超深掘った話

1. はじめに

Flutter初学者の ryuichi111std です。
以前はXamarin派だったのですが、最近はFlutterに面白さを感じてチョロチョロと学習しています(別にXamarinが嫌いになったわけではないです)。
なので、「Flutterとはこうだ!」みたいな知識はまだ持っていないのですが、その中で理解した知識をまとめておこうと思います。

ということで、まずは画面(ページ)作成の概念についてまとめようと思ったのですが・・・Hello World実装に対して「なぜ?なぜ?病」を発症してしまったので、それについて記録しようかと。。。

(注意) なぜ?なぜ?を深掘った内容なので、とにかく動くものを作る知識が欲しいんだ、という方にはおすすめしない記事ですm(_ _)m

使用環境:Mac Book Pro(High Sierra) / Flutter 0.7.3 / Visual Studio Code

2. Hello World

公式サイトにも載っていますが、まずは「Hello World」を作ります。
以下のコマンドで hello_world プロジェクトを生成。

$ flutter create hello_world

Visual Studio Codeで hello_world を開きます。(Android Studioでもお好みで)
「lib/main.dart」に、結構、はじめに見るには重めのテンプレサンプルが生成されるので、実装をざっくり削除して以下の実装に書き換えます。

// lib/main.dart

import 'package:flutter/widgets.dart';

void main() {
  Text text = new Text(
    "Hello World", 
    textDirection: TextDirection.ltr,);
  Center center = new Center(child: text);
  runApp(center);
}

実行画面は以下のようになります。

f:id:daigo-knowlbo:20180915172241p:plain

2.1. main()関数

多くの開発言語・環境と同じく main()関数 がエントリーポイントになります。

2.2. Text / Centerの作成

Text と Center が「Hello Flutter」と画面に表示するためのUI要素の定義になります。
Flutterでは、これらは共に「Widget」と呼ばれます。

Textオブジェクトの生成には、第1引数で表示する文字列を、オプション引数 textDirection にはテキストの表示方向を指定します(ltr=左から右)。
サンプルとしてはtextDirectionが余分に感じますが、Textオブジェクトでは指定必須のオプションパラメータになります(指定しないと実行時エラーが発生)。

Centerオブジェクトは、名前の通り子要素をセンターに配置する役割を持ちます。コンストラクタで、childプロパティにTextオブジェクトを指定します。

※ 最小実装の Hello World であれば、Textオブジェクトのみで実装することが出来ますが、その場合、画面左上にちょろっとテキストが表示される残念な状態になります(iPhone Xのような角が丸いタイプの画面だと角に文字がはみ出た状態にもなります)。

2.3. runApp()関数

runApp()関数は、引数に Widget オブジェクトを取り、そのWidgetをルートWidgetとして、それを画面いっぱいに満たすようにしてFlutterアプリケーションを実行します。

※ runApp()の定義は https://docs.flutter.io/flutter/widgets/runApp.html で確認することが出来ます。

2.4. import 'package:flutter/widgets.dart'

外部で定義されたオブジェクトを利用するためのインポート文です。
公式の HelloWorld だと「import 'package:flutter/material.dart'」が行われています。
ただ、最低限のimportでは本サンプルで使用した「import 'package:flutter/widgets.dart'」になります。
Text / Center / TextDirection / runApp を利用するために必要なimportなのですが・・・

2.4.1. Textクラス

textは、Flutterの実装では「flutter/packages/flutter/lib/src/widgets/text.dart」で実装されています。

https://github.com/flutter/flutter/blob/1ad538e454c77496fbd068b9e8b5f8b61c2f6d96/packages/flutter/lib/src/widgets/text.dart#L214

Hello Worldソースから辿ると・・・
main.dart から import した flutter/widget.dart は「export 'src/widgets/text.dart';」という実装を持ちます。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/widgets.dart

Text.dartは、「class Text extends StatelessWidget { }」という形で Textクラス を実装しています。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/text.dart

2.4.2. Centerクラス

Centerは、Flutterの実装では「Futter/packages/flutter/lib/src/widgets/basic.dart」に実装されています。

https://github.com/flutter/flutter/blob/1ad538e454c77496fbd068b9e8b5f8b61c2f6d96/packages/flutter/lib/src/widgets/basic.dart#L1541

Hello Worldソースから辿ると・・・
main.dart から import した flutter/widget.dart は「export 'src/widgets/basic.dart';」という実装を持ちます。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/widgets.dart

Basic.dartは、「class Center extends Align { }」という形で Centerクラス を実装しています。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/basic.dart

ちなみに Centerクラスの基本クラス Align は、同じくbasic.dartファイル内で「class Align extends SingleChildRenderObjectWidget { }」と定義されています。
(更に深掘ると Center <- Align <- SingleChildRenderObjectWidget(framework.dart) <- RenderObjectWidget(framework.dart) <- Widget(framework.dart) というクラス継承構造になっています。)

2.4.3. TextDirection列挙

TextDirectionは、Flutterの実装ではFlutter.engine側の engine/lib/ui/text.dart で実装されています(enum TextDirection { })。

https://github.com/flutter/engine/blob/master/lib/ui/text.dart

Hello Worldソースから辿ると・・・
main.dart から import した flutter/widget.dart は「export 'src/widgets/basic.dart';」という実装を持ちます。

https://github.com/flutter/flutter/blob/1ad538e454c77496fbd068b9e8b5f8b61c2f6d96/packages/flutter/lib/src/widgets/basic.dart#L1541

basic.dartは「export 'package:flutter/painting.dart';」という実装を持ち、painting.dartは「export 'src/painting/basic_types.dart';」という実装を持ちます。更にbasic_types.dartは「import 'dart:ui' show TextDirection;」という実装を持ちます。
おお、繋がった。dart:ui → engine/lib/ui/text.dart

2.4.4. runApp()

runApp()は、Flutterの実装では「Futter/packages/flutter/lib/src/widgets/binding.dart」に実装されています。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/binding.dart

Hello Worldソースから辿ると・・・
main.dart から import した flutter/widget.dart は「export 'src/widgets/binding.dart';」という実装を持ちます。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/widgets.dart

3. まとめ

ということで・・・Text / Center / TextDirection / runApp を使うために必要な import は package:flutter/material.dart であり、1行のimport文から各種必要なクラスの読み込み(export)が行われているFlutter内の実装を明確に理解することが出来ました。
5分で流し読んでしまう Hello World ですが、ちょっとした なぜ?なぜ? からえらい深掘りになったというお話です。。。
でも、本質部分を理解することで、もっと大きな問題に出会ったときに、その解決方法を自力で導き出せると思うので、まあ知っていて損はないかと^^

Flutterはリファレンスもしっかりしていて、

https://docs.flutter.io/index.html

オープンソースなので、何か疑問点やうまく動作しない部分が合ったとき、ある程度内部動作を調べることも出来て、

flutter:
https://github.com/flutter/flutter

engine:
https://github.com/flutter/engine

今後もいろいろリサーチしていこうかと思います。