TableView の実装(1) : hkob の雑記録 (454)

はじめに

hkob の雑記録の第454回目(通算27日目)は、TableView をテスト・実装していきます。

spec/views/table_view_spec.rb の作成

まず、View API に関しては blocks とは別にディレクトリを作成してここにテスト・実装を追加していきます。まずはディレクトリとファイルを作成します。

mkdir -p {spec,lib}/notion_ruby_mapping/views
touch spec/notion_ruby_mapping/views/{view,table_view}_spec.rb lib/notion_ruby_mapping/views/{view,table_view}.rb

まず、table_view_spec.rb を記載します。まずは retrieve して id が取得できることを確認します。

# frozen_string_literal: true

require_relative "../../spec_helper"

module NotionRubyMapping
  RSpec.describe TableView do
    tc = TestConnection.instance
    let!(:nc) { tc.nc }

    describe "find" do
      context "when real access" do
        let(:target) { View.find TestConnection::TABLE_VIEW_ID }

        it { expect(target).to be_a TableView }
        it { expect(target.id).to eq nc.hex_id(TestConnection::TABLE_VIEW_ID) }
      end

      context "when dry run" do
        let(:dry_run) { View.find TestConnection::TABLE_VIEW_ID, dry_run: true }

        it_behaves_like "dry run", :get, :view_path, id: TestConnection::TABLE_VIEW_ID
      end
    end
  end
end

ここで、bundle exec guard を実行したところ、こんな感じで警告が出るようになってしまいました。

> be guard
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/gems/4.0.0/gems/bundler-2.4.13/lib/bundler/rubygems_ext.rb:241: warning: already initialized constant Gem::Platform::JAVA
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/site_ruby/4.0.0/rubygems/platform.rb:279: warning: previous definition of JAVA was here
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/gems/4.0.0/gems/bundler-2.4.13/lib/bundler/rubygems_ext.rb:242: warning: already initialized constant Gem::Platform::MSWIN
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/site_ruby/4.0.0/rubygems/platform.rb:280: warning: previous definition of MSWIN was here
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/gems/4.0.0/gems/bundler-2.4.13/lib/bundler/rubygems_ext.rb:243: warning: already initialized constant Gem::Platform::MSWIN64
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/site_ruby/4.0.0/rubygems/platform.rb:281: warning: previous definition of MSWIN64 was here
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/gems/4.0.0/gems/bundler-2.4.13/lib/bundler/rubygems_ext.rb:244: warning: already initialized constant Gem::Platform::MINGW
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/site_ruby/4.0.0/rubygems/platform.rb:282: warning: previous definition of MINGW was here
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/gems/4.0.0/gems/bundler-2.4.13/lib/bundler/rubygems_ext.rb:245: warning: already initialized constant Gem::Platform::X64_MINGW
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/site_ruby/4.0.0/rubygems/platform.rb:284: warning: previous definition of X64_MINGW was here
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/gems/4.0.0/gems/bundler-2.4.13/lib/bundler/rubygems_ext.rb:247: warning: already initialized constant Gem::Platform::WINDOWS
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/site_ruby/4.0.0/rubygems/platform.rb:286: warning: previous definition of WINDOWS was here
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/gems/4.0.0/gems/bundler-2.4.13/lib/bundler/rubygems_ext.rb:248: warning: already initialized constant Gem::Platform::X64_LINUX
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/site_ruby/4.0.0/rubygems/platform.rb:287: warning: previous definition of X64_LINUX was here
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/gems/4.0.0/gems/bundler-2.4.13/lib/bundler/rubygems_ext.rb:249: warning: already initialized constant Gem::Platform::X64_LINUX_MUSL
/Users/hkob/.local/share/mise/installs/ruby/4.0.2/lib/ruby/site_ruby/4.0.0/rubygems/platform.rb:288: warning: previous definition of X64_LINUX_MUSL was here
18:34:16 - INFO - Guard::RSpec is running
18:34:16 - INFO - Guard is now watching at '/Users/hkob/Library/CloudStorage/Dropbox/ruby/notion_ruby_mapping'
[1] guard(main)> 

bundler が 2.4.13 と古いため、rubygems/platform.rb とぶつかっているようです。せっかくなので、久々に bundle update も実行してみました。

gem update bundler
gem update --system
bundle update

すると、ostruct の load error が出るようになってしまいました。development の時だけ、ostruct を読み込むようにしておきます。

  spec.add_development_dependency "ostruct"

faraday も 2.14.1 に上がってしまったので、WebMock の User-Agent のバージョンも上げておきます。

    def stub(method, json_file, path, code, payload = nil)
      request = {
        headers: {
          "Accept" => "*/*",
          "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
          "Authorization" => "Bearer #{notion_token}",
          "Notion-Version" => NotionRubyMapping::NOTION_VERSION,
          "User-Agent" => "Faraday v2.14.1",
        },
      }

ここまででとりあえず、TableView 以外のテストは全て通過しました。

Finished in 0.74452 seconds (files took 0.75217 seconds to load)
3072 examples, 0 failures, 1 error occurred outside of examples

エラー箇所は以下のようになっています。

NameError:
  uninitialized constant NotionRubyMapping::TableView
# ./spec/notion_ruby_mapping/views/table_view_spec.rb:6:in '<module:NotionRubyMapping>'
# ./spec/notion_ruby_mapping/views/table_view_spec.rb:5:in '<top (required)>'
No examples found.

TableView は当然作成していないので、まずファイルの読み込み部を記載します。

{
  (中略)
  views: %w[view table_view],
}.each do |key, values|
  values.each do |klass|
    require_relative "notion_ruby_mapping/#{key}/#{klass}"
  end

次に TableView クラスを生成します。基本は親の View で設定をします。

# frozen_string_literal: true

module NotionRubyMapping
  # Notion view
  class TableView < View
    # @return [String]
    def type
      "table"
    end
  end
end

当然、今度は View クラスがないと言われます。

NameError:
  uninitialized constant NotionRubyMapping::View

View クラスを作成します。

# frozen_string_literal: true

module NotionRubyMapping
  # Notion View object (Parent of TableView, BoardView, ListView, CalendarView, GalleryView, etc)
  class View
    # @return [String]
    def type
      raise NotImplementedError
    end
  end
end

これで find のエラーになりました。テストと雛形の作成までは完了です。

Failures:

  1) NotionRubyMapping::TableView find 
     Failure/Error: let(:target) { View.find TestConnection::TABLE_VIEW_ID }
     
     NoMethodError:
       undefined method 'find' for class NotionRubyMapping::View
     # ./spec/notion_ruby_mapping/views/table_view_spec.rb:11:in 'block (3 levels) in <module:NotionRubyMapping>'
     # ./spec/notion_ruby_mapping/views/table_view_spec.rb:13:in 'block (3 levels) in <module:NotionRubyMapping>'

find の実装

まず、find を実装してみます。ほぼ Block#find と同様になります。dry_run_script は Base のものと変わらないので、間借りします。

    # @param [String] id
    # @param [Boolean] dry_run
    # @return [View, String]
    def self.find(id, dry_run: false)
      nc = NotionCache.instance
      view_id = view_id id
      if dry_run
        Base.dry_run_script :get, nc.view_path(view_id)
      else
        nc.view view_id
      end
    end

find にはビューのリンクが渡される場合があるので、self.view_id で view_id を取得します。これもテストを先に記載します。

# frozen_string_literal: true

require_relative "../../spec_helper"

module NotionRubyMapping
  RSpec.describe View do
    describe "view_id" do
      it "extracts view id from url" do
        url = "https://www.notion.so/Notion-Ruby-Mapping-Table-View-1e5c8b9f0a3b4c8e9d2f1a2b3c4d5e6?v=1234567890abcdef1234567890abcdef"
        expect(View.view_id(url)).to eq "1234567890abcdef1234567890abcdef"
      end

      it "converts hex string to view id" do
        hex_str = "1e5c8b9f0a3b4c8e9d2f1a2b3c4d5e6"
        expect(View.view_id(hex_str)).to eq "1e5c8b9f0a3b4c8e9d2f1a2b3c4d5e6"
      end
    end
  end
end

self.view_id の実装は以下のようになります。

    # @param [String] str
    # @return [String] view_id
    def self.view_id(str)
      if /^http/.match str
        /\?v=([\da-f]{32})/.match(str)[1]
      else
        NotionCache.instance.hex_id str
      end
    end

次は NotionCache#view_path がないという警告が出ているので、view_path のテストを作成します。

    describe "view_path" do
      it { expect(nc.view_path("ABC")).to eq "v1/views/ABC" }
    end

実装は簡単です。

    # @param [String] view_id
    # @return [String] view_path
    def view_path(view_id)
      "v1/views/#{view_id}"
    end

NotionCache の実装追加

ここから先は既存の NotionCache への実装になります。NotionCache では名前の通り、一度読んだオブジェクトはキャッシュ保存されます。このため、object_for_key では object の状況で Base か View かを選択する必要が出てきます。

    # @param [String] id id (with or without "-")
    # @return [NotionRubyMapping::Base]
    def object_for_key(id)
      key = hex_id(id)
      return @object_hash[key] if @use_cache && @object_hash.key?(key)

      json = yield(@client)
      base_class = json["object"] == "view" ? View : Base
      ans = base_class.create_from_json json
      @object_hash[key] = ans if @use_cache
      ans
    end

NotionCache#view は object_for_key を使って以下のようになります。

    # @param [String] id
    # @return [NotionRubyMapping::View]
    def view(id)
      object_for_key(id) { view_request id }
    end

ここまで実装すると WebMock のエラーになりました。

       Real HTTP connections are disabled. Please stub: GET https://api.notion.com/v1/views/32fd8e4e98ab81e88ba9000c227aaf76 with headers {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'Bearer secret_J08RBf9SlofiMhuIeJZc6AnI5qHXwDOaq4RolJFaaZ0', 'Notion-Version'=>'2026-03-11', 'User-Agent'=>'Faraday v2.14.1'}

まず、generate_stubs に retrieve_view を追加し、retrive_view メソッドを作成します。

    def retrieve_view
      generate_stubs_sub :get, __method__, :view_path, {
        table_view: [TABLE_VIEW_ID, 200],
      }
    end

これで、View#create_from_json の実装を残すのみとなりました。

  2) NotionRubyMapping::TableView find when real access 
     Failure/Error: ans base_class.create_from_json json
     
     NoMethodError:
       undefined method 'create_from_json' for class NotionRubyMapping::View

View#create_from_json の実装

あとは JSON から View object を作成する create_from_json を実装します。

    # @param [Hash] json
    # @return [NotionRubyMapping::View]
    def self.create_from_json(json)
      base_class = {
        "table" => TableView,
      }[json["type"]]
      raise NotImplementedError, "View type #{json["type"]} is not implemented yet." unless base_class

      base_class.new json: json
    end

TableView#initialize を実装します。

  class TableView < View
    # @param [String, nil] id
    # @param [String, nil] json
    def initialize(id: nil, json: nil)
      super id: id, json: json
    end

親の initialize が呼ばれるので、View#initialize も実装します。

  class View
    # @param [String, nil] id
    # @param [String, nil] json
    def initialize(id: nil, json: nil)
      @nc = NotionCache.instance
      @json = json
      @id = @nc.hex_id(id || @json && @json["id"])
    end
    attr_reader :id, :json

ここまでで TableView オブジェクトが生成されたこと、id が取得されたこと、dry_run でスクリプトが生成されたことを確認できました。

NotionRubyMapping::TableView
  find
    when real access
      is expected to be a kind of NotionRubyMapping::TableView
      is expected to eq "32fd8e4e98ab81e88ba9000c227aaf76"
    when dry run
      behaves like dry run
        is expected to eq "#!/bin/sh\ncurl  'https://api.notion.com/v1/views/32fd8e4e98ab81e88ba9000c227aaf76' \\\n  -H 'Notion-Version: 2026-03-11' \\\n  -H 'Authorization: Bearer '\"$NOTION_API_KEY\"''"

Finished in 0.00587 seconds (files took 0.21088 seconds to load)
3 examples, 0 failures

おわりに

とりあえず、JSON からオブジェクトを生成するところまで実装しました。まだ、JSON を内包しているだけなので、これらを取り込む処理を明日以降にテスト・実装していきます。