notion-client-ruby 0.1.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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +8 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +95 -0
  5. data/Rakefile +120 -0
  6. data/benchmark/load.rb +9 -0
  7. data/benchmark/objects.rb +14 -0
  8. data/codegen/generator.rb +277 -0
  9. data/codegen/naming.yml +61 -0
  10. data/codegen/openapi.json +47206 -0
  11. data/codegen/openapi.lock +1 -0
  12. data/codegen/overrides.yml +14 -0
  13. data/docs/design.md +7 -0
  14. data/docs/recipes/csv-import.md +14 -0
  15. data/docs/recipes/oauth.md +9 -0
  16. data/docs/recipes/page-sync.md +10 -0
  17. data/docs/recipes/pagination.md +9 -0
  18. data/docs/recipes/uploads.md +9 -0
  19. data/docs/recipes/webhooks.md +28 -0
  20. data/lib/generators/notion/install_generator.rb +15 -0
  21. data/lib/generators/notion/templates/notion.rb +5 -0
  22. data/lib/notion/batch.rb +45 -0
  23. data/lib/notion/blocks/builder.rb +57 -0
  24. data/lib/notion/blocks/chunker.rb +70 -0
  25. data/lib/notion/blocks.rb +4 -0
  26. data/lib/notion/client.rb +138 -0
  27. data/lib/notion/compat/legacy.rb +47 -0
  28. data/lib/notion/compat.rb +94 -0
  29. data/lib/notion/config.rb +88 -0
  30. data/lib/notion/endpoints/async_tasks.rb +26 -0
  31. data/lib/notion/endpoints/base.rb +107 -0
  32. data/lib/notion/endpoints/blocks.rb +39 -0
  33. data/lib/notion/endpoints/data_sources.rb +30 -0
  34. data/lib/notion/endpoints/file_uploads.rb +35 -0
  35. data/lib/notion/endpoints/pages.rb +56 -0
  36. data/lib/notion/endpoints/views.rb +16 -0
  37. data/lib/notion/errors.rb +111 -0
  38. data/lib/notion/experimental.rb +12 -0
  39. data/lib/notion/file_uploader.rb +142 -0
  40. data/lib/notion/generated/endpoints/agents.rb +64 -0
  41. data/lib/notion/generated/endpoints/async_tasks.rb +20 -0
  42. data/lib/notion/generated/endpoints/blocks.rb +49 -0
  43. data/lib/notion/generated/endpoints/comments.rb +48 -0
  44. data/lib/notion/generated/endpoints/custom_emojis.rb +20 -0
  45. data/lib/notion/generated/endpoints/data_sources.rb +49 -0
  46. data/lib/notion/generated/endpoints/databases.rb +34 -0
  47. data/lib/notion/generated/endpoints/file_uploads.rb +51 -0
  48. data/lib/notion/generated/endpoints/meeting_notes.rb +27 -0
  49. data/lib/notion/generated/endpoints/oauth.rb +34 -0
  50. data/lib/notion/generated/endpoints/pages.rb +65 -0
  51. data/lib/notion/generated/endpoints/search.rb +20 -0
  52. data/lib/notion/generated/endpoints/sessions.rb +48 -0
  53. data/lib/notion/generated/endpoints/users.rb +34 -0
  54. data/lib/notion/generated/endpoints/views.rb +73 -0
  55. data/lib/notion/generated.rb +41 -0
  56. data/lib/notion/id.rb +14 -0
  57. data/lib/notion/markdown/local_converter.rb +34 -0
  58. data/lib/notion/middleware/api_version.rb +18 -0
  59. data/lib/notion/middleware/authentication.rb +52 -0
  60. data/lib/notion/middleware/compatibility.rb +17 -0
  61. data/lib/notion/middleware/instrumentation.rb +63 -0
  62. data/lib/notion/middleware/json_codec.rb +37 -0
  63. data/lib/notion/middleware/logging.rb +36 -0
  64. data/lib/notion/middleware/rate_limiter.rb +78 -0
  65. data/lib/notion/middleware/retry.rb +45 -0
  66. data/lib/notion/middleware/stack.rb +20 -0
  67. data/lib/notion/middleware.rb +11 -0
  68. data/lib/notion/multipart.rb +39 -0
  69. data/lib/notion/oauth.rb +91 -0
  70. data/lib/notion/object_factory.rb +39 -0
  71. data/lib/notion/objects/base.rb +62 -0
  72. data/lib/notion/objects/list.rb +56 -0
  73. data/lib/notion/objects/property_schema.rb +57 -0
  74. data/lib/notion/objects/property_value.rb +56 -0
  75. data/lib/notion/objects/types.rb +56 -0
  76. data/lib/notion/objects.rb +8 -0
  77. data/lib/notion/pagination/cursor_paginator.rb +25 -0
  78. data/lib/notion/query/builder.rb +117 -0
  79. data/lib/notion/query.rb +3 -0
  80. data/lib/notion/railtie.rb +11 -0
  81. data/lib/notion/resolver.rb +72 -0
  82. data/lib/notion/testing.rb +45 -0
  83. data/lib/notion/transport/net_http_adapter.rb +90 -0
  84. data/lib/notion/transport.rb +21 -0
  85. data/lib/notion/version.rb +5 -0
  86. data/lib/notion/webhooks/event.rb +7 -0
  87. data/lib/notion/webhooks/rack_middleware.rb +43 -0
  88. data/lib/notion/webhooks/signature.rb +22 -0
  89. data/lib/notion/webhooks.rb +7 -0
  90. data/lib/notion-client-ruby.rb +3 -0
  91. data/lib/notion.rb +53 -0
  92. metadata +132 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5a808c0cdbc436edff01996e5b83903163fabaacbce4d0cbb2d86e3562a2ca4c
4
+ data.tar.gz: e4f0033f176ce1eb1613af4d38d56803e403d7a204569a2a2d3d492a8ec6bc52
5
+ SHA512:
6
+ metadata.gz: 11f5ebc790dca26fa4e949234d73e285586fe82bb3330709e7d2dfccd99ac5476eea9228d277358fec180fa2548143dc00adf3207d9e74f4da03a4315ace1f00
7
+ data.tar.gz: 79fe7e7e9d61d8717fb226e98fecade35488713da70a3babcbaee2f32c13717bfec315a11fda17ff259aac79fc7804c0b6fccb82ec9428193149a8c38cac74ef
data/.env.example ADDED
@@ -0,0 +1,8 @@
1
+ NOTION_TOKEN=
2
+ NOTION_API_VERSION=2026-03-11
3
+ NOTION_TEST_TOKEN=
4
+ NOTION_TEST_PAGE_ID=
5
+ NOTION_TEST_PARENT_PAGE_ID=
6
+ NOTION_TEST_DATABASE_ID=
7
+ NOTION_TEST_DATA_SOURCE_ID=
8
+ NOTION_TEST_SECONDARY_DATA_SOURCE_ID=
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # notion-client-ruby
2
+
3
+ A dependency-free Ruby client for the current Notion Public API. It covers all 45 paths in the official OpenAPI document, preserves unknown response fields, retries transient failures, and supports API versions `2022-06-28`, `2025-09-03`, and `2026-03-11`.
4
+
5
+ ## Installation
6
+
7
+ Until the first RubyGems release, install from a local checkout:
8
+
9
+ ```ruby
10
+ gem "notion-client-ruby", path: "../notion-client-ruby"
11
+ ```
12
+
13
+ Then:
14
+
15
+ ```ruby
16
+ require "notion"
17
+
18
+ client = Notion::Client.new(token: ENV.fetch("NOTION_TOKEN"))
19
+ me = client.users.me
20
+ puts me.name
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ Generated endpoint groups mirror the API:
26
+
27
+ ```ruby
28
+ page = client.pages.retrieve(page_id: "...")
29
+ client.pages.update(page_id: page.id, in_trash: true)
30
+
31
+ client.data_sources.query(data_source_id: "...") do |query|
32
+ query.where(:Name).contains("roadmap")
33
+ query.order(:Created, direction: :descending)
34
+ end.each_result { |result| puts result.title }
35
+ ```
36
+
37
+ Build blocks without another dependency:
38
+
39
+ ```ruby
40
+ children = Notion.blocks do
41
+ heading_1 "Design"
42
+ paragraph "Small, boring, and resilient."
43
+ end
44
+
45
+ client.blocks.append_children(block_id: "...", children: children)
46
+ ```
47
+
48
+ Raw requests remain available for endpoints newer than the bundled spec:
49
+
50
+ ```ruby
51
+ client.request(:get, "/v1/users/me")
52
+ ```
53
+
54
+ Configuration can be shared:
55
+
56
+ ```ruby
57
+ Notion.configure do |config|
58
+ config.token = ENV.fetch("NOTION_TOKEN")
59
+ config.notion_version = "2026-03-11"
60
+ config.timeout = { open: 5, read: 65 }
61
+ config.retry = { max_attempts: 5, cap: 30 }
62
+ config.rate_limit = { rate: 3.0, burst: 6, store: :memory }
63
+ config.cache = Rails.cache if defined?(Rails.cache)
64
+ config.logger = Logger.new($stdout)
65
+ end
66
+ ```
67
+
68
+ File uploads, OAuth, webhooks, enhanced Markdown endpoints, local Markdown conversion, async task polling, and view query cleanup are included. See [`docs/recipes`](docs/recipes).
69
+
70
+ ## Development
71
+
72
+ ```sh
73
+ bundle install
74
+ bundle exec rake
75
+ ```
76
+
77
+ `rake` runs specs and generated-code verification. Live tests require the variables documented in `.env.example` and are intentionally not run for pull requests from forks.
78
+
79
+ With a test connection and writable parent page configured, `bundle exec rake test:bootstrap` creates the database and two data sources required by compatibility smoke tests and prints their IDs.
80
+
81
+ The supported Ruby matrix is 3.2–4.0. `require "notion"` is kept below 50ms and parsing 1,000 pages below 200ms; run `benchmark/load.rb` and `benchmark/objects.rb` to verify both budgets. Clients are thread-safe; Ractor sharing is not supported.
82
+
83
+ Run independent requests concurrently while preserving result order:
84
+
85
+ ```ruby
86
+ pages = client.batch(concurrency: 3) do |batch|
87
+ page_ids.each { |id| batch.call { client.pages.retrieve(page_id: id) } }
88
+ end
89
+ ```
90
+
91
+ APIs under `client.experimental` may change outside the normal semantic-versioning guarantees.
92
+
93
+ ## License
94
+
95
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "digest"
6
+ require "json"
7
+ require "net/http"
8
+ require "uri"
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ task default: [:spec, "codegen:verify"]
13
+
14
+ namespace :spec do
15
+ desc "Fetch and lock the official Notion OpenAPI document"
16
+ task :fetch do
17
+ uri = URI("https://developers.notion.com/openapi.json")
18
+ content = Net::HTTP.get(uri)
19
+ File.write("codegen/openapi.json", content)
20
+ File.write("codegen/openapi.lock", "sha256: #{Digest::SHA256.hexdigest(content)}\n")
21
+ end
22
+
23
+ desc "Show whether the remote OpenAPI document differs"
24
+ task :diff do
25
+ uri = URI("https://developers.notion.com/openapi.json")
26
+ remote = Digest::SHA256.hexdigest(Net::HTTP.get(uri))
27
+ local = Digest::SHA256.file("codegen/openapi.json").hexdigest
28
+ puts(remote == local ? "OpenAPI is current" : "OpenAPI has changed")
29
+ end
30
+
31
+ desc "Report changed OpenAPI operations and schemas"
32
+ task :report do
33
+ current = JSON.parse(File.read("codegen/openapi.json"))
34
+ previous = JSON.parse(IO.popen(%w[git show HEAD:codegen/openapi.json], &:read))
35
+ operation_entries = lambda do |document|
36
+ document.fetch("paths").each_with_object({}) do |(path, operations), entries|
37
+ operations.each do |verb, operation|
38
+ next unless operation.is_a?(Hash) && operation["operationId"]
39
+
40
+ entries[[verb, path]] = operation
41
+ end
42
+ end
43
+ end
44
+ operation_label = lambda do |(verb, path)|
45
+ operation = current.dig("paths", path, verb) || previous.dig("paths", path, verb)
46
+ "`#{verb.upcase} #{path}` (#{operation['operationId']})"
47
+ end
48
+ current_operations = operation_entries.call(current)
49
+ previous_operations = operation_entries.call(previous)
50
+ current_schemas = current.dig("components", "schemas").to_h
51
+ previous_schemas = previous.dig("components", "schemas").to_h
52
+ changed_operations = (current_operations.keys & previous_operations.keys).reject do |key|
53
+ current_operations[key] == previous_operations[key]
54
+ end
55
+ changed_schemas = (current_schemas.keys & previous_schemas.keys).reject do |key|
56
+ current_schemas[key] == previous_schemas[key]
57
+ end
58
+ sections = {
59
+ "Added operations" => (current_operations.keys - previous_operations.keys).map(&operation_label),
60
+ "Removed operations (breaking-change candidates)" =>
61
+ (previous_operations.keys - current_operations.keys).map(&operation_label),
62
+ "Changed operations (breaking-change candidates)" => changed_operations.map(&operation_label),
63
+ "Added schemas" => current_schemas.keys - previous_schemas.keys,
64
+ "Removed schemas (breaking-change candidates)" => previous_schemas.keys - current_schemas.keys,
65
+ "Changed schemas (breaking-change candidates)" => changed_schemas
66
+ }
67
+ puts "# Notion OpenAPI sync report"
68
+ sections.each do |heading, values|
69
+ puts "\n## #{heading}"
70
+ puts(values.empty? ? "- None" : values.sort.map { |value| "- #{value}" })
71
+ end
72
+ end
73
+ end
74
+
75
+ desc "Generate endpoints from the vendored OpenAPI document"
76
+ task :codegen do
77
+ ruby "codegen/generator.rb"
78
+ end
79
+
80
+ namespace :codegen do
81
+ desc "Fail when generated files are stale"
82
+ task verify: :codegen do
83
+ generated = ["lib/notion/generated.rb", "lib/notion/generated", "spec/generated"]
84
+ abort "generated files are stale" unless system("git", "diff", "--exit-code", "--", *generated)
85
+ end
86
+
87
+ desc "Print endpoint coverage"
88
+ task :coverage do
89
+ require_relative "codegen/generator"
90
+ generator = Generator.new
91
+ puts "#{generator.operations.length} operations across 45 paths (100%)"
92
+ end
93
+ end
94
+
95
+ namespace :test do
96
+ desc "Create a live-test database with two data sources"
97
+ task :bootstrap do
98
+ require_relative "lib/notion"
99
+
100
+ client = Notion::Client.new(token: ENV.fetch("NOTION_TEST_TOKEN"))
101
+ parent_id = ENV.fetch("NOTION_TEST_PARENT_PAGE_ID")
102
+ rich_text = ->(content) { [{ type: "text", text: { content: content } }] }
103
+ properties = { "Name" => { title: {} } }
104
+ database = client.databases.create(
105
+ parent: { type: "page_id", page_id: parent_id },
106
+ title: rich_text.call("notion-client-ruby live tests"),
107
+ initial_data_source: { properties: properties }
108
+ )
109
+ initial_id = database.raw.fetch("data_sources").first.fetch("id")
110
+ secondary = client.data_sources.create(
111
+ parent: { database_id: database.id },
112
+ title: rich_text.call("Secondary"),
113
+ properties: properties
114
+ )
115
+
116
+ puts "NOTION_TEST_DATABASE_ID=#{database.id}"
117
+ puts "NOTION_TEST_DATA_SOURCE_ID=#{initial_id}"
118
+ puts "NOTION_TEST_SECONDARY_DATA_SOURCE_ID=#{secondary.id}"
119
+ end
120
+ end
data/benchmark/load.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
+
5
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
6
+ require "notion"
7
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
8
+ puts format("require notion: %.2fms", elapsed * 1000)
9
+ abort "require exceeded 50ms" if elapsed >= 0.05
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "notion"
4
+
5
+ raw = {
6
+ "object" => "list",
7
+ "results" => Array.new(1000) { |index| { "object" => "page", "id" => index.to_s, "properties" => {} } }
8
+ }
9
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
10
+ results = Notion::ObjectFactory.build(raw).results
11
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
12
+
13
+ puts format("parse 1000 pages: %.2fms", elapsed * 1000)
14
+ abort "parse exceeded 200ms" if results.length != 1000 || elapsed >= 0.2
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+ require "fileutils"
6
+
7
+ class Generator
8
+ HTTP_METHODS = %w[get post patch delete put].freeze
9
+
10
+ def initialize(root: File.expand_path("..", __dir__))
11
+ @root = root
12
+ @spec = JSON.parse(File.read(File.join(@root, "codegen/openapi.json")))
13
+ @names = YAML.safe_load_file(File.join(@root, "codegen/naming.yml")) || {}
14
+ @overrides = YAML.safe_load_file(File.join(@root, "codegen/overrides.yml")) || {}
15
+ end
16
+
17
+ def generate
18
+ operations.group_by { |operation| operation[:resource] }.each do |resource, items|
19
+ write("lib/notion/generated/endpoints/#{resource}.rb", endpoint_source(resource, items))
20
+ end
21
+ write("lib/notion/generated.rb", registry_source)
22
+ write("spec/generated/endpoint_coverage_spec.rb", coverage_spec_source)
23
+ end
24
+
25
+ def operations
26
+ @operations ||= @spec.fetch("paths").flat_map do |path, path_item|
27
+ path_item.filter_map do |verb, operation|
28
+ build_operation(path, verb, operation, path_item) if HTTP_METHODS.include?(verb)
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def build_operation(path, verb, operation, path_item)
36
+ operation_id = operation.fetch("operationId")
37
+ parameters = (Array(path_item["parameters"]) + Array(operation["parameters"])).map { |item| resolve(item) }
38
+ item = {
39
+ operation_id: operation_id,
40
+ name: @names.fetch(operation_id, operation_id.tr("-", "_")),
41
+ resource: resource_name(Array(operation["tags"]).first),
42
+ verb: verb,
43
+ path: path,
44
+ path_params: path.scan(/\{([^}]+)\}/).flatten,
45
+ body_required: request_body(operation)["required"] == true,
46
+ required_body_params: required_body_params(operation),
47
+ body_requirement_variants: body_requirement_variants(operation),
48
+ summary: operation["summary"],
49
+ reference: operation["x-notion-docs-ref"]
50
+ }
51
+ item.merge(query_details(parameters), body_details(operation), symbolize(@overrides.fetch(operation_id, {})))
52
+ end
53
+
54
+ def query_details(parameters)
55
+ parameters = parameters.select { |parameter| parameter["in"] == "query" }
56
+ schemas = parameters.to_h { |parameter| [parameter["name"], parameter["schema"] || {}] }
57
+ {
58
+ query_params: parameters.map { |parameter| parameter["name"] },
59
+ required_query_params: parameters.filter_map { |parameter| parameter["name"] if parameter["required"] },
60
+ query_enums: constraints(schemas, "enum"),
61
+ query_max_lengths: constraints(schemas, "maxLength")
62
+ }
63
+ end
64
+
65
+ def body_details(operation)
66
+ schema = request_body(operation).dig("content", "application/json", "schema") || {}
67
+ properties = top_level_properties(schema)
68
+ {
69
+ body_enums: constraints(properties, "enum"),
70
+ body_max_lengths: constraints(properties, "maxLength")
71
+ }
72
+ end
73
+
74
+ def constraints(schemas, keyword)
75
+ schemas.filter_map do |key, schema|
76
+ constraint = resolve(schema)[keyword]
77
+ [key, constraint] if constraint
78
+ end.to_h
79
+ end
80
+
81
+ def top_level_properties(schema)
82
+ schema = resolve(schema)
83
+ Array(schema["allOf"]).reduce(schema.fetch("properties", {})) do |properties, item|
84
+ properties.merge(top_level_properties(item))
85
+ end
86
+ end
87
+
88
+ def resolve(item)
89
+ return item unless item["$ref"]
90
+
91
+ item["$ref"].delete_prefix("#/").split("/").reduce(@spec) { |value, key| value.fetch(key) }
92
+ end
93
+
94
+ def required_body_params(operation)
95
+ body_requirement_variants(operation).reduce(:&).to_a
96
+ end
97
+
98
+ def body_requirement_variants(operation)
99
+ schema = request_body(operation).dig("content", "application/json", "schema") || {}
100
+ variants = requirement_variants(schema).map(&:uniq).uniq
101
+ variants.any?(&:empty?) ? [] : variants
102
+ end
103
+
104
+ def request_body(operation)
105
+ resolve(operation["requestBody"] || {})
106
+ end
107
+
108
+ def requirement_variants(schema)
109
+ schema = resolve(schema)
110
+ variants = [Array(schema["required"])]
111
+ Array(schema["allOf"]).each { |item| variants = combine(variants, requirement_variants(item)) }
112
+ alternatives = Array(schema["oneOf"] || schema["anyOf"]).flat_map { |item| requirement_variants(item) }
113
+ alternatives.empty? ? variants : combine(variants, alternatives)
114
+ end
115
+
116
+ def combine(left, right)
117
+ left.product(right).map { |first, second| first + second }
118
+ end
119
+
120
+ def symbolize(hash)
121
+ hash.to_h { |key, value| [key.to_sym, value] }
122
+ end
123
+
124
+ def resource_name(tag)
125
+ tag.to_s.downcase.tr(" ", "_")
126
+ end
127
+
128
+ def class_name(resource)
129
+ resource.split("_").map(&:capitalize).join
130
+ end
131
+
132
+ def endpoint_source(resource, items)
133
+ declarations = items.map { |item| operation_declaration(item) }.join("\n")
134
+ methods = items.map { |item| endpoint_method_source(item) }.join("\n")
135
+ <<~RUBY
136
+ # frozen_string_literal: true
137
+
138
+ # This file is generated. DO NOT EDIT.
139
+
140
+ module Notion
141
+ module Generated
142
+ module Endpoints
143
+ class #{class_name(resource)} < Notion::Endpoints::Base
144
+ #{declarations}
145
+
146
+ #{methods}
147
+ end
148
+ end
149
+ end
150
+ end
151
+ RUBY
152
+ end
153
+
154
+ def operation_declaration(item)
155
+ %( operation :#{item[:name]}, :#{item[:verb]}, #{item[:path].inspect}, ) +
156
+ "path: #{item[:path_params].inspect}, query: #{item[:query_params].inspect}, " \
157
+ "query_rules: { required: #{item[:required_query_params].inspect}, enums: #{hash_literal(item[:query_enums])}, " \
158
+ "max_lengths: #{hash_literal(item[:query_max_lengths])} }, " \
159
+ "body_rules: { send_empty: #{item[:body_required]}, required: #{item[:required_body_params].inspect}, " \
160
+ "variants: #{item[:body_requirement_variants].inspect}, enums: #{hash_literal(item[:body_enums])}, " \
161
+ "max_lengths: #{hash_literal(item[:body_max_lengths])} }"
162
+ end
163
+
164
+ def hash_literal(hash)
165
+ "{#{hash.map { |key, value| "#{key.inspect} => #{value.inspect}" }.join(", ")}}"
166
+ end
167
+
168
+ def endpoint_method_source(item)
169
+ required_names = (item[:path_params] + item[:required_query_params] + item[:required_body_params]).uniq
170
+ required = required_names.map { |name| "#{name}:" }
171
+ arguments = (required + ["**params", "&block"]).join(", ")
172
+ assignments = required_names.map { |name| " params[:#{name}] = #{name}" }.join("\n")
173
+ <<~RUBY.chomp
174
+ # #{item[:summary]}
175
+ # @see #{item[:reference]}
176
+ def #{item[:name]}(#{arguments})
177
+ #{assignments}
178
+ invoke_operation(:#{item[:name]}, params, &block)
179
+ end
180
+ RUBY
181
+ end
182
+
183
+ def registry_source
184
+ resources = operations.map { |operation| operation[:resource] }.uniq.sort
185
+ requires = resources.map { |resource| %(require_relative "generated/endpoints/#{resource}") }.join("\n")
186
+ entries = resources.map do |resource|
187
+ " #{resource}: Endpoints::#{class_name(resource)}"
188
+ end.join(",\n")
189
+ <<~RUBY
190
+ # frozen_string_literal: true
191
+
192
+ # This file is generated. DO NOT EDIT.
193
+
194
+ #{requires}
195
+
196
+ module Notion
197
+ module Generated
198
+ RESOURCES = {
199
+ #{entries}
200
+ }.freeze
201
+ end
202
+ end
203
+ RUBY
204
+ end
205
+
206
+ def coverage_spec_source
207
+ count = operations.length
208
+ <<~RUBY
209
+ # frozen_string_literal: true
210
+
211
+ # This file is generated. DO NOT EDIT.
212
+
213
+ RSpec.describe "generated endpoint coverage" do
214
+ it "covers every OpenAPI operation" do
215
+ count = Notion::Generated::RESOURCES.values.sum { |endpoint| endpoint.operations.length }
216
+
217
+ expect(count).to eq(#{count})
218
+ end
219
+
220
+ it "exposes required path keywords and sends every HTTP shape" do
221
+ client = Class.new do
222
+ attr_reader :last_request
223
+
224
+ def request(*args, **kwargs)
225
+ @last_request = [args, kwargs]
226
+ {}
227
+ end
228
+ end.new
229
+
230
+ Notion::Generated::RESOURCES.each_value do |endpoint_class|
231
+ endpoint = endpoint_class.new(client)
232
+ endpoint_class.operations.each do |name, operation|
233
+ params = operation[:path_params].to_h { |key| [key.to_sym, "value"] }
234
+ params.merge!(operation[:query_params].to_h { |key| [key.to_sym, "query"] })
235
+ operation[:query_rules][:enums].each { |key, values| params[key.to_sym] = values.first }
236
+ operation[:body_rules][:required].each do |key|
237
+ params[key.to_sym] = case key
238
+ when "children" then [{}]
239
+ when "parent" then { page_id: "value" }
240
+ else "value"
241
+ end
242
+ end
243
+ operation[:body_rules][:variants].first.to_a.each do |key|
244
+ params[key.to_sym] ||= key == "parent" ? { page_id: "value" } : "value"
245
+ end
246
+ operation[:body_rules][:enums].each { |key, values| params[key.to_sym] = values.first }
247
+ endpoint.method(name).parameters.each do |kind, key|
248
+ next unless kind == :keyreq
249
+
250
+ params[key] ||= key == :children ? [{}] : "value"
251
+ params[key] = { page_id: "value" } if key == :parent
252
+ end
253
+ endpoint.public_send(name, **params)
254
+
255
+ expected_path = operation[:path_params].reduce(operation[:path]) do |path, key|
256
+ path.sub("{" + key + "}", "value")
257
+ end
258
+ expect(client.last_request.first).to eq([operation[:verb], expected_path])
259
+ required = operation[:path_params] + operation[:query_rules][:required] + operation[:body_rules][:required]
260
+ required.each do |key|
261
+ expect(endpoint.method(name).parameters).to include([:keyreq, key.to_sym])
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
267
+ RUBY
268
+ end
269
+
270
+ def write(path, content)
271
+ absolute = File.join(@root, path)
272
+ FileUtils.mkdir_p(File.dirname(absolute))
273
+ File.write(absolute, content)
274
+ end
275
+ end
276
+
277
+ Generator.new.generate if $PROGRAM_NAME == __FILE__
@@ -0,0 +1,61 @@
1
+ get-self: me
2
+ get-user: retrieve
3
+ get-users: list
4
+ post-page: create
5
+ retrieve-a-page: retrieve
6
+ patch-page: update
7
+ move-page: move
8
+ retrieve-a-page-property: retrieve_property
9
+ retrieve-page-markdown: markdown
10
+ update-page-markdown: update_markdown
11
+ retrieve-async-task: retrieve
12
+ retrieve-a-block: retrieve
13
+ update-a-block: update
14
+ delete-a-block: delete
15
+ get-block-children: children
16
+ patch-block-children: append_children
17
+ retrieve-a-data-source: retrieve
18
+ update-a-data-source: update
19
+ post-database-query: query
20
+ create-a-database: create
21
+ list-data-source-templates: templates
22
+ retrieve-database: retrieve
23
+ update-database: update
24
+ create-database: create
25
+ post-search: search
26
+ create-a-comment: create
27
+ list-comments: list
28
+ retrieve-comment: retrieve
29
+ update-a-comment: update
30
+ delete-a-comment: delete
31
+ create-file: create
32
+ list-file-uploads: list
33
+ upload-file: send
34
+ complete-file-upload: complete
35
+ retrieve-file-upload: retrieve
36
+ list-custom-emojis: list
37
+ list-views: list
38
+ create-view: create
39
+ retrieve-a-view: retrieve
40
+ update-a-view: update
41
+ delete-view: delete
42
+ create-view-query: create_query
43
+ get-view-query-results: query_results
44
+ delete-view-query: delete_query
45
+ create-meeting-note: create
46
+ query-meeting-notes: query
47
+ query-agents: query
48
+ get-agent: retrieve
49
+ delete-agent: delete
50
+ get-agent-insights: insights
51
+ update-agent-status: update_status
52
+ update-agent-credit-limit: update_credit_limit
53
+ agent-batch: batch
54
+ update-session: update
55
+ retrieve-session: retrieve
56
+ query-sessions: query
57
+ query-session-events: events
58
+ cancel-session: cancel
59
+ create-a-token: token
60
+ revoke-token: revoke
61
+ introspect-token: introspect