wikiwiki 0.5.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c94061d746d2242a2a78e735c43fa5fba37888231ba3a92e959aeafa5afaec3a
4
+ data.tar.gz: c9c3f4c74090aa044e10e48b72b03ad2a7b1b58713fec6e0447a871439153c05
5
+ SHA512:
6
+ metadata.gz: 9bae7372d114df3f32f4f60d3ae1b4664ff3581c30ba0215e5d705de733063b2d12faaa92c557439f29b1e9b9707e9c3993400e738b672125365b7be24d401c1
7
+ data.tar.gz: f6e397e07040ba97328609f437a30a29be059c2df7c7051d78def34c204f6d5bf6005e618014893ffddcb36e20cc41db414eb1187435776a60217b2177c960ee
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.5.0] - 2025-10-31
4
+
5
+ ### Added
6
+
7
+ - Initial release of Wikiwiki REST API client library
8
+ - Support for password and API key authentication
9
+ - Page operations: list, read, and update
10
+ - Attachment operations: list, download, upload, and delete
11
+ - Automatic rate limiting with configurable strategies (raise, wait, or no limit)
12
+ - Full RBS type signatures for type safety
13
+ - `Wiki#url` method to get the wiki URL as a frozen `URI::HTTPS` instance
14
+ - Logging support for API requests and responses with configurable logger (default: Logger.new($stdout))
15
+ - Request/response URLs and status codes logged at INFO level
16
+ - HTTP headers logged at DEBUG level (with Authorization masked as "Bearer ***")
17
+ - Each log entry is prefixed with `[wiki-id]` for easy filtering
18
+ - Logger accessible via `Wiki#logger` and `API#logger` reader methods
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 OZAWA Sakuro
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.ja.md ADDED
@@ -0,0 +1,96 @@
1
+ # Wikiwiki
2
+
3
+ {file:README.md English version}
4
+
5
+ [Wikiwiki](https://wikiwiki.jp/) REST API用のRubyクライアントライブラリです。
6
+
7
+ ## 概要
8
+
9
+ このgemは、Wikiwikiのwikiをプログラムから操作するためのシンプルなインターフェースを提供します。ページ操作(一覧取得、読み取り、書き込み)と添付ファイル管理(一覧取得、アップロード、ダウンロード、削除)をサポートしています。
10
+
11
+ ## インストール
12
+
13
+ アプリケーションのGemfileに以下の行を追加してください:
14
+
15
+ ```ruby
16
+ gem "wikiwiki"
17
+ ```
18
+
19
+ その後、以下を実行してください:
20
+
21
+ ```bash
22
+ bundle install
23
+ ```
24
+
25
+ または、直接インストールすることもできます:
26
+
27
+ ```bash
28
+ gem install wikiwiki
29
+ ```
30
+
31
+ ## 認証
32
+
33
+ APIを使用する前に、wikiの管理画面でREST APIアクセスを有効にしてください。
34
+
35
+ ### パスワード認証
36
+
37
+ ```ruby
38
+ auth = Wikiwiki::Auth.password(password: "your_admin_password")
39
+ ```
40
+
41
+ ### APIキー認証
42
+
43
+ ```ruby
44
+ auth = Wikiwiki::Auth.api_key(api_key_id: "your_api_key_id", secret: "your_secret")
45
+ ```
46
+
47
+ ## 使い方
48
+
49
+ ### 基本的な使用例
50
+
51
+ ```ruby
52
+ require "wikiwiki"
53
+
54
+ # 認証情報を使って初期化
55
+ auth = Wikiwiki::Auth.password(password: "admin_password")
56
+ wiki = Wikiwiki::Wiki.new(wiki_id: "your-wiki-id", auth:)
57
+
58
+ # すべてのページ名の一覧を取得
59
+ page_names = wiki.page_names
60
+ # => ["FrontPage", "SideBar", ...]
61
+
62
+ # ページを取得
63
+ page = wiki.page(page_name: "FrontPage")
64
+ puts page.source
65
+ puts page.timestamp
66
+
67
+ # ページを更新
68
+ wiki.update_page(page_name: "TestPage", source: <<~SOURCE)
69
+ TITLE:Test
70
+ # Hello World
71
+ SOURCE
72
+
73
+ # 添付ファイル名の一覧を取得
74
+ attachment_names = wiki.attachment_names(page_name: "FrontPage")
75
+
76
+ # 添付ファイルをダウンロード
77
+ attachment = wiki.attachment(page_name: "FrontPage", attachment_name: "logo.png")
78
+ File.binwrite("logo.png", attachment.content)
79
+ # 注意: attachment.nameをファイル名として使用する場合は、ディレクトリトラバーサル攻撃を防ぐために安全性を検証してください
80
+
81
+ # 添付ファイルをアップロード
82
+ content = File.binread("image.png")
83
+ wiki.add_attachment(page_name: "FrontPage", attachment_name: "image.png", content:)
84
+
85
+ # 添付ファイルを削除
86
+ wiki.delete_attachment(page_name: "FrontPage", attachment_name: "image.png")
87
+ ```
88
+
89
+ ## リファレンス
90
+
91
+ - [ページ操作API](https://z.wikiwiki.jp/wikiwiki-rest-api/topic/1)
92
+ - [ファイル操作API](https://z.wikiwiki.jp/wikiwiki-rest-api/topic/3)
93
+
94
+ ## ライセンス
95
+
96
+ このgemは[MITライセンス](https://opensource.org/licenses/MIT)の条件の下でオープンソースとして利用可能です。
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Wikiwiki
2
+
3
+ {file:README.ja.md 日本語版}
4
+
5
+ A Ruby client library for the [Wikiwiki](https://wikiwiki.jp/) REST API.
6
+
7
+ ## Overview
8
+
9
+ This gem provides a simple interface to interact with Wikiwiki wikis programmatically. It supports page operations (list, read, write) and attachment management (list, upload, download, delete).
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem "wikiwiki"
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ ```bash
22
+ bundle install
23
+ ```
24
+
25
+ Or install it yourself as:
26
+
27
+ ```bash
28
+ gem install wikiwiki
29
+ ```
30
+
31
+ ## Authentication
32
+
33
+ Before using the API, enable REST API access in your wiki's admin panel.
34
+
35
+ ### Password Authentication
36
+
37
+ ```ruby
38
+ auth = Wikiwiki::Auth.password(password: "your_admin_password")
39
+ ```
40
+
41
+ ### API Key Authentication
42
+
43
+ ```ruby
44
+ auth = Wikiwiki::Auth.api_key(api_key_id: "your_api_key_id", secret: "your_secret")
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ### Basic Example
50
+
51
+ ```ruby
52
+ require "wikiwiki"
53
+
54
+ # Initialize with authentication
55
+ auth = Wikiwiki::Auth.password(password: "admin_password")
56
+ wiki = Wikiwiki::Wiki.new(wiki_id: "your-wiki-id", auth:)
57
+
58
+ # List all page names
59
+ page_names = wiki.page_names
60
+ # => ["FrontPage", "SideBar", ...]
61
+
62
+ # Read a page
63
+ page = wiki.page(page_name: "FrontPage")
64
+ puts page.source
65
+ puts page.timestamp
66
+
67
+ # Update a page
68
+ wiki.update_page(page_name: "TestPage", source: <<~SOURCE)
69
+ TITLE:Test
70
+ # Hello World
71
+ SOURCE
72
+
73
+ # List attachment names
74
+ attachment_names = wiki.attachment_names(page_name: "FrontPage")
75
+
76
+ # Download an attachment
77
+ attachment = wiki.attachment(page_name: "FrontPage", attachment_name: "logo.png")
78
+ File.binwrite("logo.png", attachment.content)
79
+ # Note: If using attachment.name as filename, validate it first to prevent directory traversal attacks
80
+
81
+ # Upload an attachment
82
+ content = File.binread("image.png")
83
+ wiki.add_attachment(page_name: "FrontPage", attachment_name: "image.png", content:)
84
+
85
+ # Delete an attachment
86
+ wiki.delete_attachment(page_name: "FrontPage", attachment_name: "image.png")
87
+ ```
88
+
89
+ ## Reference
90
+
91
+ - [Page Operations API](https://z.wikiwiki.jp/wikiwiki-rest-api/topic/1) (Japanese)
92
+ - [File Operations API](https://z.wikiwiki.jp/wikiwiki-rest-api/topic/3) (Japanese)
93
+
94
+ ## License
95
+
96
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Wikiwiki
8
+ # Handles HTTP communication with the Wikiwiki REST API
9
+ #
10
+ # @example
11
+ # auth = Wikiwiki::Auth.password(password: "secret")
12
+ # api = Wikiwiki::API.new(wiki_id: "my-wiki", auth:)
13
+ # pages = api.get_pages
14
+ class API
15
+ attr_reader :logger
16
+
17
+ BASE_URL = URI.parse("https://api.wikiwiki.jp").freeze
18
+ private_constant :BASE_URL
19
+
20
+ # Initialize a new API client and authenticate
21
+ #
22
+ # @param wiki_id [String] the wiki identifier
23
+ # @param auth [Auth::Password, Auth::ApiKey] authentication credentials
24
+ # @param logger [Logger] logger instance for request/response logging
25
+ # @param rate_limiter [RateLimiter] rate limiter instance (default: RateLimiter.default)
26
+ # @raise [Error] if authentication fails
27
+ def initialize(wiki_id:, auth:, logger:, rate_limiter: RateLimiter.default)
28
+ @wiki_id = wiki_id
29
+ @rate_limiter = rate_limiter
30
+ @logger = logger
31
+ @token = authenticate(auth)
32
+ end
33
+
34
+ # Get list of all pages
35
+ #
36
+ # @return [Hash] response with "pages" key containing array of page info
37
+ # @raise [Error] if request fails
38
+ def get_pages
39
+ uri = BASE_URL + "/#{wiki_id}/pages"
40
+ response = request(:get, uri)
41
+
42
+ parse_json_response(response)
43
+ end
44
+
45
+ # Get a specific page
46
+ #
47
+ # @param encoded_page_name [String] the URL-encoded page name
48
+ # @return [Hash] response with "page", "source", and "timestamp" keys
49
+ # @raise [Error] if request fails
50
+ def get_page(encoded_page_name:)
51
+ uri = BASE_URL + "/#{wiki_id}/page/#{encoded_page_name}"
52
+ response = request(:get, uri)
53
+
54
+ parse_json_response(response)
55
+ end
56
+
57
+ # Update a page
58
+ #
59
+ # @param encoded_page_name [String] the URL-encoded page name
60
+ # @param source [String] the page source content
61
+ # @return [void]
62
+ # @raise [Error] if request fails
63
+ def put_page(encoded_page_name:, source:)
64
+ uri = BASE_URL + "/#{wiki_id}/page/#{encoded_page_name}"
65
+ response = request(:put, uri, body: {"source" => source})
66
+
67
+ parse_json_response(response)
68
+ end
69
+
70
+ # Get list of attachments on a page
71
+ #
72
+ # @param encoded_page_name [String] the URL-encoded page name
73
+ # @return [Hash] response with "attachments" key containing hash of file info
74
+ # @raise [Error] if request fails
75
+ def get_attachments(encoded_page_name:)
76
+ uri = BASE_URL + "/#{wiki_id}/page/#{encoded_page_name}/attachments"
77
+ response = request(:get, uri)
78
+
79
+ parse_json_response(response)
80
+ end
81
+
82
+ # Get a specific attachment
83
+ #
84
+ # @param encoded_page_name [String] the URL-encoded page name
85
+ # @param encoded_attachment_name [String] the URL-encoded attachment file name
86
+ # @param rev [String, nil] optional MD5 hash for specific revision
87
+ # @return [Hash] file info with Base64-encoded "src" data
88
+ # @raise [Error] if request fails
89
+ def get_attachment(encoded_page_name:, encoded_attachment_name:, rev: nil)
90
+ uri = BASE_URL + "/#{wiki_id}/page/#{encoded_page_name}/attachment/#{encoded_attachment_name}"
91
+ uri.query = "rev=#{rev}" if rev
92
+ response = request(:get, uri)
93
+
94
+ parse_json_response(response)
95
+ end
96
+
97
+ # Upload an attachment to a page
98
+ #
99
+ # @param encoded_page_name [String] the URL-encoded page name
100
+ # @param attachment_name [String] the attachment file name (not URL-encoded; sent in JSON body)
101
+ # @param encoded_content [String] Base64-encoded file data
102
+ # @return [void]
103
+ # @raise [Error] if request fails
104
+ def put_attachment(encoded_page_name:, attachment_name:, encoded_content:)
105
+ uri = BASE_URL + "/#{wiki_id}/page/#{encoded_page_name}/attachment"
106
+ response = request(:put, uri, body: {"filename" => attachment_name, "data" => encoded_content})
107
+
108
+ parse_json_response(response)
109
+ end
110
+
111
+ # Delete an attachment from a page
112
+ #
113
+ # @param encoded_page_name [String] the URL-encoded page name
114
+ # @param encoded_attachment_name [String] the URL-encoded attachment file name
115
+ # @return [void]
116
+ # @raise [Error] if request fails
117
+ def delete_attachment(encoded_page_name:, encoded_attachment_name:)
118
+ uri = BASE_URL + "/#{wiki_id}/page/#{encoded_page_name}/attachment/#{encoded_attachment_name}"
119
+ response = request(:delete, uri)
120
+
121
+ parse_json_response(response)
122
+ end
123
+
124
+ # Authenticate with the Wikiwiki API
125
+ #
126
+ # @param auth [Auth::Password, Auth::ApiKey] authentication credentials
127
+ # @return [String] JWT token
128
+ # @raise [Error] if authentication fails
129
+ private def authenticate(auth)
130
+ uri = BASE_URL + "/#{wiki_id}/auth"
131
+ response = request(:post, uri, body: auth.to_h, authenticate: false)
132
+ data = parse_json_response(response)
133
+ data["token"]
134
+ end
135
+
136
+ # Parse JSON response
137
+ #
138
+ # @param response [Net::HTTPResponse] HTTP response
139
+ # @return [Hash] parsed response body
140
+ # @raise [AuthenticationError] if authentication fails (401)
141
+ # @raise [ResourceNotFoundError] if resource not found (404)
142
+ # @raise [ServerError] if server error (5xx)
143
+ # @raise [APIError] if other API request fails
144
+ private def parse_json_response(response)
145
+ unless response.is_a?(Net::HTTPSuccess)
146
+ message = "API request failed: #{response.code} #{response.message}"
147
+ case Integer(response.code, 10)
148
+ when 401
149
+ raise AuthenticationError, message
150
+ when 404
151
+ raise ResourceNotFoundError, message
152
+ when 500..599
153
+ raise ServerError, message
154
+ else
155
+ raise APIError, message
156
+ end
157
+ end
158
+
159
+ JSON.parse(response.body)
160
+ end
161
+
162
+ # Send HTTP request
163
+ #
164
+ # @param method [Symbol] HTTP method (:get, :post, :put, :delete)
165
+ # @param uri [URI::HTTPS] request URI
166
+ # @param body [Hash, nil] request body (only for methods that permit body)
167
+ # @param authenticate [Boolean] whether to include authentication header
168
+ # @return [Net::HTTPResponse] HTTP response
169
+ private def request(method, uri, body: nil, authenticate: true)
170
+ @rate_limiter.acquire!
171
+
172
+ http = Net::HTTP.new(uri.host, uri.port)
173
+ http.use_ssl = true
174
+
175
+ request_class = Net::HTTP.const_get(method.capitalize)
176
+ request = request_class.new(uri.request_uri)
177
+
178
+ if request.request_body_permitted? && body
179
+ request["Content-Type"] = "application/json"
180
+ request.body = JSON.generate(body)
181
+ end
182
+
183
+ request["Authorization"] = "Bearer #{token}" if authenticate
184
+
185
+ log_request(method, uri, request)
186
+ response = http.request(request)
187
+ log_response(response)
188
+
189
+ response
190
+ end
191
+
192
+ # Log HTTP request details
193
+ #
194
+ # @param method [Symbol] HTTP method
195
+ # @param uri [URI::HTTPS] request URI
196
+ # @param request [Net::HTTPRequest] HTTP request object
197
+ private def log_request(method, uri, request)
198
+ logger.info("[#{wiki_id}] API Request: #{method.upcase} #{uri}")
199
+
200
+ request.each_header do |key, value|
201
+ if key.casecmp("authorization").zero?
202
+ logger.debug("[#{wiki_id}] #{key}: Bearer ***")
203
+ else
204
+ logger.debug("[#{wiki_id}] #{key}: #{value}")
205
+ end
206
+ end
207
+ end
208
+
209
+ # Log HTTP response details
210
+ #
211
+ # @param response [Net::HTTPResponse] HTTP response object
212
+ private def log_response(response)
213
+ logger.info("[#{wiki_id}] API Response: #{response.code} #{response.message}")
214
+
215
+ response.each_header do |key, value|
216
+ logger.debug("[#{wiki_id}] #{key}: #{value}")
217
+ end
218
+ end
219
+
220
+ private attr_reader :wiki_id, :token
221
+ end
222
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ # Represents a file attachment on a wiki page
5
+ #
6
+ # @example
7
+ # auth = Wikiwiki::Auth.password(password: "admin_password")
8
+ # wiki = Wikiwiki::Wiki.new(wiki_id: "my-wiki", auth:)
9
+ # page = wiki.page(page_name: "FrontPage")
10
+ # attachment_names = wiki.attachment_names(page_name: "FrontPage")
11
+ # attachment = wiki.attachment(page_name: "FrontPage", attachment_name: attachment_names.first)
12
+ # attachment.name # => "logo.png"
13
+ # attachment.size # => 12345
14
+ # attachment.time # => 2022-01-01 00:00:00 +0900
15
+ # attachment.content # => binary data (decoded)
16
+ Attachment = Data.define(:page_name, :name, :size, :time, :type, :content)
17
+
18
+ class Attachment
19
+ # Reopen the class to add YARD documentation for attributes
20
+
21
+ # @!attribute [r] page_name
22
+ # @return [String] the page name this attachment belongs to
23
+
24
+ # @!attribute [r] name
25
+ # @return [String] the attachment file name
26
+
27
+ # @!attribute [r] size
28
+ # @return [Integer] the file size in bytes
29
+
30
+ # @!attribute [r] time
31
+ # @return [Time] the upload time
32
+
33
+ # @!attribute [r] type
34
+ # @return [String] the MIME type
35
+
36
+ # @!attribute [r] content
37
+ # @return [String] the binary file content (decoded from Base64)
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ module Auth
5
+ # API key-based authentication credentials
6
+ #
7
+ # @example
8
+ # auth = Wikiwiki::Auth::ApiKey.new(api_key_id: "key_id", secret: "secret")
9
+ # auth.to_h # => {api_key_id: "key_id", secret: "secret"}
10
+ ApiKey = Data.define(:api_key_id, :secret)
11
+
12
+ class ApiKey
13
+ # Reopen the class to add YARD documentation for attributes
14
+
15
+ # @!attribute [r] api_key_id
16
+ # @return [String] the API key ID
17
+
18
+ # @!attribute [r] secret
19
+ # @return [String] the secret key
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ module Auth
5
+ # Password-based authentication credentials
6
+ #
7
+ # @example
8
+ # auth = Wikiwiki::Auth::Password.new(password: "admin_password")
9
+ # auth.to_h # => {password: "admin_password"}
10
+ Password = Data.define(:password)
11
+
12
+ class Password
13
+ # Reopen the class to add YARD documentation for attributes
14
+
15
+ # @!attribute [r] password
16
+ # @return [String] the admin password
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ # Authentication credentials module
5
+ #
6
+ # Provides factory methods for creating authentication objects
7
+ module Auth
8
+ # Create password-based authentication credentials
9
+ #
10
+ # @param password [String] admin password
11
+ # @return [Password] password authentication object
12
+ def self.password(password:) = Password.new(password:)
13
+
14
+ # Create API key-based authentication credentials
15
+ #
16
+ # @param api_key_id [String] API key ID
17
+ # @param secret [String] secret key
18
+ # @return [ApiKey] API key authentication object
19
+ def self.api_key(api_key_id:, secret:) = ApiKey.new(api_key_id:, secret:)
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ # Represents a wiki page
5
+ #
6
+ # @example
7
+ # auth = Wikiwiki::Auth.password(password: "admin_password")
8
+ # wiki = Wikiwiki::Wiki.new(wiki_id: "my-wiki", auth:)
9
+ # page = wiki.page(page_name: "FrontPage")
10
+ # page.name # => "FrontPage"
11
+ # page.source # => "TITLE:FrontPage\n..."
12
+ # page.timestamp # => 2022-01-01 00:00:00 +0900
13
+ Page = Data.define(:name, :source, :timestamp)
14
+
15
+ class Page
16
+ # Reopen the class to add YARD documentation for attributes
17
+
18
+ # @!attribute [r] name
19
+ # @return [String] the page name
20
+
21
+ # @!attribute [r] source
22
+ # @return [String] the page source content
23
+
24
+ # @!attribute [r] timestamp
25
+ # @return [Time] the last update timestamp
26
+ end
27
+ end