Madogiwa Blog

主に技術系の学習メモに使っていきます。

Sidekiqがどうやって動いているのか、コードを読んで概要を掴めた気がしたのでメモしてみる

みなさん、こんにちは。まどぎわです。
rubyで非同期処理やるときのデファクトスタンダード的なgemsidekiqのコードを読んで、概要が割とつかめた気がしてきたので、どういう感じで動いてるか自分の理解の範囲でメモしてみました🙇

github.com

sidekiqの機能としては大きく分けて、

  • Redisへのqueueのpush
  • Redisからqueueのpopとjobの実行

だと思ったので、それについてsidekiqのコードと合わせて概要を整理してみました。
※記載しているコードについては、読みやすいコードを削除しているので全文が読みたい方は、それぞれのリンク先で確認いただけますと🙏

前提

今回調べたsidekiqのversionは、2019/04/28現在のmasterである、6.0.0.pre1です。

# frozen_string_literal: true

module Sidekiq
  VERSION = "6.0.0.pre1"
end

Redisへのqueueのpush

非同期処理の呼び出しは、perform_asyncによって行われる。 引数にselfargsを与えて、client_pushを呼び出している。selfには、hogeJob等のjob設定される。

def perform_async(*args)
  client_push("class" => self, "args" => args)
end

https://github.com/mperham/sidekiq/blob/b76dfb9e056d6c17d7c2fe66dd9ab3f38aa5423f/lib/sidekiq/worker.rb#L92

client_pushではredisへ接続するためのpoolを取得し、引数を文字列に変換してSidekiq::Client.new(pool).push(item)を呼び出している。

def client_push(item) # :nodoc:
  # redisに接続するためのpoolを取得
  pool = Thread.current[:sidekiq_via_pool] || get_sidekiq_options["pool"] || Sidekiq.redis_pool
  # stringify
  item.keys.each do |key|
    item[key.to_s] = item.delete(key)
  end
  
  # redisへのpush処理の呼び出し
  Sidekiq::Client.new(pool).push(item)
end

https://github.com/mperham/sidekiq/blob/b76dfb9e056d6c17d7c2fe66dd9ab3f38aa5423f/lib/sidekiq/worker.rb#L142

pushメソッドでredisへの登録処理を呼び出している👀

module Sidekiq
  class Client
    def push(item)
      # to_sとかしてベーシックなjsonのような形式に変換
      normed = normalize_item(item)
      # middrewareを実行してitemを返却
      payload = process_single(item["class"], normed)
    
      if payload
        # radisのpush処理
        raw_push([payload])
        # jidを返す
        payload["jid"]
      end
    end
    
    private
    
    # radisへのpush処理
    def raw_push(payloads)
      @redis_pool.with do |conn|
        conn.multi do
          atomic_push(conn, payloads)
        end
      end
      true
    end
    
    # jsonに変換してconnectionを使ってradisに登録
    def atomic_push(conn, payloads)
      # scheduledの場合
      if payloads.first["at"]
        conn.zadd("schedule", payloads.map { |hash|
          at = hash.delete("at").to_s
          [at, Sidekiq.dump_json(hash)]
        })
      # 通常の場合
      else
        queue = payloads.first["queue"]
        now = Time.now.to_f
        to_push = payloads.map { |entry|
          entry["enqueued_at"] = now
          Sidekiq.dump_json(entry)
        }
        # ここがredisへの登録処理の本丸
        conn.sadd("queues", queue)
        conn.lpush("queue:#{queue}", to_push)
      end
    end

https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/client.rb#L69

queueから取り出して実行

sidekiqの起動

sidekiqの起動はCLI.runで行われる。runの中で、Sidekiq::Launcherインスタンスが作成されてlauncher.runが実行される。

module Sidekiq
  class CLI
   def run
      boot_system
      if environment == "development" && $stdout.tty? && Sidekiq.log_formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
        print_banner
      end
      # 省略: radisのversionのチェックとかいろいろやる
      launch(self_read)
    end
    
    def launch(self_read)
      # 省略
      @launcher = Sidekiq::Launcher.new(options)
      begin
        launcher.run
        while (readable_io = IO.select([self_read]))
          signal = readable_io.first[0].gets.strip
          handle_signal(signal)
        end
        # 省略
      end
    end

https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/cli.rb#L36

Launcher#runの中では、スレッドの作成、ポーリングの開始、Manager#startが呼ばれます。 ※Pollerは、N秒に一回scheduleされたjobがあればradisのqueueに入れるようなことをやってる。

module Sidekiq
  class Launcher
    def initialize(options)
      @manager = Sidekiq::Manager.new(options)
      @poller = Sidekiq::Scheduled::Poller.new
      @done = false
      @options = options
    end

    def run
      @thread = safe_thread("heartbeat", &method(:start_heartbeat))
      @poller.start
      @manager.start
    end
  end
end

https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/launcher.rb#L34

Manager#newの中で並列実行数concurrencyの数だけ、worker(Processor.new(self))を作成して、Manager#startで、全てstartさせる。

module Sidekiq
  class Manager
    def initialize(options = {})
      # 省略
      @count = options[:concurrency] || 10
      @count.times do
        @workers << Processor.new(self)
      end
      # 省略
    end

    def start
      @workers.each do |x|
        x.start
      end
    end

https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/manager.rb#L43

RadisからqueueのpupとJobの実行

Processorがメインの処理、ここでqueueから取り出したjobを実行している。
ざっくりとした流れは、

  1. runが実行され、process_oneが終了されるまで実行され続ける。
  2. process_oneの中でredisからdequeueして、引数workとしてprocessに渡す。
  3. processの中で、workから各種情報を引き出し、dispathを呼び出して、worker(jobのインスタンス)を作成して、execute_jobworker.performを実行し、処理を実行している。

※下記が読みやすいように、いろいろ省略したSidekiq:: Processorのコードです。

module Sidekiq
  class Processor
    def initialize(mgr)
      # 省略
      @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
      @reloader = Sidekiq.options[:reloader]
      @job_logger = (mgr.options[:job_logger] || Sidekiq::JobLogger).new
      @retrier = Sidekiq::JobRetry.new
    end
    
    # runが実行される。
    def start
      @thread ||= safe_thread("processor", &method(:run))
    end
    
    def run
      # ここがdoneになるまで繰り返されてるので、
      # queueから取り出されてjobを実行するという処理が、ずっと続く。
      process_one until @done
      @mgr.processor_stopped(self)
      # 省略
    end
    
    # ここで、radisからqueを取り出してjobに入れてprocessで処理を開始
    def process_one
      @job = fetch
      process(@job) if @job
      @job = nil
    end
    
    # 取得して、終了してたらもう一回queueに入れ直すような処理
    def fetch
      j = get_one
      if j && @done
        j.requeue
        nil
      else
        j
      end
    end
    
    # BasicFetch#retrieve_workでradisからパースしたオブジェクトを取得
    # https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/fetch.rb#L36
    def get_one
      work = @strategy.retrieve_work
      # 省略
      work
      # 省略
    end

    # jobの実行処理
    def process(work)
      # このへんはradisから取り出したオブジェクトをいい感じにする処理
      jobstr = work.job
      queue = work.queue_name
     job_hash = Sidekiq.load_json(jobstr)

      ack = true
      begin
        # この中でmiddrewareを実行して、workerと引数を渡してjobを実行してる。
        dispatch(job_hash, queue) do |worker|
          Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
            execute_job(worker, cloned(job_hash["args"]))
          end
        end
      rescue Sidekiq::Shutdown
        ack = false
      rescue Sidekiq::JobRetry::Handled => h
        raise e
      rescue Exception => ex
        raise e
      ensure
        work.acknowledge if ack
      end
    end
    
    # worker(jobのインスタンス)のworker.performを呼び出している。
    def execute_job(worker, cloned_args)
      worker.perform(*cloned_args)
    end
    
    # ここでlog出したり、workerをjobのclassのインスタンスに変更してる。
    def dispatch(job_hash, queue)
      pristine = cloned(job_hash)

      @job_logger.with_job_hash_context(job_hash) do
        @retrier.global(pristine, queue) do
          @job_logger.call(job_hash, queue) do
            stats(pristine, queue) do
              @reloader.call do
                klass  = constantize(job_hash["class"])
                worker = klass.new
                worker.jid = job_hash["jid"]
                @retrier.local(worker, pristine, queue) do
                  yield worker
                end
              end
            end
          end
        end
      end
    end

https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/processor.rb#L60

おわりに

今回はsidekiqがどのように動いているか、コードを読みながら概要をまとめてみました。

Radisからpush/popしているためjson形式のやり取りになり、symbolやmodelのインスタンス等を引数で渡すと、いろいろ問題が起きてしまうんですね。(ActiveJobはこの辺をよしなにやってくれてそうです。)

またコードを読む中で変数名が省略されている箇所があったので、PR送ったらmergeしてもらえたので、sidekiqのコントリビューターになりました🙌

github.com

(また地味にwikiも修正してたりします)
https://github.com/mperham/sidekiq/wiki/Best-Practices/_history

RubyKaigi 2019 Cookpad Daily Ruby Puzzlesを解いてみたので自分の解答をメモ

みなさん、こんにちは。まどぎわです(・∀・)
RubyKaigiでCookpadさんのブースで出題されていたRuby Puzzleを、Ruby Kaigi中に楽しくやっていたので、 解答も下記で公開されたので、自分の解答をメモしておきます。

techlife.cookpad.com

Day 1

Problem 1-1

# Hint: Use Ruby 2.6.
puts "#{"Goodbye" .. "Hello"} world"

My Answer(2文字)

puts "#{"Goodbye" .. && "Hello"} world"

HintにあるようにRuby2.6から追加された終端なしRangeを使って、 "Hello"だけを返せればいいのかなと思ったので&&を使ってみました。

www.ruby-lang.org

Problem 1-2

puts&.then {
  # Hint: &. is a safe
  # navigation operator.
  "Hello world"
}

My Answer(2文字)

puts 1&.then {
  # Hint: &. is a safe
  # navigation operator.
  "Hello world"
}

これはputsの返り値が、nilになってしまってthen以降が実行されなくなっていたので、 puts 1&.thenとして、putsの返り値を使用しないようにしてました。

Problem 1-3

include Math
# Hint: the most beautiful equation
Out, *,
     Count = $>,
             $<, E ** (2 * PI)
Out.puts("Hello world" *
         Count.abs.round)

My Answer (2文字)

include Math
# Hint: the most beautiful equation
Out, *,
     Count = $>,
             $<, E ** (2 * PI*0)
Out.puts("Hello world" *
         Count.abs.round)

Countが1に近い値になれば、"Hello world"が1回実行されるので、 E ** (2 * PI*0)として、1に近い値を作りました。

Day 2

Problem 2-1

def say
  -> {
    "Hello world"
  }
  # Hint: You should call the Proc.
  yield
end

puts say { "Goodbye world" }

My Answer(1文字)

def say
  -> {
    "Hello world"
  }.
  # Hint: You should call the Proc.
  yield
end

puts say { "Goodbye world" }

これは.yield.callと同じなので、それを使うようにしました。

docs.ruby-lang.org

Problem 2-2

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  yield "Hello world"
end

puts e.next

My Answer(2文字)

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  g.yield "Hello world"
end

puts e.next

gEnumerator::Yielderが返却される。

docs.ruby-lang.org

そして検索してたら下記のようなページを発見し、g.yieldと書けそうなことがわかったのでそれを使いました。

qiita.com

Problem 2-3

$s = 0
def say(n = 0)
  $s = $s * 4 + n
end

i, j, k = 1, 2, 3

say i
say j
say k

# Hint: Binary representation.
$s != 35 or puts("Hello world")

My Answer(2文字)

$s = 0
def say(n = 0)
  $s = $s * 4 + n
end

i, j, k = 1, 2, 3

say i
say j
say k

# Hint: Binary representation.
$s+8 != 35 or puts("Hello world")

$sが27を返すので、35となるように+8しました。

Day 3

Problem 3-1

def say s="Hello", t:'world'
  "#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.

puts say :p

My Answer(1文字)

def say s="Hello", t:'world'
  "#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.

puts say t:p

pの返り値はnilなので、s="Hello", t:nilとなり、"Hello world"が出力されます。

Problem 3-2

def say s, t="Goodbye "
  # Hint: You can ignore a warning.
  s = "#{ s } #{ t }"
  t + "world"
end

puts say :Hello

My Answer(3文字)

def say s, t="Goodbye "
  # Hint: You can ignore a warning.
  st = "#{ s } "#{ t }"
  st + "world"
end

puts say :Hello

ここは最初say :Hello,pのような形でいけるかと思ったのですが、 :Helloはsymbolのため+メソッドが使えないのです。。。

なので、st = "#{ s } "とすることで、stに"Hello"だけが入るようにして、 st + "world"として"Hello world"となるようにしました。

Problem 3-3

def say
  "Hello world" if
    false && false
  # Hint: No hint!
end

puts say

My Answer(2文字)

def say
  "Hello world" if
    :false && :false
  # Hint: No hint!
end

puts say

これは単純で、falsetrueになるようにすればいいので、 !falseと思ったのですが、それだとあまりにもと思ったので:falseにしました。

Extra Stage

Ruby Kaigi後に出題されたExtra Stageについてもやってみたので、解答をメモしておきます✍

"Cookpad Daily Ruby Puzzles" in RubyKaigi 2019 - Extra Stage · GitHub

⚠️⚠️⚠️ここからは解答がまだ発表されていないので、ネタバレ注意です⚠️⚠️⚠️























Problem Extra-1

Hello = "Hello"

# Hint: Stop the recursion.
def Hello
  Hello() +
    " world"
end

puts Hello()

My Answer(2文字)

Hello = "Hello"

# Hint: Stop the recursion.
def Hello
  Hello+#() +
    " world"
end

puts Hello()

これはHintの通り、メソッド呼び出しを止めればいいので、Hello+#() +として、変数側のHelloを参照するようにしました。

Problem Extra-2

s = ""
# Hint: https://techlife.cookpad.com/entry/2018/12/25/110240
s == s.upcase or
  s == s.downcase or puts "Hello world"

My Answer(1文字)

s = "Dz"
# Hint: https://techlife.cookpad.com/entry/2018/12/25/110240
s == s.upcase or
  s == s.downcase or puts "Hello world"

これはHintのURLを参考したら該当文字Dzが載っていたので、それを使うようにしました。

Problem Extra-3

def say
  s = 'Small'
  t = 'world'
  puts "#{s} #{t}"
end

TracePoint.new(:line){|tp|
  tp.binding.local_variable_set(:s, 'Hello')
  tp.binding.local_variable_set(:t, 'Ruby')
  tp.disable
}.enable(target: method(:say))

say

My Answer(2文字)

def say
  s = 'Small'
  t = 'world'
  puts "#{s} #{t}"
end

TracePoint.new(:line){|tp|
  tp.binding.local_variable_set(:s, 'Hello')
  #tp.binding.local_variable_set(:t, 'Ruby')
  #tp.disable
}.enable(target: method(:say))

say

tp.disableコメントアウトしたところ"Hello Ruby"が返されるようになったので、 tp.binding.local_variable_set(:s, 'Hello')だけが実行されるようにしました。

おわりに

Cookpadさんの問題が、面白すぎてセッションに集中出来なかった部分もありますが・・・笑
問題に取り組むなかで、マニュアルを読んだり、いろいろ調べたりするなかで、 rubyのイディオムやリテラル周りの知識が見について非常に勉強になりました🙌

来年のRuby Kaigiでは、もっといい感じの答えが出せるように頑張ろう💪

rails勉強botでActionViewのメソッドが呟かれるようになりました🎉

みなさん、こんばんは。 昨日までrubykaigi2019で福岡に行ってたまどぎわです💎🍜

表題の通り、rails勉強botActionViewのメソッドがつぶやかれるようになりました🙌

ActionViewのメソッドは、割と忘れがちな気もするので気になるメソッドがあったら見てみてください👀✨

👇ActionViewrailsガイドでいうとこの辺です

railsguides.jp

またrails勉強botの中身をリファクタリング(?)して、簡単につぶやくClassを追加できるようにしました🎉

こんな感じで追加出来ます!簡単🙌

class MessageBuilder::ActionView::Base < MessageBuilder::Base
  RAILS_CLASS = ActionView::Base
  RAILS_TOP_CLASS_NAME = 'actionview'.freeze
  RAILS_CLASS_FILE_REGXP = /action_view.*/.freeze
  RAILS_CLASS_REGXP = /action_view/.freeze
end

MessageBuilder::Baseにclassからメソッドをランダムに取得したり、 メソッドのsource_locationを取得して、Githubへのリンクを生成したりする処理を実装して、 それらを継承するようにしました。

(一旦楽になったけど、継承という選択が正しかったのかどうかは微妙・・・。一応、MessageBuilder::ActionView::Baseは、MessageBuilderであるはis-aの関係になっている気もするので、まぁいいかという気持ち🙈)

👇MessageBuilder::Baseの実装が気になる方はこちら

github.com

ruby勉強botも主要classしか呟かないようにしてたけど、rubykaigiに参加してTracepointとかコアなclassの内容も少し知りたくなってきたので、ちょっとつぶやくClassを追加していこうかなと思いました💪

それでは😴

はてなブログの記事を取得してカード表示するVueComponentを作った📦✨

はてなブログの記事を取得してカード表示するVueComponentを作ったので、そのへんの話をメモしておきますm( )m

コードだけ見たい人はこちら👇

はてなブログの記事をカードで表示するVueコンポーネント · GitHub

イメージ

このような形で表示されます🙌 f:id:madogiwa0124:20190413193738p:plain

使い方

BlogsコンポーネントPropsとしてendpointrss取得用のurl(https://madogiwa0124.hatenablog.com/rss等)と、displayCountに表示件数を設定してください。

<Blogs
  endpoint="https://madogiwa0124.hatenablog.com/rss"
  displayCount="6"
/>

カードのデザインを修正したい場合は、BlogCard.vuestyleを変更していただければ👩‍🎨

実装のはなし

はてなブログの記事を取得する

はてなブログの記事一覧を取得するのは意外と簡単で、ブログURL/rssrssフィードを取得できるので、まずはそれを取得します。axiosを使ってリクエストを送信すればOKです👀

import axios from 'axios';
public created() {
  axios.get(this.endpoint).then((res)  => { const rss = res.data });
}

取得した記事をParseする

取得した記事のParseには、DOMParserを使用しています。RSSxmlなのでtext/xmlを指定してあげればParse出来ます👀
buildRssBlogItemsitem(各記事)をDocumentにParseしてblogPropsでthumbnail、title、linkを持つobjectを生成してBlogの一覧(blogs)を作成しています🙌

  import axios from 'axios';
  
  public blogs: object[] = [];
  public parser: DOMParser = new DOMParser();

  public created() {
    axios.get(this.endpoint).then((res)  => {
      const rssDom = this.parser.parseFromString(res.data, 'text/xml');
      this.buildRssBlogItems(rssDom);
    });
  }
  private buildRssBlogItems(dom: Document): void {
    dom.querySelectorAll('item').forEach((item) => {
      const itemDom = this.parser.parseFromString(item.innerHTML, 'text/html');
      this.blogs.push(this.blogProps(itemDom));
    });
    this.blogs = this.blogs.slice(0, this.displayCount);
  }
  private blogProps(dom: Document): object  {
    const thumbnail = dom.querySelector('enclosure')!.getAttribute('url');
    const title = dom.querySelector('title')!.text;
    const link = dom.querySelector('body')!.firstChild!.textContent!.trim();
    return { title, thumbnail, link };
  }

おわりに

今回ははてなブログの記事の一覧を表示するVueコンポーネントを作成しました📦
DOMParserを今まであまり使ったことなかったのですが、でHTMLやXMLをParseできるのでスクレイピングの結果やRSSを使って、いろいろ出来そうですね👀

Vue.jsでpropsを使ってimgタグのsrc属性を設定する方法

Vue.jsを使っていて親要素から子要素へpropsを使ってimgタグのsrc属性を設定する方法で、ハチャメチャにハマってましたが、一応出来たのでやり方をメモしておきます✍

今回やりたかったこと

下記のような親子関係のコンポーネントを作って親コンポーネントとからpropsを使って子コンポーネントfileSrcを渡して、そのままassets配下の画像のURLを設定したかった。

// Skill.vue
<template>
  <div class="skills">
    <Card fileSrc="../asstes/ruby.png" />
  </div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import Card from './Card.vue';

@Component({
  components: { Card },
})
export default class AboutMe extends Vue {
}
</script>
<style lang="scss" scoped>
</style>
// Card.vue
<template>
  <div class="card">
    <div class="card--image">
      <img :src="fileSrc" />
    </div>
  </div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';

@Component
export default class Card extends Vue {
  @Prop() public fileSrc!: string;
}
</script>
<style lang="scss" scoped>
</style>

しかし、上記のやり方だとError in render: "Error: Cannot find module '../asstes/ruby.png'"が発生し、パスの解決が出来ずに画像が表示されなかった😭

またネット上の情報では:src = require(path)とすれば良いといった情報があったが、この対応を行っても同様のエラーが発生し、上手くいかなかった😭

vue-loader-v14.vuejs.org

forum.vuejs.org

解決策

下記のようにloadImg()メソッドを定義し、:src="loadImg()"としたところ解決した👀
ポイントは「require(../assets/${this.fileName})」です、引数ではファイル名だけ渡すようにして、 式展開をおこなってpathを生成してrequireしてあげると上手くいきました(正しい解決方法かどうかはあれですが。。。)💦

// Card.vue
<template>
  <div class="card">
    <div class="card--image">
      <img :src="loadImg()" />
    </div>
  </div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';

@Component
export default class Card extends Vue {
  @Prop() public fileSrc!: string;
  
  public loadImg(): any {
    return require(`../assets/${this.fileName}`);
  }
}
</script>
<style lang="scss" scoped>
</style>

ちょっといろいろ引数を変えてみてみたのですが、謎・・・🤔

return require(`../assets/${this.imgName}`); //OK
return require('../assets/image.png'); // OK
return require(this.imgSrc); // NG

変数を引数に渡してしまうと上手くパスを解決出来ないみたいですね😥

javascript: Cloud Firestoreのはじめ方とCRUD系クエリMEMO🔥

ちょっとCloud Firestoreを触ってみたので、はじめ方とCRUD系のクエリのサンプルとか次触る時に忘れそうなのでメモしておきます✍
※サンプルコードはjavascriptです🙇‍♂️

Cloud Firestoreとは?

Cloud FirestoreとはFirebaseのキーバリューストアです📦

Cloud Firestore は、Firebase と Google Cloud Platform からのモバイル、ウェブ、サーバー開発に対応した、柔軟でスケーラブルなデータベースです。 https://firebase.google.com/docs/firestore/?hl=ja

Realtime Databaseとの違いは下記のようです👀
(私はあまりわかってません😇)

Realtime Database は従来からある Firebase のデータベースです。リアルタイムのクライアント間同期が必要なモバイルアプリのための、効率的でレイテンシが低いソリューションです。 Cloud Firestore は、Firebase のモバイルアプリ開発用の新しい主力データベースです。直感的な新しいデータモデルで、Realtime Database をさらに効果的にしています。Cloud Firestore は、Realtime Database よりも多彩で高速なクエリと高性能なスケーリングが特長です。

Cloud Firestoreをとりあえず使ってみる

使い方は簡単で下記の手順で画面でポチポチするだけでCloud Firestoreを立ち上げることができました🙌

  1. Firebase Consoleから新しいプロジェクトを作成
  2. Databaseからデータベースの作成をクリック
  3. 表示されたモーダルでセキュリティルールを設定し有効にするをクリック(動作確認で使ったので、今回はテストモードで作成しました。)

あとは、公式ドキュメントの通りにFirebaseをアプリに導入しました🔥

$ npm install firebase --save
var firebase = require("firebase");
// configの情報は、Project Overviewのアプリを追加にあるWeb等のアイコン押下時に表示されるPopupから設定
var config = {
  apiKey: "hoge",
  authDomain: "sample-project-zzzzz.firebaseapp.com",
  databaseURL: "https://sample-project-zzzzz.firebaseio.com",
  projectId: "sample-project-zzzzz",
  storageBucket: "sample-project-zzzzz.appspot.com",
  messagingSenderId: "99999999999"
};
// configの内容で初期化
firebase.initializeApp(config);

// firestore管理用のobjectを生成
var db = firebase.firestore();

詳しい始め方は、下記の公式ドキュメントを参照してください👀

firebase.google.com

firebase.google.com

CRUD系のクエリMEMO

とりあえず基本的なCRUD系のクエリの発行方法をメモしておきます。

データ追加

.set: 1件のデータを追加する

// docRef: 登録パスを設定
var docRef = db.collection('users').doc('alovelace');

// .setの引数に値をobject形式で設定
var setAda = docRef.set({
  first: 'Ada',
  last: 'Lovelace',
  born: 1815
});

データの取得

.get: collectionに紐づくデータのリストを取得する

// collection('collection_name').get()でPromiseが返却される
db.collection('users').get().then((snapshot) => {
  // .idでid、.data()でプロパティを取得出来る
  snapshot.forEach((doc) => { console.log(doc.id, doc.data()); });
}).catch((err) => { console.log(err); });

.doc(docRef): ドキュメントを取得する

// .docにdocRefを渡すと該当するドキュメントを取得出来る。
db.collection("collection_name").doc(docRef)

データの削除

.delete: ドキュメントを削除する

// .docで取得したドキュメントに対して.delete()で削除出来る
db.collection("collection_name").doc(docRef).delete()

データの更新

.update: ドキュメントを更新する

// 取得したドキュメントに.updateを呼び出して引数に`key: 値`で更新できます
// Promiseが返却されます
var docRef = db.collection("cities").doc("SF");
docRef.update({ name: 'San Francisco_2' }).then(() => {
  console.log('updated')
}).catch(e => { console.log(e) })

おわりに

今回はCloud Firestoreの始め方と、基本的なCRUD系のクエリを整理してみました✍
ちょっと触ってみた感じですが手軽に始められて、割と扱いやすい感じに見えたので、 個人サービスで登録時にそこまでデータの加工等が必要ないサービスであれば、 フロントエンド + Cloud Firestoreの構成でも良さそうな気がしました👀!

ピュアRubyでAtomも対応したRSS Parserを作ってみたMEMO

みなさん、こんにちは(・∀・) rubyの標準RSSライブラリが思ったよりも高機能でびっくりしたので、gemを使わずにAtomRssのParserを作ってみたので、そのへんのやり方をメモしておきますm( )m

Ruby標準のRSSライブラリ

標準ライブラリを使用する場合は、下記のような形でhttpリクエストを送信したParseされたRSSフィードを取得することができます👀

require 'rss'
rss_source = Net::HTTP.get(URI.parse(endpoint))
rss = RSS::Parser.parse(rss_source)
=> #<RSS::Rss:0x00007fe3dfcc0bd0...

RSS::Parser.parseの返り値は、引数で渡されたRSSの形式によって 、Rssの場合はRSS::RssAtomの場合は、RSS::Atom::Feedのオブジェクトが返却されます。(デフォルトでAtomまで対応してる🙌)

厳密には、下記とのこと

  • RSS 1.0をパースした場合は RSS::RDF オブジェクト
  • RSS 0.9x/2.0をパースした場合は RSS::Rss オブジェクト
  • Atom をパースした場合は RSS::Atom::Feed オブジェクト

下記に詳細が乗っています👀
https://docs.ruby-lang.org/ja/latest/library/rss.html

共通のプロパティを持つオブジェクトを返却するRSSParserを作ってみる

標準ライブラリでAtomでもRssでも共通のオブジェクトを返却するRSSParserを作ってみようと思います👩‍🔧

イメージは下記のような感じ👀

# AtomでもRssでもParsedItemのオブジェクトが返却される
RssCliant.new(endpoint).parsed_items
=> [#<ParsedItem:0x00007fe3e2e3ab48
  @description="みなさん、...",
  @eye_catching_image="https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png",
  @link="https://madogiwa0124.hatenablog.com/entry/2019/03/09/194825",
  @published_at=2019-03-09 19:48:25 +0900,
  @title="vue-cliで作ったアプリをGithub Pagesでサクッとリリースする">,
 #<ParsedItem:0x00007fe3e1c0ce08
  @description=
   "自分が作っている...",
  @eye_catching_image="https://cdn-ak.f.st-hatena.com/images/fotolife/m/madogiwa0124/20190303/20190303213625.gif",
  @link="https://madogiwa0124.hatenablog.com/entry/2019/03/03/214203",
  @published_at=2019-03-03 21:42:03 +0900,
  @title="railsとVueを使って無限スクロール機能を実装するMEMO🌀">,

まずはCliant部分を作ってみる

まずは下記のようなRssCliantをというClassを作ってみました👀 endpointを受け取って標準ライブラリを使ってParse、その後AtomRssかによって使用するParserを切り替え共通のオブジェクトを返却します。

class RssClient
  def initialize(endpoint)
    @endpoint = endpoint
    @rss_source = Net::HTTP.get(URI.parse(endpoint))
  end

  attr_reader :endpoint, :rss_source

  # 不正な形式だった場合に、バリデーションなしでParseする
  def parsed_rss!
    RSS::Parser.parse(rss_source)
  rescue RSS::InvalidRSSError
    RSS::Parser.parse(rss_source, false)
  end

  def parsed_items
    parsed_xml = parsed_rss!
    # オブジェクトのClass名でAtom or Rss用のParserを使って共通のオブジェクトにParse
    case parsed_xml.class.name
    when 'RSS::Atom::Feed' then Parser::Atom.call(parsed_xml)
    when 'RSS::Rss' then Parser::Rss.call(parsed_xml)
    else []
    end
  end
end

Atom or RssのParser部分

Rss.parseでParseされたXMLを引数をもとに、build_parsed_itemPasedItemという共通のオブジェクトを生成し、それらのリストを返却するようにしています🙌 他の形式に対応する場合はParser::Hogeが増えていくようなイメージですね👀
※またAtomの場合は、記事のアイキャッチ画像の取得方法が、ちょっと不明だったので一旦nilを設定するようにしてます💦

Rss

class Parser::Rss
  def self.call(parsed_xml)
    new.call(parsed_xml)
  end

  def call(parsed_xml)
    @parsed_xml = parsed_xml
    items
  end

  attr_reader :parsed_xml

  def items
    @items = parsed_xml.items.map { |item| build_parsed_item(item) }
  end

  def build_parsed_item(item)
    Parser::ParsedItem.new(
      title: item.title,
      description: item.description,
      published_at: item.pubDate,
      link: item.link,
      eye_catching_image: item.enclosure&.url
    )
  end
end

Atom

class Parser::Atom
  def self.call(parsed_xml)
    new.call(parsed_xml)
  end

  def call(parsed_xml)
    @parsed_xml = parsed_xml
    items
  end

  attr_reader :parsed_xml

  def items
    @items = parsed_xml.entries.map { |entry| build_parsed_item(entry) }
  end

  def build_parsed_item(item)
    Parser::ParsedItem.new(
      title: item.title.content,
      description: item.content.content,
      published_at: item.published.content,
      link: item.link.href,
      eye_catching_image: nil # AtomにアイキャッチのURLなさそうなので一旦NULLを設定
    )
  end
end

完成形

これでイメージどおりの完成形ができました🙌

# AtomでもRssでもParsedItemのオブジェクトが返却される
RssCliant.new(endpoint).parsed_items
=> [#<ParsedItem:0x00007fe3e2e3ab48
  @description="みなさん、...",
  @eye_catching_image="https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png",
  @link="https://madogiwa0124.hatenablog.com/entry/2019/03/09/194825",
  @published_at=2019-03-09 19:48:25 +0900,
  @title="vue-cliで作ったアプリをGithub Pagesでサクッとリリースする">,
 #<ParsedItem:0x00007fe3e1c0ce08
  @description=
   "自分が作っている...",
  @eye_catching_image="https://cdn-ak.f.st-hatena.com/images/fotolife/m/madogiwa0124/20190303/20190303213625.gif",
  @link="https://madogiwa0124.hatenablog.com/entry/2019/03/03/214203",
  @published_at=2019-03-03 21:42:03 +0900,
  @title="railsとVueを使って無限スクロール機能を実装するMEMO🌀">,

おわりに

今回はRubyの標準ライブラリだけを使ってRssParserを実装してみました。RssParserはFeedjira等のGemが有名ですが、そんなに凝ったことしないのであればRubyの標準ライブラリが使いやすく高機能なので充分なのでは?という気持ちになりました🙌

他にも標準ライブラリには便利そうな機能がありそうだったので、ちょっと見てみると良さそうですね👀