20190227のRubyに関する記事は18件です。

payjp.jsの導入方法<Rails>

Payjp.jsを導入するにあたって、理解するのに時間がかかってしまったので(なにせ記事が少ない!)今後導入される方の時間短縮のために、ここに残しておきます。

Pay.jpとは

Pay.jpのサービスを利用することでクレジットカード決済機能を簡単に導入することができます。
gem'payjp'をインストールすることで使用可能に。
利用方法については大きく2通りあります。

「チェックアウト」

公式で用意されたスクリプトタグをHTMLに記述すれば、デザインされた決済フォーム、カード情報のバリデーション、カード情報のトークン化を行うフォームを生成することができます。こちらを使用すれば、ささっとすぐに実装可能です。
ビューについてこだわりがないという方であればこちらで十分だと思います。

「カスタムフォーム(payjp.js)」

payjp.jsを使うことで、好きなデザインや挙動でトークン化を行うフォームを組み込めます。
ここで注意をしたいのが、payjp.jsはトークン化に特化したライブラリであり、これを使用したからといって決済ができる訳ではありません。
クレジットカード情報入力ページは自分の好みのビューにしたい!という方はこちらです。
今回はこのpayjp.jsで実装していくための説明となります。

トークンとは

クレジットカード情報を自身のデータベース上で管理するのはセキュリティの観点で好ましくないので、顧客が入力したカード情報をトークン化しなくてはいけません。
顧客情報を持った情報の塊のことをトークンを呼びます。IDを使用することで顧客情報を呼び出すことができます。

トークン化については公式を参考にしてください。
https://pay.jp/docs/cardtoken

またトークンは1度のみの使用しか出来ないので、2度以上使いたい場合は顧客カードを作成してカード情報をそこに紐付けることが必要です。顧客カードへの紐付けというのは後ほど説明します。

いざ導入!

ここからは実際のコードを交えて説明します。
まずは、顧客がカード情報を入力するためのフォームを作成します。

qiita.html.haml
%h2 クレジットカード情報入力
  = form_tag("/users/purchase", method: "PATCH", id: "charge-form") do

  %label カード番号
    %input{maxlength: "16", type: "text", class: "number", id: "payment_card_no" name: "number", placeholder: "カード番号"}
  %label CVC
    %input{type: "text", class: "cvc", id: "payment_card_security_code", name: "cvc", maxlength: "3", placeholder: "CVC"}
  %label 有効期限
    %input{type: "text", class: "exp_month", id: "payment_card_expire_mm",  name: "exp_month", maxlength: "2", placeholder: "月"}
    %input{type: "text", class: "exp_year", name: "exp_year", maxlength: "4",  placeholder: "年"}

Pay.jpにトークンを作成してもらうには、公式で用意されているフォームを活用しましょう。
(本記事では所々書き換えています。)

qiita.js
$(document).on('turbolinks:load', function() {
  var form = $("#charge-form");
  Payjp.setPublicKey('pk_test_0383a1b8f91e8a6e3ea0e2a9');

  $("#charge-form").on("click", "#submit-button", function(e) {
    e.preventDefault();
    form.find("input[type=submit]").prop("disabled", true);
    var card = {
        number: parseInt($("#payment_card_no").val()),
        cvc: parseInt($("#payment_card_security_code").val()),
        exp_month: parseInt($("#payment_card_expire_mm").val()),
        exp_year: parseInt($("#payment_card_expire_yy").val())
    };
    Payjp.createToken(card, function(s, response) {
      if (response.error) {
        alert("error")
        form.find('button').prop('disabled', false);
      }
      else {
        $(".number").removeAttr("name");
        $(".cvc").removeAttr("name");
        $(".exp_month").removeAttr("name");
        $(".exp_year").removeAttr("name");

        var token = response.id;
        $("#charge-form").append($('<input type="hidden" name="payjp_token" class="payjp-token" />').val(token));
        $("#charge-form").get(0).submit();
      }
    });
  });
});

顧客がsubmitボタンを押した瞬間にこのjsファイルが反応するようにしてあげます。
このフォームの中のPayjp.createTokenで作成されたトークンIDがresponse.idに返ってきます。
後はif文で分岐をさせてあげて、最後にe.preventDefault();によって停止させていたsubmitを実行させます。

setPublicKeyにはpay.jpにログインした際の管理画面で確認できるAPIキーを入れてください。
APIキーはconfig/initializers/setting.rbを作成してその中に変数を定義して入れてあげるとrbファイル間で共有できるので、利便性・セキュア面で良いですね!
(記載しているAPIキーは公式のものなのでご心配なく)

qiita.html.haml
 %input.payjp-token{ type: "hidden", name: "payjpToken", value: "" }

そしてHTML上で上記の記述をして、jsファイルで作成したトークンIDが空のvalueに入ることで、paramsとしてコントローラーへ投げることができます。
inputタグをhiddenにすることでビュー上には反映されていません。
以上でトークン作成と、トークンIDをparamsとしてコントローラーへ渡せるようになりました。

顧客カードとの紐付け

カード情報非通過対応により2018年6月頃からサーバーサイドからのカード情報のPOSTリクエストができなくなりました。
そのためトークン化はビュー上で行いましたが、顧客作成はコントローラーで行います。

users.controller.rb
Payjp.api_key = PAYJP_SECRET_KEY
customer = Payjp::Customer.create(card: params[:payjp_token])

Payjp::Customer.createで顧客カードが作成されたので、その中のcardキーのバリューとしてトークンIDを指定してあげます。
カード情報を取り出す際にもこのIDは使用するので、私はUserテーブルのカラムに入れてあげました。

他にも支払い等の詳しいやり方は公式のリファレンスを参照ください。
https://pay.jp/docs/api/#introduction

終わりに

私自身まだエンジニアとしてプログラミングを学んで3ヶ月満たないので、ツッコミどころがあるかもしれません。
こういったやり方もあるんだな〜という、一つの実装方法として参考程度に見てください。
他に効率の良いやり方があるよ!この記述無駄じゃない?と気づいた方がいらしたらご指摘ください(_ _)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyでArrayから重複した値を取り出す方法

こんにちは、とくめいチャットサービス「ネコチャ」運営者のアカネヤ(@ToshioAkaneya)です。
今回はRubyでArrayから重複した値を取り出す方法を紹介します。

RubyでArrayから重複した値を取り出す方法

a = [1, 1, 2, 3, 4, 4, 5]
a.select{|v| a.count(v) > 1}.uniq # [1, 4]

selectとuniqを組み合わせるのがポイントです。
以上になります。

はてなブックマーク・Pocketはこちらから

はてなブックマークに追加
Pocketに追加

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on RailsのIDEをどれにしようかなぁと軽く調べた

Cloud9で開発するの、地味に疲れる

今までAWSのクラウド開発環境であるCloud9を利用してきたわけですが、それをやめようかなぁと思い始めています。
理由は単純で、Command+W 暴発問題があるから。

ファイルを閉じたくてうっかりCommand+Wを叩くと、Safariのタブが閉じてしまい、開発環境そのものが閉じてしまいます。
そんなの気をつけれてば済む話じゃんって言われるとそこまでなのですが、ロジックを書くことに集中しているといつの間にか頭から飛んでしまうのだ…つらぁ…。

Macに入れるIDEを考え始める

元々Java屋でEclipseを使っていたので、とりあえずイマドキなIDEを入れればいいかって考えに至り、探してみることに。
パッと検索した限り、以下の3つが良いっぽい。

  • Eclipse (+RadRails)
  • RubyMine
  • VS Code

Eclipse (+RadRails)

スクリーンショット 2019-02-27 23.17.44.png

Javaをやってる方で(使用有無はさておき、)Eclipseを知らない方はいないでしょう。
Pleiadesでフル日本語化されたAll in Oneを落としてくればささっと開発に取りかかれる簡単さがあります。
元々使っていたところにプラグインを入れれば使えるというのは学習コストをかなり抑えられそうです。

良さそうなところ:

  • 学習コスト低め

ちょっとなーってところ:

  • Javaやってた時と代わり映えしないので飽きが…

RubyMine

スクリーンショット 2019-02-27 23.01.02.png

使いやすいIDEを出してくることに定評のある(と伝え聞いている)JetBrains社製IDE。
ちょっと前までデファクトスタンダードだったとどこかで見ました。

良さそうなところ:

  • 情報量が豊富、何かあっても検索してどうにかなりそう

ちょっとなーってところ:

  • 有料か…

VS Code

スクリーンショット 2019-02-27 23.00.30.png

プロプライエタリ製品の名称だったVisual Studioの名を冠して登場した無料版だと思ってたら、あっという間にWeb業界へ浸透している感のあるIDE。
色んなところから「VS Codeはいいぞ」と聞こえてきます。

良さそうなところ:

  • 今超絶ホット。使ってるだけで最前線感バリバリ。

ちょっとなーってところ:

  • まだ情報が蓄積されていなさそう(& バージョン依存のトラブルシュートが引っかかる)

とりあえずVS Codeからかじってみようかなー

ローカルでRailsの開発をするのはこれからなので、切り替えの手間を考えなくていい分、新しいやつをやってみようという安直な理由でVS Code。
ダメだったらEclipseに転ぶことにしよう。

IDE?テキストエディタ使えよ

わかる、わかるよ、その方が軽いし環境選ばないしハッカーっぽいしで良いってのはわかるよ。
でもこちとらRailsに関してはホビープログラマ、楽なやり方で開発したいのだ…本当にすまない…。

おすすめあったら教えてくださいなんでもしますから!

「(なんでもするとは言ってない)」の文脈。
「RailsでこのIDEいいよ!」って意見、お待ちしております。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby|RSpecでメソッド呼び出しのテスト(ブロック付き)

いつものように「これ、誰がつかうん?」という小ネタです。

たまーにRSpecでメソッドってちゃんと呼び出されてるよね?というテストを書くと思います。withを使うと引数に狙った値が渡っているかを検証できますが、ブロック付きはどうするか調べたのでそのメモです。

先に言ってしまうと検証は「&手続きオブジェクト」の場合のみ出来ます。

前段階

検証に使うプログラムはDeckクラスを利用します。これは何枚かのカードを持っていてシャッフルしたり偶数と奇数を分けてくれます。今回の検証はDeck#split_even_numbersEnumerable#partitionにブロックを渡していますが、それをRSpecでテストする方法です。

本来であればメソッドを呼び出した結果を検証すべきですので、メソッド呼び出しのみのテストはいいと言ってはいません。用法用量を守るのはお兄さんとのお約束だぞ:wink:

class Deck
  def initialize(cards)
    @cards = cards
  end

  def shuffle
    @cards.shuffle
  end

  def split_even_numbers
    @cards.partition { |card| card.even? }
  end

  def split_odd_numbers
    @cards.partition(&:odd?)
  end
end

RSpec

ブロックの検証

withにブロックを指定すると{ |card| card.even? }を受け取ることができるようです。&blockとしているのでProcクラスのオブジェクトになっています。

Procクラスのオブジェクトをつくって比較すればいいか!と思っていたのですが、メソッドに渡してるブロックとは異なるようでテストはうまくいきませんでした:sweat_smile:

describe Deck do
  describe '#split_even_numbers' do
    let(:deck) { Deck.new(cards) }
    let(:cards) { [5, 4, 3, 2, 1] }

    it do
      expect(cards).to receive(:partition).with(no_args) do |&block|
        # この検証はうまくいかない
        expect(block).to eq(Proc.new{ |card| card.even? })
      end
      deck.split_even_numbers
    end
  end
end

&手続きオブジェクト

「&手続きオブジェクト」といっていますが検証したいProcクラスのオブジェクトとテストでの期待値が同じオブジェクトであれば今回のやりたいことは実現することが出来ます。

:odd?.to_procは常に同じオブジェクトを返すので検証で利用して期待した結果を得ることが出来ます。

describe Deck do
  describe '#split_odd_numbers' do
    let(:deck) { Deck.new(cards) }
    let(:cards) { [5, 4, 3, 2, 1] }

    it do
      expect(cards).to receive(:partition).with(no_args) do |&block|
        expect(block).to eq(:odd?.to_proc)
      end
      deck.split_odd_numbers
    end
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpec

SPecのモデルを作成

Userモデル用に作成

$ rails g rspec:model user

RSpecのテスト実行

$ rspec
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby コメント、変数、定数、リテラル

動作環境はMacとなります。
主に自分の勉強用メモとして残しています。

コメント

特定の処理を向こうにしたいときや、メモを残したい時にしようする。

comment.rb
# puts "Hello, World!" # 処理を向こうにしたいときは、プログラムの先頭に#をつける。改行までがコメント扱いとなる。
$ruby comment.rb 
$ #コメントアウトしているため出力されない。

複数行コメントアウトしたい場合

comment.rb
=begin
puts "Hello, World!"
puts "Hello, World!"
puts "Hello, World!"
=end
# =beginから=endの間の処理がコメントアウトされる
$ruby comment.rb 
$ #コメントアウトしているため出力されない。

実務上複数行に渡る場合でも、行頭に一行ごとに#をつけて実行されることが多い。

コメントを解除したい場合

commandキー+/キーで解除可能 ※複数行の場合、解除したい行をまとめて選択

メモとして残したい場合でも使用可能。

comment.rb
# This is a comment. 
$ruby comment.rb 
$ #コメントアウトしているため出力されない。

変数

扱うデータに名前をつけて、一時的に利用できるようにしたもの

変数名 = 式


name = "suzuki"
fruit = "apple"
x = 10


#変数sに Helloという文字列を代入
irb(main):001:0> s = "Hello!" 
=> "Hello!"
irb(main):002:0> puts s
Hello!
=> nil
#出力

#型の宣言は不要
irb(main):001:0> char s = "Hello!" 
irb(main):001:0> var s = "Hello!" 

数値の代入

irb(main):003:0> n = 2 * 10 #値20を変数nに代入
=> 20
irb(main):004:0> puts n  #変数nの出力
20 
=> nil

変数には何かしら代入しないといけない


#変数に何も値が入っていないとエラーになる。
irb(main):005:0> i
NameError: undefined local variable or method `i' for main:Object
    from (irb):5
    from /Users/toripurug884/.rbenv/versions/2.4.1/bin/irb:11:in `<main>'

#中身を入れて再出力
irb(main):006:0> i = nil
=> nil
irb(main):007:0> i
=> nil
#実行された!

変数名の命名規則

アルファベットの小文字かアンダースコアで始まる。
それに続けてアルファベット、数字、アンダースコアで記述する。

irb(main):014:0> price = 100
=> 100

#以下原則しないほうが良い命名規則

irb(main):015:0> price1 = 100  #数字をつける
=> 100

irb(main):016:0> _price = 100  #アンダースコアから始まる変数
=> 100

#数字から始まる変数名はエラーになる。

irb(main):017:0> 1price = 100
SyntaxError: (irb):17: syntax error, unexpected tIDENTIFIER, expecting end-of-input
1price = 100


#2語以上になる場合は原則_で繋げる。
#キャメルケース(単語と単語の間の文字を大文字にする)は原則使わない。

irb(main):018:0> price_cost = 100
=> 100
irb(main):019:0> priceCost = 100
=> 100
irb(main):020:0> 

よくない変数の使い方

エラーではないが、メンテナンス性が悪くなるので推奨されない変数の使い方

同じ変数に違う型を代入

irb(main):022:0> x = "abc" #文字列を代入
=> "abc"
irb(main):023:0> x = 123 #数字を代入
=> 123
irb(main):024:0> x = "def" #文字列を代入
=> "def"
irb(main):025:0> x = [1,2,3] #配列を代入
=> [1, 2, 3]

#同じ値を入れてしまうとわかりにくくなる

複数の変数に代入

irb(main):026:0> x,y = 1,2
=> [1, 2]
irb(main):027:0> x
=> 1
irb(main):028:0> y
=> 2

#あまりメリットがない。
irb(main):029:0> a = b = 10
=> 10
irb(main):030:0> a
=> 10
irb(main):031:0> b
=> 10

#あえて使う必要はない

定数

あまり変化しない値を扱う時に使う
例)消費税、円周率、手数料など

定数は原則全て大文字


irb(main):032:0> TAX = 1.08 #定数TAXに値を代入
=> 1.08
irb(main):036:0> puts TAX
1.08
=> nil

irb(main):037:0> TAX_RATE = 1.08 #2語以上の定数の場合単語は_で繋ぐ
=> 1.08
irb(main):038:0> puts TAX_RATE
1.08
=> nil

コードのメンテナンスを保つため、定数には再代入は原則しない。

irb(main):039:0> TAX_RATE = 1.1 #一度定義した定数に再代入


#警告が出るが、再代入されてしまうので注意
(irb):39: warning: already initialized constant TAX_RATE
(irb):37: warning: previous definition of TAX_RATE was here
=> 1.1
#再代入後の値が出力される
irb(main):040:0> puts TAX_RATE 
1.1
=> nil

リテラルとは

数値の123や文字列の"Hello! World"など
Rubyのプログラムの中に直接記述できる値

ソースコードに書いたもの

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyでインターフェイスの実装を強制させたい

Rubyでインターフェイスの実装を強制させる方法を考えたいと思います。

Rubyは動的型付け言語なので、少なくともポリモーフィズムを実現するのに、Javaのようなインターフェイスを定義する必要はありません。しかし、「ソフトウェアのモジュールを疎結合に保つためには、モジュールは他のモジュールの実装ではなくインターフェイスに依存すべきである」という原則があります。そのような観点からは、インターフェイスを明示でき、それを利用するクラスがインターフェイスの規約をみたしているかチェックできると便利そうです(本当か?)。

以下に紹介する方法では一部メタプログラミングを用いていますが、一般的にメタプログラミングはプログラムの保守性や可読性を下げるため、多用すべきではありません。また、本稿の内容によって生じた問題に関して、筆者は責任を負いかねます。

とりあえず、メソッド呼び出し時に例外にしてみる

もっとも簡単な方法は、実装されていないメソッドが呼び出されたときに、例外とする方法でしょう。

sample.rb
module SomeInterface
  def some_method
    raise NotImplementedError, "Method \"#{__method__}\" is not implemented in class \"#{self.class.name}\"."
  end
end

class SomeClass
  include SomeInterface
end

obj = SomeClass.new
obj.some_method # => Method "some_method" is not implemented in class "SomeClass". (NotImplementedError)

この例では、SomeClassでsome_methodがオーバーライドされていなければ、親モジュールのsome_methodが呼び出されて、NotImplementedErrorが投げられます。

ただ、これだと実際にsome_methodが呼び出されないと、メソッドの未実装が検知できないので微妙です(そもそもこんなことしなくても、定義されていないメソッドを呼び出せばNoMethodErrorが投げられますし)。やはり、クラス定義時にメソッドの未実装を検知できるようにしたいものです。

メタプログラミングでなんとかしてみる

というわけで目標としては、attr_accessorやincludeのように、モジュール定義内で以下のように書くことで、メソッド未定義検知に必要な処理をするようにしたいです。

module SomeInterface
  interface :some_method
end

class SomeClass
  implements SomeInterface
end
# => Method "some_method" is not implemented in class "SomeClass". (NotImplementedError)

以下、これを実装するために必要な事項をかんたんに復習します。

モンキーパッチ

Rubyでは、実行時にクラスやモジュールの定義を、変更したり拡張したりできます。
たとえば、以下の例では組み込みのクラスHashに、Structへ変換する処理を追加しています。

hash_to_struct.rb
class Hash
  # 注意: キーと値が同一でも==による比較はfalseになる
  def to_struct
    new_keys = self.keys.map(&:to_sym)
    new_values = self.values
    Struct.new(*new_keys).new(*new_values)
  end
end

h = {'name': 'Taro', 'age': 22}
s = h.to_struct # => #<struct name="Taro", age=22>

なお、クラスやモジュールの上書きは、グローバルに影響するので注意が必要です。今回は簡単のためにこの方法で説明しますが、特別な理由がなければRefinementsを用いるのが好ましいです。Refinementsを使った場合、上書きの影響範囲はusingを用いたスコープ内に限定されます。

クラスもモジュールもオブジェクト

Rubyistには今さらな話ですが、Rubyではクラスやモジュールもオブジェクトです。たとえば、クラスは「Classクラス」のインスタンスであり、クラスFooの宣言は「定数FooにClassのインスタンスを代入している」に過ぎません。なので、以下の2つのコードは同じことをやっています。

class Foo
end
f = Foo.new
Foo = Class.new
f = Foo.new

attr_accessorやincludeはメソッド

これもRubyistには今さらな話ですが、attr_accessorやincludeなどはいかにも言語の予約語みたいな見た目ですが、単なるメソッドです。たとえば、attr_accessorはModuleクラスのインスタンスメソッドであり、引数で受け取ったシンボルと同名のインスタンス変数を返すメソッドを動的に定義します。

いざ、実装

上のコードを動作させるには、attr_accessorやincludeと同じくModuleクラスのインスタンスメソッドとして、「interface(*syms)」と「implements(*consts)」を実装すればいいようです。
あとは、実装させるべきメソッド名と、実際に実装されているメソッド名が取得できれば、目的のものが作れそうです。

実装させるべきメソッド名の一覧(上述の例におけるsome_method)は、具体的なモジュール(上述の例におけるSomeInterface)が有します。
具体的なモジュールは、Moduleクラスから見ると自身のインスタンスなので、実装されるべきメソッド名は、インスタンス変数または特異メソッドから取得できそうです。
インスタンス変数は外部からは直接参照できないので、今回はinterfaceメソッド呼び出し時に、実装されるべきメソッド一覧を返す特異メソッドinterface_methodsを動的に定義するようにします。

implementsの方は、実装すべきメソッド名がすべて、実際に実装されているメソッド名一覧に含まれているかチェックします。その後、includeを呼び出して、クラスの継承関係にインターフェイスモジュールを追加します。

というわけで、できたコードが以下です。

interface.rb
class Module
  def interface(*syms)
    define_singleton_method(:interface_methods) { syms }
  end

  def implements(*consts)
    methods_should_be_implemented = consts.map(&:interface_methods).reduce(:+).uniq
    methods_actually_implemented = self.instance_methods
    methods_should_be_implemented.each do |method|
      unless methods_actually_implemented.include?(method)
        error_str = "Method \"#{method}\" is not implemented in class \"#{self.name}\"."
        raise NotImplementedError, error_str
      end
    end
    include *consts
  end
end

実際に実行してみると、どうやら意図どおりに動くようです。

sample3.rb
require './interface.rb'

module Interface1
  interface :method_a, :method_b
end

module Interface2
  interface :method_c
end

class SomeClass
  def method_a
  end
  def method_c
  end

  implements Interface1, Interface2 # 仕様上どうしても、メソッド定義のあとに書かないといけない。
end
# => NoImplementedError: Method "method_b" is not implemented in class "SomeClass".

以上です。
今回は、最低限の機能だけ果たしそうなものを書いてみましたが、いろいろ懸念点や改善点がありそうです。たとえば、

  • 他のライブラリと名前が衝突しないか
  • パフォーマンスが悪化しないか
  • 引数の個数まで含めてチェックしたい場合はどうするか
  • implementsはメソッド定義の前に書きたい

等々。

ご意見・ご感想等ございましたら、コメントいただけると幸いです。

参考文献

有名すぎて今さらですが、非常に良い本です。「メタプログラミング」という語から、何やらマニアックなテクニックばかり載っているように思われるかも知れませんが、Rubyのオブジェクトモデルやブロックの説明などは、多くのRubyプログラマーの役に立つと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails APIモードで始めるGraphQL

はじめに

本記事は、社内でGraphQL入門勉強会を実施した際の内容です。
本記事での対象は、

  • graphql-ruby/graphiql-railsの設定
  • Query(データ取得)
  • Mutation(データ更新)

になります。

内部的な仕組みなど、細かい内容は出てきません。
とりあえず動かして雰囲気を掴むことを目的としています。

環境

バージョン
Ruby 2.6.0
Rails 5.2.2
graphql-ruby 1.6.0
graphiql-rails 1.9.3

GraphQLとは

GraphQLは、2012年にFacebookによって開発されたデータクエリ言語および仕様です。
RESTベースのアーキテクチャに代わるものを提供します。
GraphQLはGitHubのAPI(GraphQL API v4)でも採用されています。

GraphQL API v4 (https://developer.github.com/v4/)

GraphQLでは、クエリ言語を使用してデータの取得や更新を行います。

  • query (データ取得系)
  • mutation (データ更新系)
  • subscription (イベント通知) ※本記事では対象外

Preparation

graphql-ruby導入前のサンプルプロジェクトを用意しています。
本記事では、このサンプルプロジェクトにgraphql-rubyを導入していきます。

git clone git@github.com:dkawabata/graphql-ruby-sample.git
cd graphql-ruby-sample
bundle install --path vendor/bundle
bundle exec rails db:migrate
bundle exec rails db:seed

サンプルプロジェクトは、usersテーブル/postsテーブル/commentsテーブルの3テーブルで構成しています。

schema.png

Setup

GraphQL Rubyのインストール

Gemfileを編集します。

Gemfile
+ gem 'graphql'
group :development do
+  gem 'graphiql-rails'
end

graphiqlというブラウザでクエリを実行して、GraphQLの動作確認ができるツールのRails版がgraphiql-railsです。
GraphQLでの開発に必須ではありませんが、今回はgraphiqlで動作確認するため、追加しています。
Gemfileを編集したら、インストールします。

bundle install
bundle exec rails g graphql:install

APIモードでは無い場合、graphiql-railsの設定(Gemfileへの追加や、以降で行うroutes.rbの編集)は不要です。
rails g graphql:installのタイミングで自動的に設定されますが、APIモードの場合はSkipされるようです。

Graphiqlの設定

routes.rbの編集

config/routes.rb
if Rails.env.development?
  mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
end

application.rbの編集

Rails APIモードでgraphiqlを利用するために、application.rbrequire "sprockets/railtie"のコメントアウトを外します。
graphiql-railsのREADMEにも記載されています。

https://github.com/rmosolgo/graphiql-rails#note-on-api-mode

If you're using Rails 5 in "API mode", you'll also need to add require "sprockets/railtie" to your application.rb.

config/application.rb
- # require "sprockets/railtie"
+ require "sprockets/railtie"

サンプルクエリの実行

ここまでで一通りの準備は完了です。
rails sでサーバを起動して、http://localhost:3000/graphiqlにアクセスしてみましょう。
graphiqlの画面が表示されます。
左側にクエリを入力して実行すると、右側に結果が表示されます。
下図のように、testFieldというクエリを実行して、"Hello World!"が表示されれば成功です :100:

{62A0459F-9BBB-4CA6-BFC6-551BA6EECE12}.png

Query

Queryはデータを取得するために定義します。
Queryを追加する前に、各モデルに対応したTypeを定義しましょう。

型の定義

user/post/commentの各モデルに対応したTypeを定義します。

app/graphql/types/user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, Int, null: false
    field :name, String, null: false
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
    field :posts, [Types::PostType], null: false
    field :comments, [Types::CommentType], null: false
  end
end
app/graphql/types/post_type.rb
module Types
  class PostType < Types::BaseObject
    field :id, Int, null: false
    field :user, Types::UserType, null: false
    field :subject, String, null: false
    field :body, String, null: false
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
    field :comments, [Types::CommentType], null: false
  end
end
app/graphql/types/comment_type.rb
module Types
  class CommentType < Types::BaseObject
    field :id, Int, null: false
    field :post, Types::PostType, null: false
    field :user, Types::UserType, null: false
    field :body, String, null: false
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end

graphql-rubyの場合、各Typeに定義しているfieldと同名のメソッド/attributeがモデルに定義されている場合、自動的にデータを取得できます。

fieldで型を指定しますが、標準で用意されている型には以下のようなものがあります。

  • String
  • Int
  • Float
  • Boolean
  • ID
  • ISO8601DateTime

[]で囲うと配列になります。

Queryの追加

それでは、実際にデータを取得するためのQueryを追加しましょう。
特定のidを持つUserを1件返すQueryと、全てのUserを返すQueryを定義してみましょう。
QueryはTypes::QueryTypeに定義を追加します。

app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
-    # Add root-level fields here.
-    # They will be entry points for queries on your schema.

-    # TODO: remove me
-    field :test_field, String, null: false,
-      description: "An example field added by the generator"
-    def test_field
-      "Hello World!"
-    end

+    field :user, Types::UserType, null: false do
+      description 'ユーザ情報を1件取得する'
+      argument :id, Int, required: true, description: 'ユーザID'
+    end
+    def user(id:)
+      User.find(id)
+    end

+    field :users, [Types::UserType], null: false, description: 'ユーザ情報を全件取得する'
+    def users
+      User.all
+    end
  end
end

Queryの実行

実際にgraphiqlからQueryを実行してみましょう。
まずは、idを指定して1件取得してみます。

query {
  user(id: 1) {
    id
    name
  }
}

次に、登録されているuser全てを取得します。

query {
  users {
    id
    name
    posts {
      id
      subject
      body
    }
  }
}

上記のQueryでは、各ユーザのidname、そのユーザが投稿したPostを全て取得しています。
Postが不要な場合は、idnameだけ取得するということも可能です。
Queryを実行する側が、必要なデータを選択して取得することができる点は、GraphQLのいい点ですね!

Mutation

次はデータを登録するためのMutationを定義しましょう。
特定のPostにコメントを追加するMutationを定義します。

Mutationクラスの作成

app/graphql/mutations/base_mutation.rb
module Mutations
  class BaseMutation < GraphQL::Schema::RelayClassicMutation
  end
end
app/graphql/mutations/create_comment.rb
module Mutations
  class CreateComment < Mutations::BaseMutation
    argument :user_id, Int, required: true
    argument :post_id, Int, required: true
    argument :body, String, required: true

    field :comment, Types::CommentType, null: true
    field :errors, [String], null: false

    def resolve(user_id:, post_id:, body:)
      comment = Comment.new(user_id: user_id, post_id: post_id, body: body)

      if comment.save
        {
          comment: comment,
          errors: []
        }
      else
        {
          comment: nil,
          errors: comment.errors.full_messages
        }
      end
    end
  end
end

Mutation定義の追加

先ほど作成したCreateCommentを使えるようにするため、Mutationの定義を追加します。

app/graphql/types/mutation_type.rb
module Types
  class MutationType < Types::BaseObject
    field :create_comment, mutation: Mutations::CreateComment
  end
end

Mutationの実行

では、graphiqlからMutationを実行してみましょう。

mutation {
  createComment(input: {
    userId: 1
    postId: 1
    body: "コメント追加"
  })
  {
    comment {
      id
      body
    }
  }
}
{
  comment {
    id
    body
  }
}

の部分は、登録したcommentの内容を返却するための記述です。

まとめ

ざっくりですが、RailsのプロジェクトにgraphQLを導入して、QueryMutationを定義する部分を行いました。
一部古い情報もありますが、graphql-rubyのガイド( https://graphql-ruby.org/guides )を見ると、色々載っているので参考になります。

参考URL

https://github.com/rmosolgo/graphql-ruby
https://github.com/rmosolgo/graphiql-rails
https://graphql-ruby.org/guides

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveStorageのファイルをアップロードするタイミングについて

ActiveStorageを使うにあたって、アップロードできたかどうかを調べるために、アップロードをどの処理で実施しているか調べたので、覚書として。(アップロードできたかどうかについては、これから改めて調査)

アプリケーションコードに近い方から遠い方へ順に追っていきます。

前提条件

  • activestorage のバージョンは5.2.2を使用する
  • 属性の定義に has_one_attached を使用する
  • attachするファイルはHTMLからアップロードしたファイルを使用する
  • ファイル保存サービスはローカル(S3やGCSではない)を使用する

ActiveStorage::Attached::Macros

https://github.com/rails/rails/blob/v5.2.2/activestorage/lib/active_storage/attached/macros.rb#L37

ActiveRecord::Baseを継承するモデルクラス内に定義したhas_one_attachedの属性に対して、値が代入された時にattachメソッドを実行します。

ActiveStorage::Attached::One

https://github.com/rails/rails/blob/v5.2.2/activestorage/lib/active_storage/attached/one.rb#L24

ActiveStorage::Attached::One.attach メソッドが create_blob_from を実行します。

ActiveStorage::Attached

https://github.com/rails/rails/blob/v5.2.2/activestorage/lib/active_storage/attached.rb#L23

ActiveStorage::Attached. create_blob_from メソッドで、attachableのクラスによって処理を分けています。
今回はアップロードされたファイルを属性に指定したので、
ActiveStorage::Blob.create_after_upload! を実行します。

ActiveStorage::Blob

https://github.com/rails/rails/blob/v5.2.2/activestorage/app/models/active_storage/blob.rb#L65

ActiveStorage::Blob.create_after_upload!ActiveStorage::Blob.build_after_upload を実行します。

https://github.com/rails/rails/blob/v5.2.2/activestorage/app/models/active_storage/blob.rb#L57

ActiveStorage::Blob.build_after_uploadActiveStorage::Blobnewして、upload を実行します。

https://github.com/rails/rails/blob/v5.2.2/activestorage/app/models/active_storage/blob.rb#L159

ActiveStorage::Blob#uploadserviceインスタンスのuploadを実行します。
このserviceは、公式ガイドのセットアップで service に指定した DiskS3ActiveStorage::Service::DiskServiceActiveStorage::Service::S3Serviceをnewしたインスタンスが格納されています。

今回はローカルディスクを保存サービスに指定しているので、ActiveStorage::Service::DiskServiceuploadを実行します。

ActiveStorage::Service::DiskService

https://github.com/rails/rails/blob/v5.2.2/activestorage/lib/active_storage/service/disk_service.rb#L20

ActiveStorage::Service::DiskService.uploadIO.copy_stream でファイルを Rails.root/storage/にコピーして、アップロード処理が終わります。

instrument のブロックで囲まれていますが、これは ActiveSupport::Notifications.instrument をラップするためのメソッドです。
ログを見ると、

logs/development.log
Disk Storage (31.0ms) Uploaded file to key: fH4PhWtse6qwM6hBqsoqqmtz (checksum: ci+Mp/YmvV385qYcTJnlgg==)

こんな感じのログが、アップロードファイルを属性に代入、またはattachの処理あたりで流れてくると思います。

感想

調べ終わってから記事を書いたので思考とは逆順になっています。

調べ始める前は、モデルクラスをsaveするタイミングでupload的なメソッドをアプリケーションコードに近いところで実行していると予想していたので、attachがそれに相当すると気づくまで時間がかかりました。
saveに合わせてファイルを保存するようにしたい場合は、直接属性に代入するのではなく、after_saveかそれに近いタイミングでattachしてあげると良さそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Serverless FrameworkでGemをLayersに切り出す

環境

  • Mac OS X 10.14.3
  • Serverless Framework 1.36.1
  • Ruby 2.5

はじめに

Serverless FrameworkでRubyを使用する場合にパッケージ容量をもっとも圧迫するvendor(gemの内包ディレクトリ)を切り出す方法について書きます。

こちらの記事を読んで出来るようになること

  • Serverless Framework x RubyでGemをLayerに切り出す
  • Layersのバージョンアップにも対応可能

Lambda Layersとは

ざっくりと、各Lambda関数で共通化した処理をLambda関数の中に内包するのではなく、外に切り出してしまう仕組みのこと。

1. サンプルアプリケーションの作成

この記事ではserverless frameworkの使い方などは書きません

以下のコマンドでサンプルアプリケーションを作成します。

$ sls create -t aws-ruby -p sample

上記を実行すると、以下のようなディレクトリが作成されます。

sample/
- serverless.yml
- handler.rb

2. bundle init & gem install

$ cd sample

上記で、sampleディレクトリに移動しておきます。
sampleディレクトリ内で、以下を実行していきます。

$ bundle init

上記で、Gemfileが作成されるので、エディタでGemfileを編集していきます。

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# 以下を追加
gem "business_time"

適当なGemをGemfileに追加して、以下を実行します。

$ bundle install --path vendor/bundle

3. serverless.ymlを編集

service: sample

provider:
  name: aws
  runtime: ruby2.5
  region: ap-northeast-1
  role: arn:aws:iam::ロールのarn

package:
 exclude:
   - vendor/**
   - Gemfile
   - Gemfile.lock

functions:
  hello:
    handler: handler.hello

layers:
  gems:
    path: vendor

この時点で一度デプロイします。

$ sls deploy

デプロイを実行すると、layersが作成されるので、再度serverless.ymlを編集します。

同一のserverless.yml内であれば、REFで参照をすることが出来ます。
公式ドキュメント

service: sample

provider:
  name: aws
  runtime: ruby2.5
  region: ap-northeast-1
  role: arn:aws:iam::ロールのarn

package:
 exclude:
   - vendor/**
   - Gemfile
   - Gemfile.lock

functions:
  hello:
    handler: handler.hello
    layers:
      - {Ref: GemsLambdaLayer}

layers:
  gems:
    path: vendor

functionlayersを追加して、Refで参照させるのですが、この時に参照方法のルールとして、Layer名をTitleCaseで記載し、LambdaLayerを末尾に結合させる必要があります。
今回のLayer名はgemsなので、GemsLambdaLayerとなっています。

Layer名がhogeだったら、HogeLambdaLayerとなるかと思います。

これで再度デプロイします。

$ sls deploy

これでAWSのLambda関数ページを見ても、まだLayerが反映されません。

どこが問題かと言うと、serverless.ymlには記載する順番を気をつける必要があるようで、以下のように、functionsより上にlayersを記載する必要があります。

上記を修正したserverless.ymlが以下

service: sample

provider:
  name: aws
  runtime: ruby2.5
  region: ap-northeast-1
  role: arn:aws:iam::ロールのARN

package:
 exclude:
   - vendor/**
   - Gemfile
   - Gemfile.lock

layers:
  gems:
    path: vendor

functions:
  hello:
    handler: handler.hello
    layers:
      - {Ref: GemsLambdaLayer}

これで再度デプロイすることでLambda関数にLayerが適用されます。

4. Lambda関数でgemを使用する

handler.rbを以下のようにします

require 'json'
require 'business_time'

def hello(event:, context:)
  puts Date.today.workday?

  { statusCode: 200, body: JSON.generate('Go Serverless v1.0! Your function executed successfully!') }
end

image.png

これで、適当にテストを作成し、「テスト」ボタンを押下すると、以下のようなエラーが発生します。

{
  "errorMessage": "cannot load such file -- business_time",
  "errorType": "Init<LoadError>",
  "stackTrace": [
    "/var/lang/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'",
    "/var/lang/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'",
    "/var/task/handler.rb:2:in `<top (required)>'",
    "/var/lang/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'",
    "/var/lang/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'"
  ]
}

bussiness_timeを参照出来なくなっています。
Lambda Layersを使用する際、格納したファイルやディレクトリは、/optディレクトリに格納される仕様になっています。なので、こちらの対応が必要になります。

対応方法は以下の二つです。
* gemの参照先を絶対パスで記述する
* LOAD_PATHを変更する

後者で対応することにしました。
以下のように、handler.rbを変更します。

load_path = Dir["/opt/bundle/ruby/2.5.0/gems/**/lib"]
$LOAD_PATH.unshift(*load_path)

require 'json'
require 'business_time'

def hello(event:, context:)
  puts Date.today.workday?

  { statusCode: 200, body: JSON.generate('Go Serverless v1.0! Your function executed successfully!') }
end

ちなみに、/opt/bundle/ruby/2.5.0/gems/**/libのパスはlayersの作成方法によって変わってしまうため、適宜変更してください。

これでテストを実行すると、200が返却され、ログにtrueが返ってくるかと思います。

5. sls invokeを使用出来るようにする

上記までだと、LOAD_PATHを変更してしまうため、ローカルでsls invokeが使用できなくなってしまうため、対応したいと思います。

serverless frameworkのsls invokeコマンドを実行すると、IS_LOCALという環境変数が作成され、trueがセットされます。※ こちらを参照ください

なので、これをフラグにLambda関数に条件分岐を設定します。

handler.rbを以下のように変更します。

if ENV['IS_LOCAL'].nil?
  load_paths = Dir["/opt/bundle/ruby/2.5.0/gems/**/lib"]
  $LOAD_PATH.unshift(*load_paths)
end

require 'json'
require 'business_time'

def hello(event:, context:)
  puts Date.today.workday?

  { statusCode: 200, body: JSON.generate('Go Serverless v1.0! Your function executed successfully!') }
end

これで、sls invokeだった時は、LOAD_PATHが参照されなくなるので、sls invokeが使えるようになるかと思います。
当然この方法だと、関数が増えた時に全てに分岐を加えなければいけないので、対応策としては微妙ですが...

もっといい方法があればコメントいただけますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

/xl/drawings/drawing1.xml が壊れたので、なんとかした話

あらすじ

せっせと作ったExcelファイル。完成して他の人に送ったら「開けないんですけど」とのこと。
そんなわけあるかい、、と思い、手元のオリジナルを開くと、「一部に問題が見つかりました。」とのこと。

image.png

可能な限り内容を修復してほしいので、「はい(Y)」を選択

image.png

図形や画像ファイルが綺麗さっぱり消えていました。

こんなに悲しいことがあるだろうか。(いや、ない)

なので、紆余曲折の後、壊れた箇所の特定を手伝うツールを作成したのであった。

調査

しばらくインターネットの海を漂うと、同じ問題に直面した人がいるということが分かった。

OpenOfficeを試す

Excel2010で壊れた図形を復活させてみたを参考にOpenOfficeで開き直すと、確かに開くことができた。

ただ、図の矢印や画像のアスペクト比がぐちゃくちゃになっていて、とても、完全復活といえる代物ではなかった。
テキストベースのexcelなら、この方法で復活させて完了で良い気もするが、図や絵がふんだんに使われていたので、今回はPass。

ちなみに、OpenOfficeではxls形式で保存することしかできないため注意が必要。

drawing1.xmlから壊れている図を特定する

エクセルが開かないによると、

  • xlsxは拡張子をzipに変えるとzipで開くことができる
  • /xl/drawings/drawing1.xml というファイルの中にある<xdr:twoCellAnchor>という要素で図や絵を表現している
  • 壊れた<xdr:twoCellAnchor>を取り除くことで、壊れていない部分は復旧させることが可能

とのこと。

例えば、<xdr:twoCellAnchor>要素を半分にして、エラーが起こるかどうかを確認する、発生したら更に半分にして、、、みたいに、二分探索していけばいつかはたどり着くんだろうけど、、、

  1. 拡張子をxlsxからzipに変更
  2. 解凍
  3. drawing1.xmlを書き換える <= 改行の無いxmlなので、ツールが無いと結構しんどい
  4. 解凍した内容を再び圧縮
  5. 拡張子をzipからxlsxに変更
  6. excelで開いてみる
  7. 壊れていれば、再び3から
  8. (゚д゚)ウマー

という手順が想定されて、そこそこ手間がかかる。

今回は、170個ぐらい図や絵が貼ってあるExcelファイルだったので、[log_2 170] + 1 = 8 で最大8回この作業が発生するようだ。

壊れたら困る(情報量の多い)ファイルほど、壊れるというのが世の常なので、
いざという時のために、ツールを準備しておくのも悪くないだろう、ということで、ひと仕事しました。

それらしいツール作成

それらしいツールを作りました。せっかくなので、置いていきます。

結果的に線形探索になりました。
急いでいたので、Rubyで書きました。
Rubyの記述力に私の実力が追いついていないので、冗長な箇所モリモリですが、お気になさらずどうぞ。

やっていること

<xdr:twoCellAnchor>、もしくは<xdr:oneCellAnchor>要素をdrawing1.xmlに追加しては、その都度rebuild_[連番].xlsxを生成する

使い方

  1. RubyのインストールされたOSを準備する
  2. gem install rubyzipする
  3. githubから例のツールを取得する
  4. targetフォルダにxlsxファイルをzipにして解凍したファイルの中身を配置する
  5. $ ruby ./src/rebuild_excel_book.rbする
  6. resultフォルダに生成されたExcelファイルを上から叩いて、イカれたメンバーを特定するぜ

感想

  • <xdr:twoCellAnchor>だけでなく、<xdr:oneCellAnchor>も存在するようだ。更なる調査が必要
  • xmlは改行が入っていても問題ないので、(</xdr:oneCellAnchor)(</xdr:twoCellAnchor)\r\n$1で置換してしまえば読みやすくなる
  • xmlは<!-- -->でコメントアウトできるので、drawing1.xmlの内容が少なければ、手作業で怪しい箇所をコメントアウトして確認しても大した手間じゃないかも
  • <xdr:twoCellAnchor>の子要素に一意となるid的な属性を持っていることに後から気付いた。rebuildするファイルのファイル名をidにすると、特定した後、壊れた要素を削除するのが楽になるかも
  • Excelがdrawing1.xmlを読み込むモジュールが分かれば、該当箇所の特定と削除を1つのプログラムで完結できそう(となるとVBSかC#が適しているかな)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NetApp Data ONTAP device moduleを使ってNetAppの初期設定作業をPuppet化してみた

Puppet社が公開しているNetApp Data ONTAP device moduleの検証メモです。この記事ではData ONTAP SimulatorをVirtualBox上にディプロイして検証しています。

環境

  • Puppet MasterのOS : CentOS7
  • Puppet AgentのOS : CentOS7
  • Puppet : 6.0.5
  • Data ONTAP Simulator : 8.3
  • NetApp Manageability SDK : 5.7
  • Vagrant : 2.2.3
  • VirtualBox : 6.0.4

準備

ダウンロード

NetApp社のサイトからNetApp Manageability SDKをダウンロードします。事前にアカウント登録が必要です。

同じサイトからSimulate ONTAP 8.3 for VMware Workstation, VMware Player, and VMware FusionとCMode_licenses_8.3.txt をダウンロードします。ESXi版のovaと間違えないように注意が必要です。

Puppet社のサイトからPuppet Enterpriseをダウンロードします。

Puppetのセットアップ

Master側でダウンロード済みのPuppet Enterpriseのインストールバイナリを解いてインストールスクリプトを実行します。

$ sudo ./puppet-enterprise-installer
どのように続行しますか? [1]: 1
--省略--
## Puppet Enterpriseの設定が完了しました!
--省略--

puppetを実行してインストールは完了です。

$ sudo /opt/puppetlabs/bin/puppet agent -t

Agent側でAgentのインストールスクリプトを実行します。

$ curl -k https:<Puppet Masterのホスト名>:8140/packages/current/install.bash | sudo bash

Agent側でpuppetを実行してMasterに証明書のサインを要求します。

 sudo /opt/puppetlabs/bin/puppet agent -t

Master側で証明書にサインします。

$ sudo /opt/puppetlabs/bin/puppetserver ca sign --all
Successfully signed certificate request for device-proxy.puppet.com

再度Agent側でpuppetを実行します。エラーが出なければ成功です。

$ sudo /opt/puppetlabs/bin/puppet agent -t

NetApp Data ONTAP device moduleをMaster/Agent側両方にインストールします。

$ sudo puppet module install puppetlabs-netapp --version 1.3.0 --modulepath /etc/puppetlabs/code/environments/production/modules/
Notice: /etc/puppetlabs/code/environments/production/modulesへのインストールを準備をしています ...
Notice: https://forgeapi.puppet.comからダウンロードしています ...
Notice: インストール中 -- 中断しないでください ...
/etc/puppetlabs/code/environments/production/modules
└── puppetlabs-netapp (v1.3.0)

ダウンロード済みのNetApp Manageability SDKをほどいてnetapp-manageability-sdk-5.7/lib/ruby/NetApp配下にある4つのファイルをAgent側の/etc/puppetlabs/code/environments/production/modules/netapp/lib/puppet/netapp_sdkにコピーします。

$ cp ./netapp-manageability-sdk-5.7/lib/ruby/NetApp/* /etc/puppetlabs/code/environments/production/modules/netapp/lib/puppet/netapp_sdk

Master側でpuppet.confの最終行に autosign = true を追記して、autosign.confに対象のホスト名を追記します。

$ sudo vi /etc/puppetlabs/puppet/puppet.conf
--省略--
[master]
autosign = true
$ sudo vi ~/.puppetlabs/etc/puppet/autosign.conf
ontap
puppetsvm

Data ONTAP Simulatorのセットアップ

GitHubのリポジトリをクローンします。

$ git clone git@github.com:tlichten/vagrant-vsim.git

ダウンロード済みのNetApp Manageability SDKをほどかずにzipファイルのままコピーします。

$ cp netapp-manageability-sdk-5.7.zip ./vagrant-vsim

ダウンロード済みのSimulate ONTAP 8.3 for VMware Workstation, VMware Player, and VMware Fusionをコピーします。

$ cp vsim-netapp-DOT8.3-cm.ova ./vagrant-vsim

vsim.confを編集してライセンスキーを貼り付けます。ライセンスキーはダウンロード済みのCMode_licenses_8.3.txt:21行目に書いてあります。

$ vi vsim.conf
--省略--
CLUSTER_BASE_LICENSE="xxx"

$ 起動します。エラーが出なければ成功です。

$ vagrant up

$ Cluster ONTAPのノード管理コンソールにログインしてみます。VSIM::>というプロンプトが出れば成功です。

$ vagrant ssh vsim
VSIM::>

動作確認

Agent側でdevice.confを編集してData ONTAPの接続情報を記載します。

$ sudo vi /etc/puppetlabs/puppet/device.conf
[ontap]
type netapp
url https://admin:netapp123@VSIM-01

Master側でData ONTAPのノードからStorage Virtual Machineを作成するマニフェストを作成します。

$ sudo vi /etc/puppetlabs/code/environments/production/manifests/site.pp
node 'ontap' {
  netapp_aggregate { 'puppetaggr1':
    ensure    => present,
    diskcount => '8',
    raidtype => 'raid_dp',
    nodes => ['VSIM-01']
  }

  netapp_vserver { 'puppetsvm':
    ensure          => present,
    rootvol         => 'puppetsvm_root',
    rootvolaggr     => 'puppetaggr1',
    rootvolsecstyle => 'unix',
    allowedprotos   => 'nfs',
    state           => 'running'
  }

  netapp_security_login {'ssh:password:vsadmin:puppetsvm':
    ensure    => present,
    password  => 'netapp123',
    role_name => 'vsadmin',
    is_locked => false
  }

  netapp_lif { 'puppet_lif':
    ensure        => present,
    role          => 'data',
    homeport      => 'e0c',
    homenode      => 'VSIM-01',
    address       => '10.0.207.5',
    vserver       => 'puppetsvm',
    netmask       => '255.255.255.0',
    dataprotocols => ['nfs']
  }
}

Agent側でPuppetを実行してマニフェストを適用します。

$ sudo RUBYLIB=/etc/puppetlabs/code/environments/production/modules/netapp/lib/puppet/netapp_sdk puppet device --verbose
--省略--
Info: Node[ontap]: Unscheduling all events on Node[ontap]
Notice: Applied catalog in 0.93 seconds

Data ONTAPの管理コンソールにログインしてマニフェストの通りにリソースが作成されたか確認します。

VSIM::> aggr show

Aggregate     Size Available Used% State   #Vols  Nodes            RAID Status
--------- -------- --------- ----- ------- ------ ---------------- ------------
aggr0        855MB   35.97MB   96% online       1 VSIM-01          raid_dp,
                                                                   normal
puppetaggr1 5.27GB    3.24GB   39% online       2 VSIM-01          raid_dp,
                                                                   normal
2 entries were displayed.

VSIM::> vserver show
                               Admin      Operational Root
Vserver     Type    Subtype    State      State       Volume     Aggregate
----------- ------- ---------- ---------- ----------- ---------- ----------
VSIM        admin   -          -          -           -          -
VSIM-01     node    -          -          -           -          -
puppetsvm   data    default    running    running     puppetsvm_ puppetaggr
                                                      root       1
3 entries were displayed.

VSIM::> security login show

Vserver: VSIM
                             Authentication                  Acct
User/Group Name  Application Method         Role Name        Locked
---------------- ----------- -------------- ---------------- ------
admin            console     password       admin            no
admin            http        password       admin            no
admin            ontapi      password       admin            no
admin            service-processor
                             password       admin            no
autosupport      console     password       autosupport      no
vagrant          ontapi      password       admin            no
vagrant          ssh         password       admin            no
vagrant          ssh         publickey      admin            -

Vserver: puppetsvm
                             Authentication                  Acct
User/Group Name  Application Method         Role Name        Locked
---------------- ----------- -------------- ---------------- ------
vsadmin          ontapi      password       vsadmin          yes
vsadmin          ssh         password       vsadmin          yes
10 entries were displayed.

VSIM::> network interface show
            Logical    Status     Network            Current       Current Is
Vserver     Interface  Admin/Oper Address/Mask       Node          Port    Home
----------- ---------- ---------- ------------------ ------------- ------- ----
VSIM
            VSIM-01_mgmt_auto
                         up/up    10.0.207.3/24      VSIM-01       e0c     true
puppetsvm
            puppet_lif   up/up    10.0.207.5/24      VSIM-01       e0c     true
2 entries were displayed.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

条件文の分解(Decompose Conditional)

image.png

1つずつリファクタリング技法まとめ
個人的に簡単かつ取り入れ易いと思うものから

目的

すぐ引き出せるようにする

基本作業サイクル

  • システムを動かして仕様を精査
  • テストメソッドを作成
  • テストの失敗を確認
  • テストの成功を確認
  • 小さい変更、随時テスト実行(失敗確認->成功確認)
  • 最後テスト実行
  • 最後動作確認

条件文の分解(Decompose Conditional)とは

判定メソッド化
条件文をメソッドとして抽出し、メソッド名に意味を持たせること

ポイント

  • 算術演算子をifの条件式から消す。
  • 複数の条件式の示す意味をメソッド名にする。
  • コメントに記載してある内容をメソッド名にする。

# 平成
if i >= 1989 && i <= 2019

end

   ↓

def heisei?(i)
  i >= 1989 && i <= 2019
end

if heisei?(i)
  puts '多分平成'
end

書籍情報

Jay Fields (著), Shane Harvie (著), Martin Fowler (著), Kent Beck (著),
長尾 高弘(訳), リファクタリング:Rubyエディション
https://amzn.to/2VlyWML

雑感

スコープが狭くて影響が小さいものから

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rspec導入まとめ

最低限の導入手順を記載
後ほどいつもやりそうな初期設定を追記

事前設定

config/database.ymlにtestの指定がある事

config/database.yml
test:
  <<: *default
  database: プロジェクト名_test

テストを導入したい環境に以下のgemを指定している事

Gemfile
group :development, :test do
    
    
  gem 'rspec-rails', '~> 3.6'
  gem 'factory_bot_rails'
  gem 'database_cleaner'
    
    
end

導入

rspecに必要なモジュールをインストールする

bundle install
rspec:install
rails generate rspec:install

下記を追記

spec/support/factory_bot.rb
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

テスト用DBの作成

rails db:migrate RAILS_ENV=test

実行方法

rspec spec/対象の階層下 or ファイル
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】No route matches [GET]の原因と解決策

エラー

post/indexのブラウザ上で、postコントローラーのdestroyアクションを実施した結果、以下のエラーが起きた。

image.png

エラーの該当箇所

<%= link_to("削除", "/posts/#{@post.id}/destroy", {method: "delete"}) %>

解決策

rake routesでひとまず確認

rake routes
 posts_create POST   /posts/create(.:format)           posts#create
               GET    /posts/:id/edit(.:format)         posts#edit
               POST   /posts/:id/update(.:format)       posts#update
               POST   /posts/:id/destroy(.:format)      posts#destroy

解決策

routesと、メソッドを変えたら解決した。

①POSTをDELETEに変更

routesで、POST /posts/:id/destroy(.:format) を、DELETE /posts/:id/destroy(.:format)に変更

参考記事:https://railsguides.jp/routing.html

②link_toをbutton_toに変更

<%= button_to("削除", "/posts/#{@post.id}/destroy",method: :delete) %>

参考記事:https://qiita.com/y-temp4/items/2d50feb3ff0d65acdf67

メモ

link_toからbutton_toに変更すると、Viewはこうなった。

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

No route matches [GET]の原因と解決策

エラー

post/indexのブラウザ上で、postコントローラーのdestroyアクションを実施した結果、以下のエラーが起きた。

image.png

エラーの該当箇所

<%= link_to("削除", "/posts/#{@post.id}/destroy", {method: "delete"}) %>

解決策

rake routesでひとまず確認

rake routes
 posts_create POST   /posts/create(.:format)           posts#create
               GET    /posts/:id/edit(.:format)         posts#edit
               POST   /posts/:id/update(.:format)       posts#update
               POST   /posts/:id/destroy(.:format)      posts#destroy

解決策

routesと、メソッドを変えたら解決した。

①POSTをDELETEに変更

routesで、POST /posts/:id/destroy(.:format) を、DELETE /posts/:id/destroy(.:format)に変更

参考記事:https://railsguides.jp/routing.html

②link_toをbutton_toに変更

<%= button_to("削除", "/posts/#{@post.id}/destroy",method: :delete) %>

参考記事:https://qiita.com/y-temp4/items/2d50feb3ff0d65acdf67

メモ

link_toからbutton_toに変更すると、Viewはこうなった。

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】undefined method `name' for nil:NilClassの原因と解決策

エラー

2行目のpost.user.nameの「name」のメソッドが見当たらない

post.index
ActionView::Template::Error (undefined method `name' for nil:NilClass):
    1: <% @posts.each do |post| %>
    2:       <P><%= link_to(post.user.name, "/users/#{post.user.id}") %></p>
    3:       <p><%= link_to(post.content, "/posts/#{post.id}") %></p>
    4:   <% end %>

原因:postテーブル内の何かしらのレコードがなかった

考えられる原因は2つ

(1)postはpost.controller.rbに定義されていない
(2)データベースのpostモデルに、あるべきレコードがない

前者は、コントローラーで定義されていたので、後者の方に原因があると見立てた。

解決策:DBの全てのレコードを空にした

rake db:reset

全てのテーブルを dropして、"db/schema.rb" を元にテーブルを再作成する。(参考記事:http://o.inchiki.jp/obbr/183)

メモ

DBの全てのレコードを空にした後、改めて、post.createしたら無事にpost.indexに反映された。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

undefined method `name' for nil:NilClassの原因と解決策

エラー

2行目のpost.user.nameの「name」のメソッドが見当たらない

post.index
ActionView::Template::Error (undefined method `name' for nil:NilClass):
    1: <% @posts.each do |post| %>
    2:       <P><%= link_to(post.user.name, "/users/#{post.user.id}") %></p>
    3:       <p><%= link_to(post.content, "/posts/#{post.id}") %></p>
    4:   <% end %>

原因:postテーブル内の何かしらのレコードがなかった

考えられる原因は2つ

(1)postはpost.controller.rbに定義されていない
(2)データベースのpostモデルに、あるべきレコードがない

前者は、コントローラーで定義されていたので、後者の方に原因があると見立てた。

解決策:DBの全てのレコードを空にした

rake db:reset

全てのテーブルを dropして、"db/schema.rb" を元にテーブルを再作成する。(参考記事:http://o.inchiki.jp/obbr/183)

メモ

DBの全てのレコードを空にした後、改めて、post.createしたら無事にpost.indexに反映された。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む