Yaya作った

github.com

こんなんつくりました。

導入

1.用意するもの

2.Herokuにデプロイ

https://github.com/anoChick/yaya

↑の[Deploy to Heroku]ボタンを押します f:id:anoChick:20170306031846p:plain

HerokuにログインしていればApp作成画面が出てくると思います。 環境変数をいれていきましょう

[SLACK_API_TOKEN] SLACKのAPIトークンをいれてください。 [ROOT_URL] アプリケーションのROOTURLを入れる所なんですが、まだ生成されていないので適当な文字列を入れておいて下さい。 f:id:anoChick:20170306032129p:plain

Deployボタンを押せばデプロイされます。

デプロイが完了したらwebページが生成されるので、そのURLをROOT_URLに設定しておいて下さい。 (Settingsの[Config Variables])でできます。

slackのbotがオンライン状態になったらデプロイ成功です。 f:id:anoChick:20170306033026p:plain

動かす

GraphQLのfieldに付くresolveをTypeによって指定する。

GraphQLでDateTimeを扱う時、フォーマットを指定する。 - あのにのに

前回の記事で 「DateTime型とかはクエリ側で出力フォーマットを引数として指定できると便利!」 って話をしました。

ついでに課題として

  • DateTimeTypeって作ったけど意味なくない..?

  • いちいちresolveにフォーマット付与を記述するの面倒じゃない?

という物があったのでこれを解決したいと思います。

やること

Typeが**ならresolveでxxをする。と言うような仕組みを作ります。

今回の場合だと 「fieldのTypeがDateTimeTypeの場合、そのfieldに引数[:format]を用意し、resolveではformatに応じで出力フォーマット通りに書こうするようにする。」 になります。

これができるようになると、DateTimeTypeが付いたfieldは全て[:format]が使えるようになります。

ちなみに使用するgemは前回に引き続き

https://github.com/rmosolgo/graphql-ruby

です。

やってく

今回はGraphQL::Define::AssignObjectFieldをモンキーパッチをあてる形で動かしてみます。 GraphQL::Define::AssignObjectField#callの程よくfieldが得られたタイミングで

if field.type.to_s == 'DateTime'
  field.arguments['format'] = GraphQL::Argument.define(name: 'format', type: -> { GraphQL::STRING_TYPE })
end

f:id:anoChick:20170201041027p:plain

ついた!

次にリゾルバを作ります。

class DateTimeTypeFormatResolver
  def initialize(attr_name)
    @attr_name = attr_name
  end
  def call(obj, args, ctx)
    return obj[@attr_name].strftime(args[:format]) if args[:format].present?

    obj[@attr_name]
  end
end

こんな感じ。あとは

if field.type.to_s == 'DateTime'
  field.arguments['format'] = GraphQL::Argument.define(name: 'format', type: -> { GraphQL::STRING_TYPE })
  field.resolve=(DateTimeTypeFormatResolver.new(name))
end

とか書けばok

f:id:anoChick:20170201041249p:plain

一応これで完成。

割りと便利な気がする。

今回新たにうまれた課題としては

  • 他でリゾルバを記述したくなったらどうするんだ
  • この書き方だとModelの属性名とTypeのfield名が一致している場合しかつかえない

次↑について考えます。

あとはその他便利拡張あればやる。

GraphQLでDateTimeを扱う時、フォーマットを指定する。

GraphQLにはScalarTypeというクラスが存在する。

ScalarTypeは配列やオブジェクト(いわゆるkey-valueペア)、enumではないタイプの基底クラスになる。

GraphQLではこのScalarTypeをベースとした5つの基本タイプが仕様として定義されている。

  • Int - 符号付き32ビット整数
  • Float - 倍精度少数
  • String - UTF-8文字シーケンス
  • Boolean - true or false
  • ID - GUID

これだけだとDateTime等で困るので、DateTimeのGraphQLTypeを作ってみる。

date_time_type.rb

graphql-rubyを使う

https://github.com/rmosolgo/graphql-ruby

DateTimeType = GraphQL::ScalarType.define do
  name 'DateTimeType'
  description 'ActiveRecord::Type::DateTimeに対応したType'

end

このままだとクエリに対する出力結果が

{
  "data": {
    "series": {
      "created_at": "2017-01-09T18:55:30.000Z"
    }
  }
}

になる。せっかくGraphQLなので、フォーマットもクエリとして与えられるようにしたい。

created_atフィールドをいじる

普通にcreated_atを一属性として扱うだけなら

field :title, DateTimeType

の記述だけで済むが、今回は引数formatを与えると、その通りに整形してくれるようにする。

  field :created_at do
    type DateTimeType
    argument :format, types.String
    resolve ->(obj, args, ctx) {
      return obj.created_at if args[:format].nil?

      obj.created_at.strftime(args[:format])
    }
  end

これでqueryを叩く

{
  series(id: 3) {
    created_at(format:"%Y年%m月%d日 %H:%M:%S")
  }
}

{
  "data": {
    "series": {
      "created_at": "2017年01月09日 18:55:30"
    }
  }
}

良い。 本当はフォーマットの仕組みをTypeそのものに持たせたいのだけれど、TypeのresultがStringを取ることになるので適切では無さそう。

RailsのカスタムGeneratorを自分で作る

事前に用意したテンプレートを基にファイルを生成するようなコマンドを作る。

今回はGemにしたいのでプラグイン作成の想定でやる。

Railsプラグイン作成環境を用意

bin/rails plugin new sampleplugin

gemspecファイルのTODOになってるところを書き換えて、bundle installをする。

プラグイン自体はlibディレクトリ以下に作っていき、 test/dummyrailsプロジェクトがあるのでそこで動作確認をしていく。

Generatorクラスを作る

lib/generators/sample_generator.rb にgeneratorの処理を記述する

class SampleGenerator < Rails::Generators::Base

  def initialize(args, *options)
  super

  @_args, @_options = args, options
  end

  def main
    # ここに処理を書く  
  end
end

これでbin/rails generate sampleなどと叩くとmainが実行される。

テンプレートの用意

例えば

<%= @type_name %> = GraphQL::ObjectType.define do
  name '<%= @model_name %>'

end

lib/generators/templates/types.rbにこんな感じでファイルを作っておく。

テンプレートを使う

class SampleGenerator < Rails::Generators::Base
  source_root File.expand_path('../templates', __FILE__)

  def initialize(args, *options)
  super

  @type_name ='sample_type'
  @model_name = 'Sample'
  end

  def main
    template "types.rb", "app/graphql/types/#{@type_name}.rb"
  end
end

f:id:anoChick:20170118032812p:plain

できた。

Model(ActiveRecord)からGraphQL::ObjectTypesを自動生成する仕組みを考えるメモ

最近個人的にWebアプリ作ってます。 フロントはReactJS+Redux サーバサイドはRails それぞれ独立していて、GraphQLを用いて通信しています。

GraphQLのRuby実装として一番スターの多いgraphql-rubyを使っています。

GraphQL周りはまだ発展途上なのでいろいろと不便。 Typesを手動で定義するのが面倒なのでこれについて考えてみる。

Modelに対応するObjectの構成要素について

ActiveRecord内の要素で、今回着目するつもりなのは以下の通り - ActiveRecord::Attributes - ActiveRecord::Relation - ActiveRecord::Enum

ActiveRecordは GraphQL::ObjectTypeと対応させるとして, ActiveRecord::Relationの場合は has_oneの場合:GraphQL::ObjectType has_manyの場合:GraphQL::ListType(GraphQL::ObjectType) でよさそう。 ActiveRecord::EnumはそのままGraphQL::EnumTypeで問題ないはず。

ActiveRecord::AttributesはGraphQL::ScalarTypeになるのだけれど、 GraphQLの標準として定められているScalarTypeは

  • Int
  • Float
  • String
  • Boolean
  • ID

5つだけ。

- ActiveModel::Type::BigInteger
- ActiveModel::Type::Binary
- ActiveModel::Type::Boolean
- ActiveModel::Type::Decimal
- ActiveModel::Type::DecimalWithoutScale
- ActiveModel::Type::Float
- ActiveModel::Type::Integer
- ActiveModel::Type::String
- ActiveModel::Type::Text
- ActiveModel::Type::UnsignedInteger
- ActiveModel::Type::Value

Rails側はこんな感じ

対応できないGraphQL::ScalarTypeを新たに定義するのでもいいけど ユーザ独自で定義したActiveModel::Typeにも対応させることを考えると、 ActiveModel::Typeレベルから対応関係を結んだほうがよさそう。

StorybookでReactJSコンポーネントのレビュー環境を作る

f:id:anoChick:20161230123929p:plain

https://getstorybook.io/

StorybookとはWeb開発におけるUIコンポーネントを作る環境のこと。

インストール

まずは導入するReactJSプロジェクトを用意する。

mkdir react-sample
cd react-sample
npm init
npm i --save react react-dom

次にStorybookを導入するためのコマンド getstorybook をいれる。

npm i -g getstorybook
getstorybook
 getstorybook - the simplest way to add a storybook to your project.

 • Detecting project type. ✓
 • Adding storybook support to your "React" app. ✓
 • Preparing to install dependencies. ✓

yarn install v0.17.6
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 📃  Building fresh packages...
success Saved lockfile.
✨  Done in 21.14s.

 • Installing dependencies. ✓

To run your storybook, type:

   yarn run storybook

For more information visit: http://getstorybook.io

このようになったら導入成功。 yarnの利用が推奨されてるらしい。

storiesというディレクトリが作れていて、中に

  • Welcome.js
  • Button.js
  • index.js

3つのファイルが生成されている。

WelcomeとButtonはサンプルコンポーネントで,indexはコンポーネントをStorybookにつなぐためのもの。

使う

早速 yarn run storybookで実行してみる。

http://localhost:6006/にStorybookが表示される。 f:id:anoChick:20161230125638p:plain

import React from 'react';
import { storiesOf, action, linkTo } from '@kadira/storybook';
import Button from './Button';
import Welcome from './Welcome';

storiesOf('Welcome', module)
  .add('to Storybook', () => (
    <Welcome showApp={linkTo('Button')}/>
  ));

storiesOf('Button', module)
  .add('with text', () => (
    <Button onClick={action('clicked')}>Hello Button</Button>
  ))
  .add('with some emoji', () => (
    <Button onClick={action('clicked')}>😀 😎 👍 💯</Button>
  ));

storiesOfでストーリーを定義出来る。 コンポーネントの視覚的なテストのようなイメージ。 addでprops等を入れていく。 試しに small というpropertyがつくとボタンが小さくなるようにする。

index.js

.add('with small', () => (
    <Button onClick={action('clicked')} small>Small Button</Button>
  ))

を追加する。

import React from 'react';


export default class Button extends React.Component {

  constructor(props){
    super(props);
    this.styles = {
      border: '1px solid #eee',
      borderRadius: 3,
      backgroundColor: '#FFFFFF',
      cursor: 'pointer',
      fontSize: 15,
      padding: '3px 10px',
      margin: 10,
    };
  }

  render() {
    if(this.props.small){
      this.styles.fontSize = 10;
    }

    return (
      <button
        style={this.styles}
        onClick={this.props.onClick}
      >
        {this.props.children}
      </button>
    );
  }
}



Button.propTypes = {
  children: React.PropTypes.string.isRequired,
  onClick: React.PropTypes.func,
};

Buttonコンポーネントをこんな感じに書き換える。

f:id:anoChick:20161230155419p:plain

リアルタイムで見た目が変わった。

共有する

作成したStorybookプロジェクトはStorybookHUBで共有できる。

Storybook Hub - The Perfect Place To Develop & Review User Interfaces

まずStorybookにコメント機能を付ける。

// To get built in addons.
npm i -D @kadira/storybook-database-cloud
npm i -D @kadira/storybook-addon-comments

↑を実行した後`.storybook/addons.jsを記述する

import '@kadira/storybook/addons';

import '@kadira/storybook-database-cloud/register';
import '@kadira/storybook-addon-comments/register';

コミットしてGitHubにプッシュする。

Storybook Hub - The Perfect Place To Develop & Review User Interfaces StorybookHUBのサイトにAppを登録する。 いまプッシュしたGitHubリポジトリを対象にする。

f:id:anoChick:20161230171741p:plain

自動でビルドされて、StorybookのURLが生成される。 ↓こんな感じ

React Storybook

publicで公開することもできれば、privateでAppを作成して招待するという事もできる。

f:id:anoChick:20161230172150p:plain Storyに対してコメントが出来る。

ちなみにPublicリポジトリの場合は無料で利用できるけれど、 Privateリポジトリはコラボレータを増やすのにお金がかかる。

クローリングアプリを作る Part.1

今年得た知識を集めたらいい感じのクローリングアプリが作れそうな気がしたのでやってみる。

作りたい物

Architect

f:id:anoChick:20161229051026p:plain

こんな感じ。 非技術者でも簡単に利用できるような、 アプリ単体でも稼働するネイティブアプリケーション。

ポイントとしてはサーバとDBはちゃんと分離して、外に出せるようにしたい。 サーバは一般的なWebサーバとして実装しておけば外に出したとき接続先の変更だけで対応出来る。 DBは一工夫要りそう。

とりあえずは単体で完結するようなものを作る。