NotionRubyMapping のアップデート(11) : hkob の雑記録 (167)

はじめに

hkob の雑記録の第167回目は、ページアイコンをアップロードする set_icon をテスト・実装します。

FileObject のコンストラクタの修正

FileUploadObject を FileObject に登録できるようにするために、コンストラクタのテストを追加します。今回、File のアップロード部分までテストするのは面倒なので、FileUploadObject は instance_double で用意します。

    let(:file_upload_object) { instance_double(FileUploadObject, id: TestConnection::FILE_UPLOAD_IMAGE_ID) }

テストは以下のようになりました。

    describe "constructor" do
      context "with url" do
        (中略)
      end

      context "with file_upload_object" do
        subject { FileObject.new file_upload_object: file_upload_object }

        it { expect(subject.file_upload_object).to eq file_upload_object }
        it { expect(subject.type).to eq :file_upload }
        it { expect(subject.will_update).to be_falsey }
      end

      context "with json" do
        (中略)
      end

      context "without arguments" do
        (中略)
      end
    en

実装を以下のように修正しています。file_upload_object の引数が追加されています。

module NotionRubyMapping
  # TextObject
  class FileObject
    # @param [String] url
    # @return [TextObject]
    def initialize(url: nil, file_upload_object: nil, json: {})
      if url
        @type = :external
        @url = url
      elsif file_upload_object
        @type = :file_upload
        @file_upload_object = file_upload_object
      elsif json
        @type = json[:type].to_sym
        @url = json[@type][:url]
        @expiry_time = json[@type][:expiry_time]
      else
        raise StandardError, "FileObject requires url: or json:"
      end
      @will_update = false
    end
    attr_reader :will_update, :url, :type, :file_upload_object

FileObject.file_object の修正

FileObject には file_object というクラスメソッドがあります。これは、引数が String の場合には URL として、FileObject の場合にはそのものを返却するメソッドでした。今回は、ここに FileUploadObject が渡された時に、これを内部に保持する FileObject を作成し、返却するようにします。テストは以下のようになりました。ここで、file_upload_object は FileUploadObject ではなく、その mock なので、is_a? に対する stub を用意しています。

 describe "self.file_object" do
      subject { FileObject.file_object fo }
      context "with url string" do
        (中略)
      end

      context "with FileUploadObject" do
        let(:fo) { file_upload_object }

        before { allow(file_upload_object).to receive(:is_a?).with(FileUploadObject).and_return(true) }

        it { expect(subject).to be_is_a FileObject }
        it { expect(subject.file_upload_object).to eq file_upload_object }
      end

      context "with FileObject" do
        (中略)
      end
    end

実装は以下のようになりました。

    # @param [FileObject, FileUploadObject, String] url_or_fuo_or_fo
    # @return [FileObject] self or created FileObject
    # @see https://www.notion.so/hkob/FileObject-6218c354e985423a90904f47a985be33#54b37c567e1d4dfcab06f6d8f8fd412e
    def self.file_object(url_or_fuo_or_fo)
      if url_or_fuo_or_fo.is_a? FileUploadObject
        FileObject.new file_upload_object: url_or_fuo_or_fo
      elsif url_or_fuo_or_fo.is_a? FileObject
        url_or_fuo_or_fo
      else
        FileObject.new url: url_or_fuo_or_fo
      end
    end

FileObject#property_values_json の修正

作成した FileObject から JSON を作成する部分も当然ながら修正が必要です。以下のテストを追加しています。

 describe "property_values_json" do
      context "when internal image" do
        (中略)
      end

      context "when external image" do
        (中略)
      end

      context "when file upload object" do
        let(:target) { FileObject.new file_upload_object: file_upload_object }

        it_behaves_like "property values json", {
          type: "file_upload",
          file_upload: {
            id: TestConnection::FILE_UPLOAD_IMAGE_ID,
          },
        }
      end
    end

実装は以下のようになりました。

    # @return [Hash]
    def property_values_json
      if @type == :file_upload
        {
          type: @type.to_s,
          @type => {
            id: @file_upload_object.id,
          },
        }
      else
        ans = {
          type: @type.to_s,
          @type => {
            url: @url,
          },
        }
        ans[@type][:expiry_time] = @expiry_time if @expiry_time
        ans
      end
    end

FileObject#file_upload_object= の修正

FileObject の最後は、オブジェクトに後から FileUploadObject を追加する file_upload_object= です。

    describe "file_upload_object=" do
      let(:target) { FileObject.new json: file_internal_json }

      before { target.file_upload_object = file_upload_object }

      it_behaves_like "property values json", {
        type: "file_upload",
        file_upload: {
          id: TestConnection::FILE_UPLOAD_IMAGE_ID,
        },
      }
      it { expect(target.file_upload_object).to eq file_upload_object }
    end

以下のような実装を追加しました。

    # @param [FileUploadObject] fuo
    def file_upload_object=(fuo)
      @file_upload_object = fuo
      @type = :file_upload
      @url = nil
      @expiry_time = nil
      @will_update = true
    end

FileObject のテスト

ここまでの修正におけるテスト結果は以下のようになります。

20:50:39 - INFO - Running: spec/notion_ruby_mapping/objects/file_object_spec.rb

NotionRubyMapping::FileObject
  constructor
    with url
      is expected to eq "https://example.com/external.jpg"
      is expected to eq :external
      is expected to be falsey
    with file_upload_object
      is expected to eq #<InstanceDouble(FileUploadObject) (anonymous)>
      is expected to eq :file_upload
      is expected to be falsey
    with json
      is expected to eq "https://s3.us-west-2.amazonaws.com/secure.notion-static.com/f7b6864c-f809-498d-8725-03fc7e85a9ff/nr....83da3411466cead5f144b5a955ea5be1844ec06c6893689a3fb86c369e2&X-Amz-SignedHeaders=host&x-id=GetObject"                                                         
      is expected to eq :file
      is expected to be falsey
    without arguments
      is expected to raise StandardError
  self.file_object
    with url string
      is expected to be is a NotionRubyMapping::FileObject
      is expected to eq "https://example.com/external.jpg"
    with FileUploadObject
      is expected to be is a NotionRubyMapping::FileObject
      is expected to eq #<InstanceDouble(FileUploadObject) (anonymous)>
    with FileObject
      is expected to be is a NotionRubyMapping::FileObject
      is expected to eq "https://example.com/external.jpg"
  property_values_json
    when internal image
      behaves like property values json
        is expected to eq {file: {expiry_time: "2022-03-10T00:56:24.105Z", url: "https://s3.us-west-2.amazonaws.com/secure.noti...f144b5a955ea5be1844ec06c6893689a3fb86c369e2&X-Amz-SignedHeaders=host&x-id=GetObject"}, type: "file"}                                                       
    when external image
      behaves like property values json
        is expected to eq {external: {url: "https://img.icons8.com/ios-filled/250/000000/mac-os.png"}, type: "external"}                                                                      
    when file upload object
      behaves like property values json
        is expected to eq {file_upload: {id: "20cd8e4e98ab81aa973b00b23083c115"}, type: "file_upload"}                                                                                        
  create from json
    when internal image
      behaves like property values json
        is expected to eq {file: {expiry_time: "2022-03-10T00:56:24.105Z", url: "https://s3.us-west-2.amazonaws.com/secure.noti...f144b5a955ea5be1844ec06c6893689a3fb86c369e2&X-Amz-SignedHeaders=host&x-id=GetObject"}, type: "file"}                                                       
    when external image
      is expected to eq "https://img.icons8.com/ios-filled/250/000000/mac-os.png"
      behaves like property values json
        is expected to eq {external: {url: "https://img.icons8.com/ios-filled/250/000000/mac-os.png"}, type: "external"}                                                                      
  url=
    when internal image
      is expected to eq "https://example.com/external.jpg"
      behaves like property values json
        is expected to eq {external: {url: "https://example.com/external.jpg"}, type: "external"}                                                                                             
    when external image
      is expected to eq "https://example.com/external.jpg"
      behaves like property values json
        is expected to eq {external: {url: "https://example.com/external.jpg"}, type: "external"}                                                                                             
  file_upload_object=
    is expected to eq #<InstanceDouble(FileUploadObject) (anonymous)>
    behaves like property values json
      is expected to eq {file_upload: {id: "20cd8e4e98ab81aa973b00b23083c115"}, type: "file_upload"}                                                                                          

Finished in 0.01339 seconds (files took 0.42259 seconds to load)
28 examples, 0 failures

Payload#set_icon の修正

次に Payload の set_icon メソッドの修正です。

    describe "set_icon" do
      before { payload.set_icon(**params) }

      context "with emoji icon" do
        (中略)
      end

      context "with link icon" do
        (中略)
      end

      context "with file upload object" do
        let(:id) { TestConnection::FILE_UPLOAD_IMAGE_ID }
        let(:file_upload_object) { instance_double(FileUploadObject, id: id) }
        let(:params) { {file_upload_object: file_upload_object} }

        it "update icon (file upload)" do
          expect(subject).to eq({icon: {type: "file_upload", file_upload: {id: id}}})
        end
      end
    end

実装は以下のようになりました。

    # @param [String] emoji
    # @param [String] url
    # @param [FileUploadObject] file_upload_object
    # @return [NotionRubyMapping::Payload] updated Payload
    def set_icon(emoji: nil, url: nil, file_upload_object: nil)
      payload = if emoji
                  {type: "emoji", emoji: emoji}
                elsif url
                  {type: "external", external: {url: url}}
                elsif file_upload_object
                  {type: "file_upload", file_upload: {id: file_upload_object.id}}
                else
                  {}
                end
      @json[:icon] = payload
      self
    end

テスト結果は以下のようになりました。

21:03:05 - INFO - Running: spec/notion_ruby_mapping/controllers/payload_spec.rb

NotionRubyMapping::Payload
  constructor
    can create an object
  description=
    is expected to eq {description: [{href: nil, plain_text: "Title", text: {content: "Title", link: nil}, type: "text"}]}                                                                    
  is_inline=
    is expected to eq {is_inline: true}
  set_icon
    with emoji icon
      update icon (emoji)
    with link icon
      update icon (link)
    with file upload object
      update icon (file upload)

Finished in 0.05328 seconds (files took 0.29941 seconds to load)
6 examples, 0 failures

Page#set_icon の修正

最後に今回の目的である Page の set_icon のテストを修正します。FileUploadObject の icon で save しますが、Page object 自体は返却された JSON で上書きされるため、internal_file の FileObject に変換して保存されます。

    describe "update_icon" do
      let(:target) { Page.new id: TestConnection::TOP_PAGE_ID }

      before do
        target.set_icon(**params)
      end

      subject { target.icon }

      context "with emoji icon" do
        (中略)
      end

      context "with link icon" do
        (中略)
      end

      context "with file upload object" do
        let(:id) { TestConnection::FILE_UPLOAD_IMAGE_ID }
        let(:file_upload_object) { instance_double(FileUploadObject, id: id) }
        let(:params) { {file_upload_object: file_upload_object} }

        describe "dry_run" do
          let(:dry_run) { target.save dry_run: true }

          it_behaves_like "dry run", :patch, :page_path, use_id: true, json_method: :property_values_json
        end

        describe "save" do
          before { target.save }

          it "update icon (file upload object)" do
            expect(subject).to eq(
              {
                file: {

テスト結果は以下のようになります。Page はテストが多いので、該当箇所のみ示します。

21:04:54 - INFO - Running: spec/notion_ruby_mapping/blocks/page_spec.rb

NotionRubyMapping::Page
        (中略)
  set_icon
    with emoji icon
      dry_run
        behaves like dry run
          is expected to eq "#!/bin/sh\ncurl -X PATCH 'https://api.notion.com/v1/pages/c01166c613ae45cbb96818b4ef2f5a77' \\\n  -H... -H 'Content-Type: application/json' \\\n  --data '{\"icon\":{\"type\":\"emoji\",\"emoji\":\"😀\"}}'"                                                    
      save
        update icon (emoji)
    with link icon
      dry_run
        behaves like dry run
          is expected to eq "#!/bin/sh\ncurl -X PATCH 'https://api.notion.com/v1/pages/c01166c613ae45cbb96818b4ef2f5a77' \\\n  -H...rnal\",\"external\":{\"url\":\"https://cdn.profile-image.st-hatena.com/users/hkob/profile.png\"}}}'"                                                     
      save
        update icon (link)
    with file upload object
      dry_run
        behaves like dry run
          is expected to eq "#!/bin/sh\ncurl -X PATCH 'https://api.notion.com/v1/pages/c01166c613ae45cbb96818b4ef2f5a77' \\\n  -H...\"icon\":{\"type\":\"file_upload\",\"file_upload\":{\"id\":\"20cd8e4e98ab81aa973b00b23083c115\"}}}'"                                                     
      save
        update icon (file upload object)

おわりに

FileUploadObject が生成でき、今回それを含む FileObject の実装が完了したので、後はそれらを使ったテストと実装を追加していくだけです。急いで実装していきます。

hkob.notion.site