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.
- checksums.yaml +7 -0
- data/.env.example +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +95 -0
- data/Rakefile +120 -0
- data/benchmark/load.rb +9 -0
- data/benchmark/objects.rb +14 -0
- data/codegen/generator.rb +277 -0
- data/codegen/naming.yml +61 -0
- data/codegen/openapi.json +47206 -0
- data/codegen/openapi.lock +1 -0
- data/codegen/overrides.yml +14 -0
- data/docs/design.md +7 -0
- data/docs/recipes/csv-import.md +14 -0
- data/docs/recipes/oauth.md +9 -0
- data/docs/recipes/page-sync.md +10 -0
- data/docs/recipes/pagination.md +9 -0
- data/docs/recipes/uploads.md +9 -0
- data/docs/recipes/webhooks.md +28 -0
- data/lib/generators/notion/install_generator.rb +15 -0
- data/lib/generators/notion/templates/notion.rb +5 -0
- data/lib/notion/batch.rb +45 -0
- data/lib/notion/blocks/builder.rb +57 -0
- data/lib/notion/blocks/chunker.rb +70 -0
- data/lib/notion/blocks.rb +4 -0
- data/lib/notion/client.rb +138 -0
- data/lib/notion/compat/legacy.rb +47 -0
- data/lib/notion/compat.rb +94 -0
- data/lib/notion/config.rb +88 -0
- data/lib/notion/endpoints/async_tasks.rb +26 -0
- data/lib/notion/endpoints/base.rb +107 -0
- data/lib/notion/endpoints/blocks.rb +39 -0
- data/lib/notion/endpoints/data_sources.rb +30 -0
- data/lib/notion/endpoints/file_uploads.rb +35 -0
- data/lib/notion/endpoints/pages.rb +56 -0
- data/lib/notion/endpoints/views.rb +16 -0
- data/lib/notion/errors.rb +111 -0
- data/lib/notion/experimental.rb +12 -0
- data/lib/notion/file_uploader.rb +142 -0
- data/lib/notion/generated/endpoints/agents.rb +64 -0
- data/lib/notion/generated/endpoints/async_tasks.rb +20 -0
- data/lib/notion/generated/endpoints/blocks.rb +49 -0
- data/lib/notion/generated/endpoints/comments.rb +48 -0
- data/lib/notion/generated/endpoints/custom_emojis.rb +20 -0
- data/lib/notion/generated/endpoints/data_sources.rb +49 -0
- data/lib/notion/generated/endpoints/databases.rb +34 -0
- data/lib/notion/generated/endpoints/file_uploads.rb +51 -0
- data/lib/notion/generated/endpoints/meeting_notes.rb +27 -0
- data/lib/notion/generated/endpoints/oauth.rb +34 -0
- data/lib/notion/generated/endpoints/pages.rb +65 -0
- data/lib/notion/generated/endpoints/search.rb +20 -0
- data/lib/notion/generated/endpoints/sessions.rb +48 -0
- data/lib/notion/generated/endpoints/users.rb +34 -0
- data/lib/notion/generated/endpoints/views.rb +73 -0
- data/lib/notion/generated.rb +41 -0
- data/lib/notion/id.rb +14 -0
- data/lib/notion/markdown/local_converter.rb +34 -0
- data/lib/notion/middleware/api_version.rb +18 -0
- data/lib/notion/middleware/authentication.rb +52 -0
- data/lib/notion/middleware/compatibility.rb +17 -0
- data/lib/notion/middleware/instrumentation.rb +63 -0
- data/lib/notion/middleware/json_codec.rb +37 -0
- data/lib/notion/middleware/logging.rb +36 -0
- data/lib/notion/middleware/rate_limiter.rb +78 -0
- data/lib/notion/middleware/retry.rb +45 -0
- data/lib/notion/middleware/stack.rb +20 -0
- data/lib/notion/middleware.rb +11 -0
- data/lib/notion/multipart.rb +39 -0
- data/lib/notion/oauth.rb +91 -0
- data/lib/notion/object_factory.rb +39 -0
- data/lib/notion/objects/base.rb +62 -0
- data/lib/notion/objects/list.rb +56 -0
- data/lib/notion/objects/property_schema.rb +57 -0
- data/lib/notion/objects/property_value.rb +56 -0
- data/lib/notion/objects/types.rb +56 -0
- data/lib/notion/objects.rb +8 -0
- data/lib/notion/pagination/cursor_paginator.rb +25 -0
- data/lib/notion/query/builder.rb +117 -0
- data/lib/notion/query.rb +3 -0
- data/lib/notion/railtie.rb +11 -0
- data/lib/notion/resolver.rb +72 -0
- data/lib/notion/testing.rb +45 -0
- data/lib/notion/transport/net_http_adapter.rb +90 -0
- data/lib/notion/transport.rb +21 -0
- data/lib/notion/version.rb +5 -0
- data/lib/notion/webhooks/event.rb +7 -0
- data/lib/notion/webhooks/rack_middleware.rb +43 -0
- data/lib/notion/webhooks/signature.rb +22 -0
- data/lib/notion/webhooks.rb +7 -0
- data/lib/notion-client-ruby.rb +3 -0
- data/lib/notion.rb +53 -0
- 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
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__
|
data/codegen/naming.yml
ADDED
|
@@ -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
|