notion_ruby_mapping 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 366fc4989968bff0fb71d9376b58d3e270fa3e7d91581d37ab75653ec06cb8e3
4
- data.tar.gz: e36897b1704fafa277c1996459d148abac1fcf03d62d9e23a8ee67a7325e9a11
3
+ metadata.gz: 4d8cbce3013176a87d6e023e5bbbd0b5ae6f5c43b437f50c3c6df468ce302d19
4
+ data.tar.gz: 0ea1f4d67320d4039a42eb156ba03a29595a4362c51a44f7541a874c735c257b
5
5
  SHA512:
6
- metadata.gz: 531c093bf3a67cf10dba9208c1321c6f3a8d35f808195167947f761563cc961e3bac59be090c70dc7bfdee1f453bba964d160f912ad035ebdc114d441541765d
7
- data.tar.gz: 3e7f35ff1b8bbcfc51052f34cc873da0f4436175faced7ac12140c34d0dff4b91f83b555ea888483643d068485c43a23714b7186dd9b8b950d957759a7c347dc
6
+ metadata.gz: 05a4e8f93a32115e06047aa0324879e63c5ec761d7b98e1cfc7c1df76033389705d1a25ca0b79a1b657dfb4df27b80fa4eeb0793ec21be359d3c2105cbe3f98c
7
+ data.tar.gz: cc86a8c085ed3da0b7905560c4f92ec8c627739480c3ee13a951f9b9c82bf0ec5cf042f0d888af7bc98a9ecaee788282d042b4de7961852c9cd3407ed8389ab6
data/README.md CHANGED
@@ -143,6 +143,7 @@ NotionRubyMapping.configuration { |c| c.notion_token = ENV["NOTION_API_TOKEN"] }
143
143
 
144
144
  ## 3. ChangeLog
145
145
 
146
+ - 2026/9/7 [v4.2.0] Refactor file handling (move FileUploadObject into NotionRubyMapping, remove FileObject update tracking, and reject missing or empty file sources)
146
147
  - 2026/8/23 [v4.1.0] Refactor Property base class (remove def_delegators and contents?, raise for unsupported property type)
147
148
  - 2026/7/14 [v4.0.2] Add clear method for DateProperty
148
149
  - 2026/5/4 [v4.0.1] Add retrieve / update / delete a comment
@@ -14,54 +14,10 @@ module NotionRubyMapping
14
14
  end
15
15
  attr_reader :can_have_children, :can_append, :type, :rich_text_array, :url, :caption, :color, :language
16
16
 
17
- def self.type2class(type, has_children = false)
18
- @type2class ||= {
19
- false => {
20
- audio: AudioBlock,
21
- bookmark: BookmarkBlock,
22
- breadcrumb: BreadcrumbBlock,
23
- bulleted_list_item: BulletedListItemBlock,
24
- callout: CalloutBlock,
25
- child_database: ChildDatabaseBlock,
26
- child_page: ChildPageBlock,
27
- code: CodeBlock,
28
- column: ColumnBlock,
29
- column_list: ColumnListBlock,
30
- divider: DividerBlock,
31
- embed: EmbedBlock,
32
- equation: EquationBlock,
33
- file: FileBlock,
34
- heading_1: Heading1Block,
35
- heading_2: Heading2Block,
36
- heading_3: Heading3Block,
37
- heading_4: Heading4Block,
38
- image: ImageBlock,
39
- link_preview: LinkPreviewBlock,
40
- link_to_page: LinkToPageBlock,
41
- numbered_list_item: NumberedListItemBlock,
42
- paragraph: ParagraphBlock,
43
- pdf: PdfBlock,
44
- quote: QuoteBlock,
45
- synced_block: SyncedBlock,
46
- table: TableBlock,
47
- table_row: TableRowBlock,
48
- table_of_contents: TableOfContentsBlock,
49
- to_do: ToDoBlock,
50
- toggle: ToggleBlock,
51
- video: VideoBlock,
52
- },
53
- true => {
54
- heading_1: ToggleHeading1Block,
55
- heading_2: ToggleHeading2Block,
56
- heading_3: ToggleHeading3Block,
57
- heading_4: ToggleHeading4Block,
58
- },
59
- }
60
- @klass = @type2class[has_children][type.to_sym] || @type2class[false][type.to_sym] || Block
61
- end
62
-
63
17
  def self.decode_block(json)
64
- type2class(json["type"], json["has_children"]).new json: json
18
+ type = json["type"]
19
+ is_toggleable = !!(json[type] && json[type]["is_toggleable"])
20
+ type2class(type, is_toggleable).new json: json
65
21
  end
66
22
 
67
23
  # @see https://www.notion.so/hkob/Block-689ad4cbff50404d8a1baf67b6d6d78d#298916c7c379424682f39ff09ee38544
@@ -101,7 +57,7 @@ module NotionRubyMapping
101
57
 
102
58
  # @return [NotionRubyMapping::RichTextArray]
103
59
  def decode_block_caption
104
- @caption = RichTextArray.new "caption", json: @json[type]["caption"] if @json[type]["caption"]
60
+ @caption = RichTextArray.new "caption", json: @json[type]["caption"] || []
105
61
  end
106
62
 
107
63
  # @return [String]
@@ -154,5 +110,52 @@ module NotionRubyMapping
154
110
  @color = color
155
111
  self
156
112
  end
113
+
114
+ def self.type2class(type, is_toggleable = false)
115
+ @type2class ||= {
116
+ false => {
117
+ audio: AudioBlock,
118
+ bookmark: BookmarkBlock,
119
+ breadcrumb: BreadcrumbBlock,
120
+ bulleted_list_item: BulletedListItemBlock,
121
+ callout: CalloutBlock,
122
+ child_database: ChildDatabaseBlock,
123
+ child_page: ChildPageBlock,
124
+ code: CodeBlock,
125
+ column: ColumnBlock,
126
+ column_list: ColumnListBlock,
127
+ divider: DividerBlock,
128
+ embed: EmbedBlock,
129
+ equation: EquationBlock,
130
+ file: FileBlock,
131
+ heading_1: Heading1Block,
132
+ heading_2: Heading2Block,
133
+ heading_3: Heading3Block,
134
+ heading_4: Heading4Block,
135
+ image: ImageBlock,
136
+ link_preview: LinkPreviewBlock,
137
+ link_to_page: LinkToPageBlock,
138
+ numbered_list_item: NumberedListItemBlock,
139
+ paragraph: ParagraphBlock,
140
+ pdf: PdfBlock,
141
+ quote: QuoteBlock,
142
+ synced_block: SyncedBlock,
143
+ table: TableBlock,
144
+ table_row: TableRowBlock,
145
+ table_of_contents: TableOfContentsBlock,
146
+ to_do: ToDoBlock,
147
+ toggle: ToggleBlock,
148
+ video: VideoBlock,
149
+ },
150
+ true => {
151
+ heading_1: ToggleHeading1Block,
152
+ heading_2: ToggleHeading2Block,
153
+ heading_3: ToggleHeading3Block,
154
+ heading_4: ToggleHeading4Block,
155
+ },
156
+ }
157
+ @type2class[is_toggleable][type.to_sym] || @type2class[false][type.to_sym] || Block
158
+ end
159
+ private_class_method :type2class
157
160
  end
158
161
  end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionRubyMapping
4
- # Notion block
4
+ # Notion breadcrumb block
5
5
  class BreadcrumbBlock < Block
6
- # @return [String (frozen)]
6
+ # @return [String] block type
7
7
  def type
8
8
  "breadcrumb"
9
9
  end
10
10
 
11
- # @param [Boolean] not_update false when update
12
- # @return [Hash{String (frozen)->Hash}]
11
+ # @param [Boolean] not_update true for a full block payload; false for an update payload
12
+ # @return [Hash{String => Object}] block payload
13
13
  def block_json(not_update: true)
14
14
  ans = super
15
15
  ans[type] = {}
@@ -14,8 +14,13 @@ module NotionRubyMapping
14
14
  if @json
15
15
  decode_block_rich_text_array
16
16
  decode_color
17
+ @emoji = json[type]&.dig("icon", "emoji")&.then { |str| EmojiObject.new emoji: str }
18
+ @file_object = json[type]&.dig("icon")&.then { |file_json| FileObject.new json: file_json }
17
19
  else
18
20
  rich_text_array_and_color "rich_text", text_info, color
21
+ raise ArgumentError, "Specify either emoji or file_url." if emoji.nil? && file_url.nil?
22
+ raise ArgumentError, "Specify either emoji or file_url, not both." if !emoji.nil? && !file_url.nil?
23
+
19
24
  @emoji = EmojiObject.emoji_object emoji if emoji
20
25
  @file_object = FileObject.file_object file_url if file_url
21
26
  add_sub_blocks sub_blocks
@@ -34,7 +39,7 @@ module NotionRubyMapping
34
39
  ans[type] = @rich_text_array.update_property_schema_json not_update
35
40
  ans[type]["color"] = @color
36
41
  ans[type]["icon"] = @emoji.property_values_json if @emoji
37
- ans[type]["icon"] = @file_object.property_values_json if @file_object
42
+ ans[type]["icon"] = @file_object.property_values_json if @file_object && @file_object.url
38
43
  ans[type]["children"] = @sub_blocks.map(&:block_json) if @sub_blocks
39
44
  ans
40
45
  end
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionRubyMapping
4
- # Notion block
4
+ # Notion divider block
5
5
  class DividerBlock < Block
6
- # @param [Boolean] not_update false when update
7
- # @return [Hash{String (frozen)->Hash}]
6
+ # @param [Boolean] not_update true for a full block payload; false for an update payload
7
+ # @return [Hash{String => Object}] block payload
8
8
  def block_json(not_update: true)
9
9
  ans = super
10
10
  ans[type] = {}
11
11
  ans
12
12
  end
13
13
 
14
- # @return [String (frozen)]
14
+ # @return [String] block type
15
15
  def type
16
16
  "divider"
17
17
  end
@@ -3,7 +3,7 @@
3
3
  module NotionRubyMapping
4
4
  # Notion block
5
5
  class EquationBlock < Block
6
- # @param [String, NotionRubyMapping::EquationObject] expression
6
+ # @param [String, NotionRubyMapping::EquationObject, nil] expression
7
7
  # @see https://www.notion.so/hkob/EquationBlock-df0f823dc5ac41b798052f161dd6540c#cfcd2ceb77194c0e915500b429e8b91b
8
8
  def initialize(expression = nil, json: nil, id: nil, parent: nil)
9
9
  super(json: json, id: id, parent: parent)
@@ -28,7 +28,7 @@ module NotionRubyMapping
28
28
  @equation_object&.expression
29
29
  end
30
30
 
31
- # @param [String] new_expression
31
+ # @param [String, nil] new_expression
32
32
  # @see https://www.notion.so/hkob/EquationBlock-df0f823dc5ac41b798052f161dd6540c#f05e3b2c82914cea9f05e9e6644647e1
33
33
  def expression=(new_expression)
34
34
  @equation_object = EquationObject.equation_object new_expression
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionRubyMapping
4
- # Notion block
4
+ # Base class for blocks that contain a file and an optional caption.
5
+ #
6
+ # This class manages the file representation, caption, and update payload
7
+ # shared by file-based block types.
8
+ # @abstract Subclass and implement {#type}.
5
9
  class FileBaseBlock < Block
6
- # @param [String, FileUploadObject, NilClass] url or file upload object
7
- # @param [RichTextArray, String, Array<String>, RichTextObject, Array<RichTextObject>, nil] caption
8
- # @see https://www.notion.so/hkob/FileBlock-08f2aa6948364d00b92beacaac9a619c#ace53d2e6ff2404f937179ae1e966e98
9
- # @see https://www.notion.so/hkob/ImageBlock-806b3d2a9a2c4bf5a5aca6e3fbc8a7e2#4cb790b8b5c847acab4341d55e4fa66a
10
+ # Creates a file-based block.
11
+ #
12
+ # @param url_or_fuo [String, FileUploadObject, FileObject, nil]
13
+ # an external URL, a file upload object, or a file object
14
+ # @param caption [RichTextArray, String, Array<String>, RichTextObject,
15
+ # Array<RichTextObject>, nil] caption content
16
+ # @param json [Hash, nil] block JSON returned by the Notion API
17
+ # @param id [String, nil] block ID
18
+ # @param parent [Page, Block, nil] parent object
10
19
  def initialize(url_or_fuo = nil, caption: [], json: nil, id: nil, parent: nil)
11
20
  super(json: json, id: id, parent: parent)
12
21
  if @json
@@ -19,12 +28,18 @@ module NotionRubyMapping
19
28
  end
20
29
  end
21
30
 
22
- # @see https://www.notion.so/hkob/FileBlock-08f2aa6948364d00b92beacaac9a619c#158487a7e1644fae8778dcff59869356
23
- # @see https://www.notion.so/hkob/ImageBlock-806b3d2a9a2c4bf5a5aca6e3fbc8a7e2#19a4aa3e06514bbe84be9d3b8a45a20f
24
- attr_reader :caption, :file_object
31
+ # @return [RichTextArray] block caption
32
+ attr_reader :caption
25
33
 
26
- # @param [Boolean] not_update false when update
27
- # @return [Hash{String (frozen)->Hash}]
34
+ # @return [FileObject] current file representation
35
+ attr_reader :file_object
36
+
37
+ # Builds the block JSON representation.
38
+ #
39
+ # @param not_update [Boolean] true for a complete block payload;
40
+ # false for an update payload
41
+ # @return [Hash{String => Object}] block JSON
42
+ # @api private
28
43
  def block_json(not_update: true)
29
44
  ans = super
30
45
  ans[type] = @file_object.property_values_json
@@ -32,27 +47,41 @@ module NotionRubyMapping
32
47
  ans
33
48
  end
34
49
 
35
- # @return [String]
36
- # @see https://www.notion.so/hkob/FileBlock-08f2aa6948364d00b92beacaac9a619c#d3e7d31b7b274955aa7603163867fa57
37
- # @see https://www.notion.so/hkob/ImageBlock-806b3d2a9a2c4bf5a5aca6e3fbc8a7e2#01e1883119f14c5f9f7f6823793e72ec
50
+ # Returns the current file URL.
51
+ #
52
+ # A URL returned for a Notion-hosted file may expire.
53
+ #
54
+ # @return [String, nil] current file URL
38
55
  def url
39
56
  @file_object&.url
40
57
  end
41
58
 
42
- # @param [String] url
43
- # @see https://www.notion.so/hkob/FileBlock-08f2aa6948364d00b92beacaac9a619c#23497b8eb2214c45b3d5881796f984cb
44
- # @see https://www.notion.so/hkob/ImageBlock-806b3d2a9a2c4bf5a5aca6e3fbc8a7e2#61598d260b6140f2a359f7d22ea2548a
59
+ # Replaces the current file with an external URL.
60
+ #
61
+ # Call {Block#save} to send the change to the Notion API.
62
+ #
63
+ # @param url [String] new external URL
45
64
  def url=(url)
46
65
  @file_object.url = url
47
66
  @payload.add_update_block_key "external"
48
67
  end
49
68
 
50
- # @param [FileUploadObject] fuo
51
- def file_upload_object=(fuo)
52
- @file_object.file_upload_object = fuo
69
+ # Replaces the current file with an uploaded file.
70
+ #
71
+ # Call {Block#save} to send the change to the Notion API. After saving,
72
+ # the file is reconstructed as a Notion-hosted file.
73
+ #
74
+ # @param file_upload_object [FileUploadObject] uploaded file
75
+ def file_upload_object=(file_upload_object)
76
+ @file_object.file_upload_object = file_upload_object
53
77
  @payload.add_update_block_key "file_upload"
54
78
  end
55
79
 
80
+ # Reconstructs the file and caption from an API response.
81
+ #
82
+ # @param json [Hash] block JSON returned by the Notion API
83
+ # @return [void]
84
+ # @api private
56
85
  def update_file_object_from_json(json)
57
86
  @file_object = FileObject.new json: json[type]
58
87
  decode_block_caption
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionRubyMapping
4
- # Notion block
4
+ # Represents a Notion file block.
5
5
  class FileBlock < FileBaseBlock
6
- # @return [String]
6
+ # Returns the Notion block type.
7
+ #
8
+ # @return [String] `"file"`
7
9
  def type
8
10
  "file"
9
11
  end
@@ -5,7 +5,7 @@ module NotionRubyMapping
5
5
  class Heading1Block < TextSubBlockColorBaseBlock
6
6
  # @param [RichTextArray, String, Array<String>, RichTextObject, Array<RichTextObject>, nil] text_info
7
7
  # @param [String] color
8
- def initialize(text_info = nil, color: nil, json: nil, id: nil, parent: nil)
8
+ def initialize(text_info = nil, color: "default", json: nil, id: nil, parent: nil)
9
9
  super(text_info, color: color, json: json, id: id, parent: parent)
10
10
  @can_have_children = false
11
11
  end
@@ -5,7 +5,7 @@ module NotionRubyMapping
5
5
  class Heading2Block < TextSubBlockColorBaseBlock
6
6
  # @param [RichTextArray, String, Array<String>, RichTextObject, Array<RichTextObject>] text_info
7
7
  # @param [String] color
8
- def initialize(text_info = nil, color: nil, json: nil, id: nil, parent: nil)
8
+ def initialize(text_info = nil, color: "default", json: nil, id: nil, parent: nil)
9
9
  super(text_info, color: color, json: json, id: id, parent: parent)
10
10
  @can_have_children = false
11
11
  end
@@ -5,7 +5,7 @@ module NotionRubyMapping
5
5
  class Heading3Block < TextSubBlockColorBaseBlock
6
6
  # @param [RichTextArray, String, Array<String>, RichTextObject, Array<RichTextObject>] text_info
7
7
  # @param [String] color
8
- def initialize(text_info = nil, color: nil, json: nil, id: nil, parent: nil)
8
+ def initialize(text_info = nil, color: "default", json: nil, id: nil, parent: nil)
9
9
  super(text_info, color: color, json: json, id: id, parent: parent)
10
10
  @can_have_children = false
11
11
  end
@@ -5,7 +5,7 @@ module NotionRubyMapping
5
5
  class Heading4Block < TextSubBlockColorBaseBlock
6
6
  # @param [RichTextArray, String, Array<String>, RichTextObject, Array<RichTextObject>] text_info
7
7
  # @param [String] color
8
- def initialize(text_info = nil, color: nil, json: nil, id: nil, parent: nil)
8
+ def initialize(text_info = nil, color: "default", json: nil, id: nil, parent: nil)
9
9
  super(text_info, color: color, json: json, id: id, parent: parent)
10
10
  @can_have_children = false
11
11
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionRubyMapping
4
- # Notion block
5
- # @param [String] color
4
+ # Notion table of contents block
6
5
  class TableOfContentsBlock < Block
6
+ # @param [String] color block color
7
7
  def initialize(color = "default", json: nil, id: nil, parent: nil)
8
8
  super(json: json, id: id, parent: parent)
9
9
  if @json
@@ -13,18 +13,19 @@ module NotionRubyMapping
13
13
  end
14
14
  end
15
15
 
16
+ # @param [String] new_color new block color
16
17
  def color=(new_color)
17
18
  @color = new_color
18
19
  @payload.add_update_block_key "color"
19
20
  end
20
21
 
21
- # @return [String (frozen)]
22
+ # @return [String] block type
22
23
  def type
23
24
  "table_of_contents"
24
25
  end
25
26
 
26
- # @param [Boolean] not_update false when update
27
- # @return [Hash{String (frozen)->Hash}]
27
+ # @param [Boolean] not_update true for a full block payload; false for an update payload
28
+ # @return [Hash{String => Object}] block payload
28
29
  def block_json(not_update: true)
29
30
  ans = super
30
31
  ans[type] = {"color" => @color}
@@ -19,13 +19,14 @@ module NotionRubyMapping
19
19
  end
20
20
  @will_update = will_update
21
21
  end
22
+ attr_reader :rich_text_objects
22
23
  attr_writer :will_update
23
24
 
24
25
  def self.rich_text_array(key, text_objects = nil)
25
26
  if text_objects.nil?
26
27
  RichTextArray.new key
27
28
  elsif text_objects.is_a? RichTextArray
28
- text_objects
29
+ RichTextArray.new key, text_objects: text_objects.rich_text_objects
29
30
  else
30
31
  RichTextArray.new key, text_objects: text_objects
31
32
  end
@@ -3,16 +3,17 @@
3
3
  module NotionRubyMapping
4
4
  # EquationObject
5
5
  class EquationObject < RichTextObject
6
- # @param [String] expression
7
- # @return [TextObject]
6
+ # @param [String, nil] expression
7
+ # @return [EquationObject]
8
8
  def initialize(expression, options = {})
9
+ expression ||= ""
9
10
  super "equation", {"plain_text" => expression}.merge(options)
10
11
  @expression = expression
11
12
  @will_update = false
12
13
  end
13
14
  attr_reader :will_update, :expression
14
15
 
15
- # @param [EquationObject, String] uo
16
+ # @param [EquationObject, String, nil] expression_or_eo
16
17
  # @return [EquationObject] self or created EquationObject
17
18
  # @see https://www.notion.so/hkob/EquationObject-cd50126fce544ad5bb76463a4269859b#45b0b0810d8647a1968a5e4293920aeb
18
19
  def self.equation_object(expression_or_eo)
@@ -23,9 +24,10 @@ module NotionRubyMapping
23
24
  end
24
25
  end
25
26
 
26
- # @param [String] expression
27
+ # @param [String, nil] expression
27
28
  # @see https://www.notion.so/hkob/EquationObject-cd50126fce544ad5bb76463a4269859b#155800e2c69d4676a2d74572ed8f0de8
28
29
  def expression=(expression)
30
+ expression ||= ""
29
31
  @expression = expression
30
32
  @options["plain_text"] = expression
31
33
  @will_update = true
@@ -1,11 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionRubyMapping
4
- # TextObject
4
+ # Represents file information shared by file-based blocks and file properties.
5
+ #
6
+ # Use the owning block or property to update a file. This object is primarily
7
+ # intended for inspecting the current file representation.
5
8
  class FileObject
6
- # @param [String] url
7
- # @return [TextObject]
8
- def initialize(url: nil, file_upload_object: nil, json: {})
9
+ # Creates a file representation.
10
+ #
11
+ # @param url [String, nil] external file URL
12
+ # @param file_upload_object [FileUploadObject, nil] uploaded file
13
+ # @param json [Hash, nil] file JSON returned by the Notion API
14
+ # @raise [ArgumentError] if no file source is provided
15
+ # @api private
16
+ def initialize(url: nil, file_upload_object: nil, json: nil)
9
17
  if url
10
18
  @type = "external"
11
19
  @url = url
@@ -17,49 +25,66 @@ module NotionRubyMapping
17
25
  @url = json[@type]["url"]
18
26
  @expiry_time = json[@type]["expiry_time"]
19
27
  else
20
- raise StandardError, "FileObject requires url: or json:"
28
+ raise ArgumentError, "FileObject requires url:, file_upload_object:, or json:"
21
29
  end
22
- @will_update = false
23
30
  end
24
- attr_reader :will_update, :url, :type, :file_upload_object
31
+ # @return [String, nil] current file URL
32
+ attr_reader :url
25
33
 
26
- # @param [FileObject, FileUploadObject, String] url_or_fuo_or_fo
27
- # @return [FileObject] self or created FileObject
28
- # @see https://www.notion.so/hkob/FileObject-6218c354e985423a90904f47a985be33#54b37c567e1d4dfcab06f6d8f8fd412e
29
- def self.file_object(url_or_fuo_or_fo)
30
- if url_or_fuo_or_fo.is_a? FileUploadObject
31
- FileObject.new file_upload_object: url_or_fuo_or_fo
32
- elsif url_or_fuo_or_fo.is_a? FileObject
33
- url_or_fuo_or_fo
34
+ # @return [String] file type: `"external"`, `"file"`, or `"file_upload"`
35
+ attr_reader :type
36
+
37
+ # @return [FileUploadObject, nil] current file upload object
38
+ attr_reader :file_upload_object
39
+
40
+ # Converts a supported file value to a file object.
41
+ #
42
+ # @param value [String, FileUploadObject, FileObject]
43
+ # external URL, uploaded file, or existing file object
44
+ # @return [FileObject] converted file object
45
+ # @api private
46
+ def self.file_object(value)
47
+ if value.is_a? FileUploadObject
48
+ FileObject.new file_upload_object: value
49
+ elsif value.is_a? FileObject
50
+ value
34
51
  else
35
- FileObject.new url: url_or_fuo_or_fo
52
+ FileObject.new url: value
36
53
  end
37
54
  end
38
55
 
39
- # @return [TrueClass, FalseClass] true if "type" is "external"
56
+ # Checks whether the file uses an external URL.
57
+ #
58
+ # @return [Boolean] true if the file type is `"external"`
40
59
  def external?
41
60
  @type == "external"
42
61
  end
43
62
 
44
- # @param [FileUploadObject] fuo
45
- def file_upload_object=(fuo)
46
- @file_upload_object = fuo
63
+ # Replaces the current representation with an uploaded file.
64
+ #
65
+ # @param file_upload_object [FileUploadObject] uploaded file
66
+ # @api private
67
+ def file_upload_object=(file_upload_object)
68
+ @file_upload_object = file_upload_object
47
69
  @type = "file_upload"
48
70
  @url = nil
49
71
  @expiry_time = nil
50
- @will_update = true
51
72
  end
52
73
 
53
- # @param [String] url
54
- # @see https://www.notion.so/hkob/FileObject-6218c354e985423a90904f47a985be33#6b841f75d0234a1aac93fb54348abb96
74
+ # Replaces the current representation with an external URL.
75
+ #
76
+ # @param url [String] external file URL
77
+ # @api private
55
78
  def url=(url)
56
79
  @url = url
57
80
  @type = "external"
58
81
  @expiry_time = nil
59
- @will_update = true
60
82
  end
61
83
 
62
- # @return [Hash]
84
+ # Builds the Notion API file JSON.
85
+ #
86
+ # @return [Hash{String => Object}] file JSON
87
+ # @api private
63
88
  def property_values_json
64
89
  if @type == "file_upload"
65
90
  {
@@ -1,86 +1,132 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class FileUploadObject
4
- MAX_SIZE = 10 * 1024 * 1024 # 10 MB
5
- # @param [String] fname
6
- def initialize(fname:, external_url: nil)
7
- @fname = fname
8
- if external_url
9
- payload = {mode: "external_url", external_url: external_url, filename: fname}
10
- create payload
11
- else
12
- raise StandardError, "FileUploadObject requires a valid file name: #{fname}" unless File.exist?(fname)
3
+ module NotionRubyMapping
4
+ # Uploads a local or external file to Notion.
5
+ #
6
+ # Creating an instance starts the upload immediately. Large local files are
7
+ # split into multiple parts before being uploaded.
8
+ class FileUploadObject
9
+ # Maximum size of each upload part.
10
+ MAX_SIZE = 10 * 1024 * 1024
13
11
 
14
- @file_size = File.size fname
15
- @number_of_parts = (@file_size - 1) / MAX_SIZE + 1
16
- payload = if @number_of_parts == 1
17
- {}
18
- else
19
- {number_of_parts: @number_of_parts, mode: "multi_part",
20
- filename: File.basename(@fname)}
21
- end
22
- create payload
23
- if @number_of_parts == 1
24
- single_file_upload
12
+ # Creates and uploads a file.
13
+ #
14
+ # When +external_url+ is provided, +fname+ is used as the filename sent to
15
+ # Notion. Otherwise, +fname+ must point to an existing local file.
16
+ #
17
+ # @param fname [String] local file path or filename
18
+ # @param external_url [String, nil] URL of a file for Notion to import
19
+ # @raise [StandardError] if the local file does not exist
20
+ # @raise [ArgumentError] if the local file is empty
21
+ def initialize(fname:, external_url: nil)
22
+ @fname = fname
23
+ if external_url
24
+ payload = {mode: "external_url", external_url: external_url, filename: fname}
25
+ create payload
25
26
  else
26
- @temp_files = FileUploadObject.split_to_small_files(@fname, MAX_SIZE)
27
- @temp_files.each_with_index do |temp_file, i|
28
- single_file_upload temp_file.path, i + 1
29
- temp_file.close
30
- temp_file.unlink
27
+ raise StandardError, "FileUploadObject requires a valid file name: #{fname}" unless File.exist?(fname)
28
+
29
+ @file_size = File.size fname
30
+ raise ArgumentError, "FileUploadObject requires a non-empty file: #{fname}" if @file_size.zero?
31
+
32
+ @number_of_parts = (@file_size - 1) / MAX_SIZE + 1
33
+ payload = if @number_of_parts == 1
34
+ {}
35
+ else
36
+ {number_of_parts: @number_of_parts, mode: "multi_part",
37
+ filename: File.basename(@fname)}
38
+ end
39
+ create payload
40
+ if @number_of_parts == 1
41
+ single_file_upload
42
+ else
43
+ @temp_files = FileUploadObject.split_to_small_files(@fname, MAX_SIZE)
44
+ @temp_files.each_with_index do |temp_file, i|
45
+ single_file_upload temp_file.path, i + 1
46
+ temp_file.close
47
+ temp_file.unlink
48
+ end
49
+ NotionRubyMapping::NotionCache.instance.complete_a_file_upload_request @id
31
50
  end
32
- NotionRubyMapping::NotionCache.instance.complete_a_file_upload_request @id
33
51
  end
34
52
  end
35
- end
36
- attr_reader :id, :fname, :status
53
+ # @return [String] file upload ID
54
+ attr_reader :id
37
55
 
38
- # @return [FileUploadObject]
39
- def create(payload)
40
- nc = NotionRubyMapping::NotionCache.instance
41
- response = nc.create_file_upload_request(payload)
42
- @id = nc.hex_id response["id"]
43
- @status = response["status"]
44
- end
56
+ # @return [String] local file path or filename
57
+ attr_reader :fname
45
58
 
46
- def reload
47
- nc = NotionRubyMapping::NotionCache.instance
48
- response = nc.file_upload_request @id
49
- @status = response["status"]
50
- self
51
- end
59
+ # @return [String] last retrieved upload status
60
+ attr_reader :status
52
61
 
53
- # @param [String] fname
54
- # @param [Integer, NilClass] part_number
55
- def single_file_upload(fname = @fname, part_number = 0)
56
- if @number_of_parts > 1
57
- options = {"part_number" => part_number}
58
- status = "pending"
59
- else
60
- options = {}
61
- status = "uploaded"
62
+ # Creates a file upload through the Notion API.
63
+ #
64
+ # @param payload [Hash] file upload creation payload
65
+ # @return [String] initial upload status
66
+ # @api private
67
+ def create(payload)
68
+ nc = NotionRubyMapping::NotionCache.instance
69
+ response = nc.create_file_upload_request(payload)
70
+ @id = nc.hex_id response["id"]
71
+ @status = response["status"]
62
72
  end
63
- nc = NotionRubyMapping::NotionCache.instance
64
- response = nc.send_file_upload_request fname, @id, options
65
- return if nc.hex_id(response["id"]) == @id && response["status"] == status
66
73
 
67
- raise StandardError, "File upload failed: #{response}"
68
- end
74
+ # Reloads the current upload status from Notion.
75
+ #
76
+ # @return [FileUploadObject] self
77
+ def reload
78
+ nc = NotionRubyMapping::NotionCache.instance
79
+ response = nc.file_upload_request @id
80
+ @status = response["status"]
81
+ self
82
+ end
69
83
 
70
- def self.split_to_small_files(org_file, max_size = MAX_SIZE)
71
- raise StandardError, "File does not exist: #{org_file}" unless File.exist?(org_file)
84
+ # Uploads a local file or one part of a multipart upload.
85
+ #
86
+ # @param fname [String] path of the file to upload
87
+ # @param part_number [Integer] multipart part number
88
+ # @return [void]
89
+ # @raise [StandardError] if the upload response is invalid
90
+ # @api private
91
+ def single_file_upload(fname = @fname, part_number = 0)
92
+ if @number_of_parts > 1
93
+ options = {"part_number" => part_number}
94
+ status = "pending"
95
+ else
96
+ options = {}
97
+ status = "uploaded"
98
+ end
99
+ nc = NotionRubyMapping::NotionCache.instance
100
+ response = nc.send_file_upload_request fname, @id, options
101
+ return if nc.hex_id(response["id"]) == @id && response["status"] == status
102
+
103
+ raise StandardError, "File upload failed: #{response}"
104
+ end
105
+
106
+ # Splits a file into temporary files.
107
+ #
108
+ # The caller is responsible for closing and deleting the returned files.
109
+ #
110
+ # @param org_file [String] path of the original file
111
+ # @param max_size [Integer] maximum size of each part in bytes
112
+ # @return [Array<Tempfile>] temporary file parts
113
+ # @raise [StandardError] if the original file does not exist
114
+ # @api private
115
+ def self.split_to_small_files(org_file, max_size = MAX_SIZE)
116
+ raise StandardError, "File does not exist: #{org_file}" unless File.exist?(org_file)
72
117
 
73
- temp_files = []
74
- File.open(org_file, "rb") do |file|
75
- until file.eof?
76
- chunk = file.read(max_size)
77
- temp_file = Tempfile.new("part_")
78
- temp_file.binmode
79
- temp_file.write(chunk)
80
- temp_file.rewind
81
- temp_files << temp_file
118
+ temp_files = []
119
+ File.open(org_file, "rb") do |file|
120
+ until file.eof?
121
+ chunk = file.read(max_size)
122
+ temp_file = Tempfile.new("part_")
123
+ temp_file.binmode
124
+ temp_file.write(chunk)
125
+ temp_file.rewind
126
+ temp_files << temp_file
127
+ end
82
128
  end
129
+ temp_files
83
130
  end
84
- temp_files
85
131
  end
86
132
  end
@@ -168,7 +168,7 @@ module NotionRubyMapping
168
168
  @json = json[type]
169
169
  end
170
170
 
171
- # @return [Symbol] property type
171
+ # @return [String] property type
172
172
  def type
173
173
  self.class::TYPE
174
174
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionRubyMapping
4
- VERSION = "4.1.0"
4
+ VERSION = "4.2.0"
5
5
  NOTION_VERSION = "2026-03-11"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: notion_ruby_mapping
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hiroyuki KOBAYASHI