api-regulator 0.1.0 → 0.1.2
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 +4 -4
- data/.github/workflows/spec.yml +35 -0
- data/.gitignore +1 -0
- data/Gemfile.lock +61 -61
- data/README.md +2 -3
- data/bin/console +1 -1
- data/lib/api-regulator.rb +4 -2
- data/lib/api_regulator/api.rb +26 -6
- data/lib/api_regulator/configuration.rb +2 -7
- data/lib/api_regulator/dsl.rb +9 -2
- data/lib/api_regulator/open_api_generator.rb +50 -16
- data/lib/api_regulator/param.rb +12 -3
- data/lib/api_regulator/security.rb +13 -0
- data/lib/api_regulator/shared_schema.rb +18 -9
- data/lib/api_regulator/validator.rb +7 -7
- data/lib/api_regulator/version.rb +1 -1
- data/lib/api_regulator/webhook.rb +59 -0
- data/lib/tasks/api_regulator_tasks.rake +137 -6
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 805a7ee5772352ec9d9f1e8bea5f6f79ef5dd67c170bfb3ca62898a2685a0067
|
4
|
+
data.tar.gz: 75d36c102ed4f9494cfb8f97be7cca09de8206006fb1ca28352734f15ecc29d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45f235550878457b9af59dc1f3d4d2b70ac8afe581dfc35d9f5a13a37da67eb17ef9f6e4da1d877352eca27229847367909645f66d20cdaa807d9c52f133c894
|
7
|
+
data.tar.gz: a67c9b219a20a6e9dffd99fa33f285222452621b99c7304ffd3e7366f86fb8046505857c27047a86161bf3232cf07b81e35a75a7375fe7bace8a0708982ba9c1
|
@@ -0,0 +1,35 @@
|
|
1
|
+
name: Run Specs
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- main
|
7
|
+
pull_request:
|
8
|
+
branches:
|
9
|
+
- main
|
10
|
+
|
11
|
+
jobs:
|
12
|
+
test:
|
13
|
+
name: Run Tests
|
14
|
+
runs-on: ubuntu-latest
|
15
|
+
|
16
|
+
steps:
|
17
|
+
# Checkout the code
|
18
|
+
- name: Checkout code
|
19
|
+
uses: actions/checkout@v3
|
20
|
+
|
21
|
+
# Set up Ruby
|
22
|
+
- name: Set up Ruby
|
23
|
+
uses: ruby/setup-ruby@v1
|
24
|
+
with:
|
25
|
+
ruby-version: '3.2' # Specify your gem's Ruby version
|
26
|
+
bundler-cache: true # Cache gems to speed up the workflow
|
27
|
+
|
28
|
+
# Install dependencies
|
29
|
+
- name: Install dependencies
|
30
|
+
run: bundle install
|
31
|
+
|
32
|
+
# Run RSpec tests
|
33
|
+
- name: Run specs
|
34
|
+
run: bundle exec rspec
|
35
|
+
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,36 +1,36 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
api-regulator (0.1.
|
4
|
+
api-regulator (0.1.2)
|
5
5
|
activemodel
|
6
6
|
activesupport
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
|
-
actioncable (7.2.2)
|
12
|
-
actionpack (= 7.2.2)
|
13
|
-
activesupport (= 7.2.2)
|
11
|
+
actioncable (7.2.2.1)
|
12
|
+
actionpack (= 7.2.2.1)
|
13
|
+
activesupport (= 7.2.2.1)
|
14
14
|
nio4r (~> 2.0)
|
15
15
|
websocket-driver (>= 0.6.1)
|
16
16
|
zeitwerk (~> 2.6)
|
17
|
-
actionmailbox (7.2.2)
|
18
|
-
actionpack (= 7.2.2)
|
19
|
-
activejob (= 7.2.2)
|
20
|
-
activerecord (= 7.2.2)
|
21
|
-
activestorage (= 7.2.2)
|
22
|
-
activesupport (= 7.2.2)
|
17
|
+
actionmailbox (7.2.2.1)
|
18
|
+
actionpack (= 7.2.2.1)
|
19
|
+
activejob (= 7.2.2.1)
|
20
|
+
activerecord (= 7.2.2.1)
|
21
|
+
activestorage (= 7.2.2.1)
|
22
|
+
activesupport (= 7.2.2.1)
|
23
23
|
mail (>= 2.8.0)
|
24
|
-
actionmailer (7.2.2)
|
25
|
-
actionpack (= 7.2.2)
|
26
|
-
actionview (= 7.2.2)
|
27
|
-
activejob (= 7.2.2)
|
28
|
-
activesupport (= 7.2.2)
|
24
|
+
actionmailer (7.2.2.1)
|
25
|
+
actionpack (= 7.2.2.1)
|
26
|
+
actionview (= 7.2.2.1)
|
27
|
+
activejob (= 7.2.2.1)
|
28
|
+
activesupport (= 7.2.2.1)
|
29
29
|
mail (>= 2.8.0)
|
30
30
|
rails-dom-testing (~> 2.2)
|
31
|
-
actionpack (7.2.2)
|
32
|
-
actionview (= 7.2.2)
|
33
|
-
activesupport (= 7.2.2)
|
31
|
+
actionpack (7.2.2.1)
|
32
|
+
actionview (= 7.2.2.1)
|
33
|
+
activesupport (= 7.2.2.1)
|
34
34
|
nokogiri (>= 1.8.5)
|
35
35
|
racc
|
36
36
|
rack (>= 2.2.4, < 3.2)
|
@@ -39,35 +39,35 @@ GEM
|
|
39
39
|
rails-dom-testing (~> 2.2)
|
40
40
|
rails-html-sanitizer (~> 1.6)
|
41
41
|
useragent (~> 0.16)
|
42
|
-
actiontext (7.2.2)
|
43
|
-
actionpack (= 7.2.2)
|
44
|
-
activerecord (= 7.2.2)
|
45
|
-
activestorage (= 7.2.2)
|
46
|
-
activesupport (= 7.2.2)
|
42
|
+
actiontext (7.2.2.1)
|
43
|
+
actionpack (= 7.2.2.1)
|
44
|
+
activerecord (= 7.2.2.1)
|
45
|
+
activestorage (= 7.2.2.1)
|
46
|
+
activesupport (= 7.2.2.1)
|
47
47
|
globalid (>= 0.6.0)
|
48
48
|
nokogiri (>= 1.8.5)
|
49
|
-
actionview (7.2.2)
|
50
|
-
activesupport (= 7.2.2)
|
49
|
+
actionview (7.2.2.1)
|
50
|
+
activesupport (= 7.2.2.1)
|
51
51
|
builder (~> 3.1)
|
52
52
|
erubi (~> 1.11)
|
53
53
|
rails-dom-testing (~> 2.2)
|
54
54
|
rails-html-sanitizer (~> 1.6)
|
55
|
-
activejob (7.2.2)
|
56
|
-
activesupport (= 7.2.2)
|
55
|
+
activejob (7.2.2.1)
|
56
|
+
activesupport (= 7.2.2.1)
|
57
57
|
globalid (>= 0.3.6)
|
58
|
-
activemodel (7.2.2)
|
59
|
-
activesupport (= 7.2.2)
|
60
|
-
activerecord (7.2.2)
|
61
|
-
activemodel (= 7.2.2)
|
62
|
-
activesupport (= 7.2.2)
|
58
|
+
activemodel (7.2.2.1)
|
59
|
+
activesupport (= 7.2.2.1)
|
60
|
+
activerecord (7.2.2.1)
|
61
|
+
activemodel (= 7.2.2.1)
|
62
|
+
activesupport (= 7.2.2.1)
|
63
63
|
timeout (>= 0.4.0)
|
64
|
-
activestorage (7.2.2)
|
65
|
-
actionpack (= 7.2.2)
|
66
|
-
activejob (= 7.2.2)
|
67
|
-
activerecord (= 7.2.2)
|
68
|
-
activesupport (= 7.2.2)
|
64
|
+
activestorage (7.2.2.1)
|
65
|
+
actionpack (= 7.2.2.1)
|
66
|
+
activejob (= 7.2.2.1)
|
67
|
+
activerecord (= 7.2.2.1)
|
68
|
+
activesupport (= 7.2.2.1)
|
69
69
|
marcel (~> 1.0)
|
70
|
-
activesupport (7.2.2)
|
70
|
+
activesupport (7.2.2.1)
|
71
71
|
base64
|
72
72
|
benchmark (>= 0.3)
|
73
73
|
bigdecimal
|
@@ -99,7 +99,7 @@ GEM
|
|
99
99
|
irb (1.14.1)
|
100
100
|
rdoc (>= 4.0.0)
|
101
101
|
reline (>= 0.4.2)
|
102
|
-
logger (1.6.
|
102
|
+
logger (1.6.3)
|
103
103
|
loofah (2.23.1)
|
104
104
|
crass (~> 1.0.2)
|
105
105
|
nokogiri (>= 1.12.0)
|
@@ -111,7 +111,7 @@ GEM
|
|
111
111
|
marcel (1.0.4)
|
112
112
|
mini_mime (1.1.5)
|
113
113
|
mini_portile2 (2.8.8)
|
114
|
-
minitest (5.25.
|
114
|
+
minitest (5.25.4)
|
115
115
|
net-imap (0.5.1)
|
116
116
|
date
|
117
117
|
net-protocol
|
@@ -122,10 +122,10 @@ GEM
|
|
122
122
|
net-smtp (0.5.0)
|
123
123
|
net-protocol
|
124
124
|
nio4r (2.7.4)
|
125
|
-
nokogiri (1.
|
125
|
+
nokogiri (1.17.2)
|
126
126
|
mini_portile2 (~> 2.8.2)
|
127
127
|
racc (~> 1.4)
|
128
|
-
nokogiri (1.
|
128
|
+
nokogiri (1.17.2-arm64-darwin)
|
129
129
|
racc (~> 1.4)
|
130
130
|
psych (5.2.0)
|
131
131
|
stringio
|
@@ -137,30 +137,30 @@ GEM
|
|
137
137
|
rack (>= 1.3)
|
138
138
|
rackup (2.2.1)
|
139
139
|
rack (>= 3)
|
140
|
-
rails (7.2.2)
|
141
|
-
actioncable (= 7.2.2)
|
142
|
-
actionmailbox (= 7.2.2)
|
143
|
-
actionmailer (= 7.2.2)
|
144
|
-
actionpack (= 7.2.2)
|
145
|
-
actiontext (= 7.2.2)
|
146
|
-
actionview (= 7.2.2)
|
147
|
-
activejob (= 7.2.2)
|
148
|
-
activemodel (= 7.2.2)
|
149
|
-
activerecord (= 7.2.2)
|
150
|
-
activestorage (= 7.2.2)
|
151
|
-
activesupport (= 7.2.2)
|
140
|
+
rails (7.2.2.1)
|
141
|
+
actioncable (= 7.2.2.1)
|
142
|
+
actionmailbox (= 7.2.2.1)
|
143
|
+
actionmailer (= 7.2.2.1)
|
144
|
+
actionpack (= 7.2.2.1)
|
145
|
+
actiontext (= 7.2.2.1)
|
146
|
+
actionview (= 7.2.2.1)
|
147
|
+
activejob (= 7.2.2.1)
|
148
|
+
activemodel (= 7.2.2.1)
|
149
|
+
activerecord (= 7.2.2.1)
|
150
|
+
activestorage (= 7.2.2.1)
|
151
|
+
activesupport (= 7.2.2.1)
|
152
152
|
bundler (>= 1.15.0)
|
153
|
-
railties (= 7.2.2)
|
153
|
+
railties (= 7.2.2.1)
|
154
154
|
rails-dom-testing (2.2.0)
|
155
155
|
activesupport (>= 5.0.0)
|
156
156
|
minitest
|
157
157
|
nokogiri (>= 1.6)
|
158
|
-
rails-html-sanitizer (1.6.
|
158
|
+
rails-html-sanitizer (1.6.2)
|
159
159
|
loofah (~> 2.21)
|
160
160
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
161
|
-
railties (7.2.2)
|
162
|
-
actionpack (= 7.2.2)
|
163
|
-
activesupport (= 7.2.2)
|
161
|
+
railties (7.2.2.1)
|
162
|
+
actionpack (= 7.2.2.1)
|
163
|
+
activesupport (= 7.2.2.1)
|
164
164
|
irb (~> 1.13)
|
165
165
|
rackup (>= 1.0.0)
|
166
166
|
rake (>= 12.2)
|
@@ -192,13 +192,13 @@ GEM
|
|
192
192
|
rspec-mocks (~> 3.10)
|
193
193
|
rspec-support (~> 3.10)
|
194
194
|
rspec-support (3.13.1)
|
195
|
-
securerandom (0.
|
195
|
+
securerandom (0.4.0)
|
196
196
|
stringio (3.1.2)
|
197
197
|
thor (1.3.2)
|
198
198
|
timeout (0.4.2)
|
199
199
|
tzinfo (2.0.6)
|
200
200
|
concurrent-ruby (~> 1.0)
|
201
|
-
useragent (0.16.
|
201
|
+
useragent (0.16.11)
|
202
202
|
websocket-driver (0.7.6)
|
203
203
|
websocket-extensions (>= 0.1.0)
|
204
204
|
websocket-extensions (0.1.5)
|
data/README.md
CHANGED
@@ -48,9 +48,8 @@ Run `bundle install` to install the gem.
|
|
48
48
|
|
49
49
|
```ruby
|
50
50
|
ApiRegulator.configure do |config|
|
51
|
-
config.base_controller = "Api::ApplicationController" # Set your base API controller
|
52
51
|
config.api_base_url = "/api/v1" # Set a common base path for your API endpoints
|
53
|
-
config.docs_path = Rails.root.join("doc"
|
52
|
+
config.docs_path = Rails.root.join("doc").to_s # Path for folder for docs
|
54
53
|
config.app_name = "My API" # shows in docs
|
55
54
|
config.rdme_api_id = ENV["RDME_API_ID"] # Optional: ReadMe API ID for schema uploads
|
56
55
|
config.servers = [
|
@@ -111,7 +110,7 @@ end
|
|
111
110
|
Define reusable schemas for common responses in your initializer:
|
112
111
|
|
113
112
|
```ruby
|
114
|
-
ApiRegulator.
|
113
|
+
ApiRegulator.register_shared_schema :validation_errors, "Validation error response" do
|
115
114
|
param :errors, :array, desc: "Array of validation errors", items_type: :string
|
116
115
|
end
|
117
116
|
```
|
data/bin/console
CHANGED
data/lib/api-regulator.rb
CHANGED
@@ -5,10 +5,12 @@ require_relative 'api_regulator/dsl'
|
|
5
5
|
require_relative 'api_regulator/formats'
|
6
6
|
require_relative 'api_regulator/open_api_generator'
|
7
7
|
require_relative 'api_regulator/param'
|
8
|
+
require_relative 'api_regulator/security'
|
8
9
|
require_relative 'api_regulator/shared_schema'
|
9
10
|
require_relative 'api_regulator/validation_error'
|
10
11
|
require_relative 'api_regulator/validator'
|
11
12
|
require_relative 'api_regulator/version'
|
13
|
+
require_relative 'api_regulator/webhook'
|
12
14
|
|
13
15
|
# Load tasks if Rails is present
|
14
16
|
if defined?(Rake)
|
@@ -28,8 +30,8 @@ module ApiRegulator
|
|
28
30
|
|
29
31
|
def prepare_validators
|
30
32
|
Rails.application.eager_load! # Ensure all controllers and API definitions are loaded
|
31
|
-
|
32
|
-
ApiRegulator::Validator.build_all(api_definitions)
|
33
|
+
|
34
|
+
ApiRegulator::Validator.build_all(ApiRegulator.api_definitions)
|
33
35
|
end
|
34
36
|
end
|
35
37
|
end
|
data/lib/api_regulator/api.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
module ApiRegulator
|
2
2
|
class Api
|
3
|
-
attr_reader :controller_class, :controller_path, :controller_name, :action_name, :description, :params, :responses
|
3
|
+
attr_reader :controller_class, :controller_path, :controller_name, :action_name, :description, :title, :params, :responses
|
4
4
|
|
5
|
-
def initialize(controller_class, action_name,
|
5
|
+
def initialize(controller_class, action_name, desc: nil, title: nil, &block)
|
6
6
|
@controller_class = controller_class
|
7
7
|
@controller_name = controller_class.name
|
8
8
|
@controller_path = controller_class.controller_path
|
9
9
|
@action_name = action_name.to_s
|
10
|
-
@description =
|
10
|
+
@description = desc
|
11
|
+
@title = title
|
11
12
|
|
12
13
|
@params = []
|
13
14
|
@responses = {}
|
@@ -20,11 +21,20 @@ module ApiRegulator
|
|
20
21
|
@params << param
|
21
22
|
end
|
22
23
|
|
23
|
-
def ref(ref_name)
|
24
|
-
shared_schema = ApiRegulator.
|
24
|
+
def ref(ref_name, except: [], only: [])
|
25
|
+
shared_schema = ApiRegulator.shared_schema(ref_name)
|
25
26
|
raise "Shared schema #{ref_name} not found" unless shared_schema
|
26
27
|
|
27
|
-
|
28
|
+
# Filter parameters based on `only` or `except` options
|
29
|
+
filtered_params = shared_schema.params
|
30
|
+
|
31
|
+
if only.any?
|
32
|
+
filtered_params = filtered_params.select { |param| only.include?(param.name) }
|
33
|
+
elsif except.any?
|
34
|
+
filtered_params = filtered_params.reject { |param| except.include?(param.name) }
|
35
|
+
end
|
36
|
+
|
37
|
+
filtered_params.each do |shared_param|
|
28
38
|
@params << shared_param
|
29
39
|
end
|
30
40
|
end
|
@@ -79,4 +89,14 @@ module ApiRegulator
|
|
79
89
|
http_method != "get"
|
80
90
|
end
|
81
91
|
end
|
92
|
+
|
93
|
+
class << self
|
94
|
+
def api_definitions
|
95
|
+
@api_definitions ||= []
|
96
|
+
end
|
97
|
+
|
98
|
+
def reset_api_definitions
|
99
|
+
@api_definitions = []
|
100
|
+
end
|
101
|
+
end
|
82
102
|
end
|
@@ -1,18 +1,13 @@
|
|
1
1
|
module ApiRegulator
|
2
2
|
class Configuration
|
3
|
-
attr_accessor :
|
3
|
+
attr_accessor :api_base_url, :app_name, :docs_path, :rdme_api_id, :servers
|
4
4
|
|
5
5
|
def initialize
|
6
6
|
# Set default values
|
7
|
-
@base_controller = "ApplicationController"
|
8
7
|
@api_base_url = "api/v1"
|
9
8
|
@app_name = "API Documentation"
|
10
|
-
@docs_path = "
|
9
|
+
@docs_path = "doc"
|
11
10
|
@servers = []
|
12
11
|
end
|
13
|
-
|
14
|
-
def base_controller_klass
|
15
|
-
ApiRegulator.configuration.base_controller.constantize
|
16
|
-
end
|
17
12
|
end
|
18
13
|
end
|
data/lib/api_regulator/dsl.rb
CHANGED
@@ -9,17 +9,24 @@ module ApiRegulator
|
|
9
9
|
end
|
10
10
|
|
11
11
|
module ClassMethods
|
12
|
-
def api(controller_class, action,
|
12
|
+
def api(controller_class, action, desc: nil, title: nil, &block)
|
13
13
|
@api_definitions ||= []
|
14
14
|
|
15
15
|
api_definition = Api.new(
|
16
16
|
controller_class,
|
17
17
|
action.to_s,
|
18
|
-
|
18
|
+
desc: desc,
|
19
|
+
title: title,
|
19
20
|
&block
|
20
21
|
)
|
21
22
|
|
22
23
|
@api_definitions << api_definition
|
24
|
+
ApiRegulator.api_definitions << api_definition
|
25
|
+
end
|
26
|
+
|
27
|
+
def webhook(event_name, desc: nil, title: nil, tags: [], &block)
|
28
|
+
webhook = Webhook.new(event_name, desc: desc, title: title, tags: tags, &block)
|
29
|
+
ApiRegulator.webhook_definitions << webhook
|
23
30
|
end
|
24
31
|
|
25
32
|
def api_definitions
|
@@ -16,13 +16,15 @@ module ApiRegulator
|
|
16
16
|
|
17
17
|
add_components(schema)
|
18
18
|
add_security(schema)
|
19
|
+
add_webhooks(schema)
|
19
20
|
|
20
21
|
api_definitions.each do |api|
|
21
22
|
add_api_to_schema(schema, api)
|
22
23
|
end
|
23
24
|
|
24
|
-
|
25
|
-
|
25
|
+
schema_path = "#{ApiRegulator.configuration.docs_path}/openapi.json"
|
26
|
+
File.write(schema_path, JSON.pretty_generate(schema))
|
27
|
+
puts "OpenAPI schema generated: #{schema_path}"
|
26
28
|
end
|
27
29
|
|
28
30
|
private
|
@@ -30,13 +32,13 @@ module ApiRegulator
|
|
30
32
|
def self.add_api_to_schema(schema, api)
|
31
33
|
schema[:paths][api.path] ||= {}
|
32
34
|
data = {
|
33
|
-
summary: api.
|
34
|
-
description: api.description,
|
35
|
+
summary: api.title,
|
36
|
+
description: api.description.presence || api.title,
|
35
37
|
operationId: api.operation_id,
|
36
38
|
tags: api.tags,
|
37
39
|
parameters: generate_parameters(api),
|
38
40
|
responses: generate_responses(api)
|
39
|
-
}
|
41
|
+
}.compact
|
40
42
|
|
41
43
|
data[:requestBody] = generate_request_body(api) if api.allows_body?
|
42
44
|
data.delete(:requestBody) if data[:requestBody].blank?
|
@@ -113,16 +115,20 @@ module ApiRegulator
|
|
113
115
|
def self.generate_param_schema(param)
|
114
116
|
schema = {}
|
115
117
|
|
118
|
+
schema[:description] = param.desc if param.desc.present?
|
119
|
+
|
116
120
|
if param.parameter?
|
117
121
|
schema[:in] = param.location
|
118
122
|
schema[:schema] = { type: param.type.to_s.downcase }
|
123
|
+
generate_param_schema_details(param, schema[:schema])
|
119
124
|
else
|
120
125
|
schema[:type] = param.type.to_s.downcase
|
126
|
+
generate_param_schema_details(param, schema)
|
121
127
|
end
|
128
|
+
schema
|
129
|
+
end
|
122
130
|
|
123
|
-
|
124
|
-
|
125
|
-
|
131
|
+
def self.generate_param_schema_details(param, schema)
|
126
132
|
# Add length constraints
|
127
133
|
if param.options[:length]
|
128
134
|
schema[:minLength] = param.options[:length][:minimum] if param.options[:length][:minimum]
|
@@ -142,12 +148,12 @@ module ApiRegulator
|
|
142
148
|
end
|
143
149
|
end
|
144
150
|
|
145
|
-
if param.options[:numericality]
|
146
|
-
schema[:type] =
|
147
|
-
schema[:minimum] =
|
148
|
-
schema[:maximum] =
|
149
|
-
schema[:exclusiveMinimum] =
|
150
|
-
schema[:exclusiveMaximum] =
|
151
|
+
if numericality = param.options[:numericality]
|
152
|
+
schema[:type] = numericality[:only_integer] || schema[:type] == "integer" ? 'integer' : 'number'
|
153
|
+
schema[:minimum] = numericality[:greater_than_or_equal_to] if numericality[:greater_than_or_equal_to]
|
154
|
+
schema[:maximum] = numericality[:less_than_or_equal_to] if numericality[:less_than_or_equal_to]
|
155
|
+
schema[:exclusiveMinimum] = numericality[:greater_than] if numericality[:greater_than]
|
156
|
+
schema[:exclusiveMaximum] = numericality[:less_than] if numericality[:less_than]
|
151
157
|
end
|
152
158
|
|
153
159
|
if param.options[:inclusion]
|
@@ -160,13 +166,12 @@ module ApiRegulator
|
|
160
166
|
|
161
167
|
schema[:nullable] = true if param.options[:allow_nil]
|
162
168
|
|
163
|
-
schema
|
164
169
|
end
|
165
170
|
|
166
171
|
def self.generate_responses(api)
|
167
172
|
api.responses.each_with_object({}) do |(status_code, schema), responses|
|
168
173
|
if schema.options[:ref]
|
169
|
-
shared_schema = ApiRegulator.
|
174
|
+
shared_schema = ApiRegulator.shared_schema(schema.options[:ref])
|
170
175
|
raise "Shared schema not found for ref: #{schema.options[:ref]}" unless shared_schema
|
171
176
|
|
172
177
|
responses[status_code.to_s] = {
|
@@ -177,6 +182,10 @@ module ApiRegulator
|
|
177
182
|
}
|
178
183
|
}
|
179
184
|
}.compact
|
185
|
+
elsif schema.children.empty?
|
186
|
+
responses[status_code.to_s] = {
|
187
|
+
description: schema.desc.presence
|
188
|
+
}
|
180
189
|
else
|
181
190
|
responses[status_code.to_s] = {
|
182
191
|
description: schema.desc.presence,
|
@@ -195,6 +204,31 @@ module ApiRegulator
|
|
195
204
|
add_security_schemes(schema)
|
196
205
|
end
|
197
206
|
|
207
|
+
def self.add_webhooks(schema)
|
208
|
+
return if ApiRegulator.webhook_definitions.empty?
|
209
|
+
|
210
|
+
schema[:webhooks] ||= {}
|
211
|
+
ApiRegulator.webhook_definitions.each do |webhook|
|
212
|
+
schema[:webhooks][webhook.event_name] = {
|
213
|
+
post: {
|
214
|
+
summary: webhook.title,
|
215
|
+
description: webhook.description.presence || webhook.title,
|
216
|
+
tags: webhook.tags,
|
217
|
+
requestBody: {
|
218
|
+
required: true,
|
219
|
+
content: {
|
220
|
+
'application/json' => {
|
221
|
+
schema: expand_nested_params(webhook.params),
|
222
|
+
examples: webhook.examples || {}
|
223
|
+
}
|
224
|
+
}
|
225
|
+
},
|
226
|
+
responses: generate_responses(webhook)
|
227
|
+
}.compact
|
228
|
+
}
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
198
232
|
def self.add_shared_schemas(schema)
|
199
233
|
return unless ApiRegulator.shared_schemas.present?
|
200
234
|
|
data/lib/api_regulator/param.rb
CHANGED
@@ -19,11 +19,20 @@ module ApiRegulator
|
|
19
19
|
@children << child
|
20
20
|
end
|
21
21
|
|
22
|
-
def ref(ref_name)
|
23
|
-
shared_schema = ApiRegulator.
|
22
|
+
def ref(ref_name, except: [], only: [])
|
23
|
+
shared_schema = ApiRegulator.shared_schema(ref_name)
|
24
24
|
raise "Shared schema #{ref_name} not found" unless shared_schema
|
25
25
|
|
26
|
-
|
26
|
+
# Filter parameters based on `only` or `except` options
|
27
|
+
filtered_params = shared_schema.params
|
28
|
+
|
29
|
+
if only.any?
|
30
|
+
filtered_params = filtered_params.select { |param| only.include?(param.name) }
|
31
|
+
elsif except.any?
|
32
|
+
filtered_params = filtered_params.reject { |param| except.include?(param.name) }
|
33
|
+
end
|
34
|
+
|
35
|
+
filtered_params.each do |shared_param|
|
27
36
|
@children << shared_param
|
28
37
|
end
|
29
38
|
end
|
@@ -15,23 +15,32 @@ module ApiRegulator
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
@shared_schemas = {}
|
19
|
+
@shared_schema_registry = {}
|
20
|
+
|
18
21
|
class << self
|
19
|
-
attr_accessor :
|
22
|
+
attr_accessor :shared_schema_registry
|
20
23
|
|
21
24
|
def shared_schemas
|
22
|
-
|
23
|
-
|
25
|
+
shared_schema_registry.each do |name, (description, block)|
|
26
|
+
@shared_schemas[name] = SharedSchema.new(name, description, &block)
|
27
|
+
end
|
24
28
|
|
25
|
-
|
26
|
-
@security_schemes ||= {}
|
29
|
+
@shared_schemas
|
27
30
|
end
|
28
31
|
|
29
|
-
def shared_schema(name
|
30
|
-
|
32
|
+
def shared_schema(name)
|
33
|
+
if shared_schema_registry[name]
|
34
|
+
description, block = shared_schema_registry.delete(name)
|
35
|
+
|
36
|
+
@shared_schemas[name] = SharedSchema.new(name, description, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
@shared_schemas[name]
|
31
40
|
end
|
32
41
|
|
33
|
-
def
|
34
|
-
|
42
|
+
def register_shared_schema(name, description, &block)
|
43
|
+
shared_schema_registry[name] = [description, block]
|
35
44
|
end
|
36
45
|
end
|
37
46
|
end
|
@@ -110,7 +110,7 @@ module ApiRegulator
|
|
110
110
|
end
|
111
111
|
|
112
112
|
raw_value.each_with_index do |value, index|
|
113
|
-
unless value.is_a?(Hash)
|
113
|
+
unless value.is_a?(Hash) || raw_value.is_a?(ActionController::Parameters)
|
114
114
|
errors.add("#{attribute}[#{index}]", "must be a hash")
|
115
115
|
next
|
116
116
|
end
|
@@ -164,7 +164,8 @@ module ApiRegulator
|
|
164
164
|
errors.add(attribute, "can't be blank") if param.options[:presence]
|
165
165
|
return
|
166
166
|
end
|
167
|
-
|
167
|
+
|
168
|
+
unless raw_value.is_a?(Hash) || raw_value.is_a?(ActionController::Parameters)
|
168
169
|
errors.add(attribute, "must be a hash")
|
169
170
|
return
|
170
171
|
end
|
@@ -231,6 +232,10 @@ module ApiRegulator
|
|
231
232
|
end
|
232
233
|
end
|
233
234
|
|
235
|
+
def self.reset_validators
|
236
|
+
@validators = {}
|
237
|
+
end
|
238
|
+
|
234
239
|
def initialize(attributes = {})
|
235
240
|
@raw_attributes = attributes.deep_symbolize_keys
|
236
241
|
allowed_attributes = attributes.slice(*self.class.defined_attributes.map(&:to_sym))
|
@@ -238,8 +243,3 @@ module ApiRegulator
|
|
238
243
|
end
|
239
244
|
end
|
240
245
|
end
|
241
|
-
|
242
|
-
class FooValidator
|
243
|
-
include ActiveModel::Model
|
244
|
-
include ActiveModel::Attributes
|
245
|
-
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module ApiRegulator
|
2
|
+
class Webhook
|
3
|
+
attr_reader :event_name, :description, :params, :responses, :examples, :tags, :title
|
4
|
+
|
5
|
+
def initialize(event_name, desc: nil, title: nil, tags: [], &block)
|
6
|
+
@event_name = event_name
|
7
|
+
@description = desc
|
8
|
+
@title = title
|
9
|
+
@tags = tags
|
10
|
+
@params = []
|
11
|
+
@responses = {}
|
12
|
+
|
13
|
+
instance_eval(&block) if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def param(name, type = nil, item_type: nil, desc: "", location: :body, **options, &block)
|
17
|
+
param = Param.new(name, type, item_type: item_type, desc: desc, location: location, **options, &block)
|
18
|
+
@params << param
|
19
|
+
end
|
20
|
+
|
21
|
+
def ref(ref_name, except: [], only: [])
|
22
|
+
shared_schema = ApiRegulator.shared_schema(ref_name)
|
23
|
+
raise "Shared schema #{ref_name} not found" unless shared_schema
|
24
|
+
|
25
|
+
# Filter parameters based on `only` or `except` options
|
26
|
+
filtered_params = shared_schema.params
|
27
|
+
|
28
|
+
if only.any?
|
29
|
+
filtered_params = filtered_params.select { |param| only.include?(param.name) }
|
30
|
+
elsif except.any?
|
31
|
+
filtered_params = filtered_params.reject { |param| except.include?(param.name) }
|
32
|
+
end
|
33
|
+
|
34
|
+
filtered_params.each do |shared_param|
|
35
|
+
@params << shared_param
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def response(status_code, description, &block)
|
40
|
+
@responses[status_code] = Param.new(:root, :object, desc: description, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def example(name, value, default: false)
|
44
|
+
@examples ||= {}
|
45
|
+
@examples[name] = { summary: "#{name} Example", value: value }
|
46
|
+
@default_example = value if default
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class << self
|
51
|
+
def webhook_definitions
|
52
|
+
@webhook_definitions ||= []
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset_webhook_definitions
|
56
|
+
@webhook_definitions = []
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -1,10 +1,11 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
1
3
|
namespace :api_docs do
|
2
4
|
desc 'Generate OpenAPI schema'
|
3
5
|
task generate: :environment do
|
4
|
-
Rails.application.eager_load!
|
6
|
+
Rails.application.eager_load! # Ensure all controllers and API definitions are loaded
|
5
7
|
|
6
|
-
|
7
|
-
ApiRegulator::OpenApiGenerator.generate(api_definitions)
|
8
|
+
ApiRegulator::OpenApiGenerator.generate(ApiRegulator.api_definitions)
|
8
9
|
end
|
9
10
|
|
10
11
|
desc "Upload OpenAPI schema to ReadMe"
|
@@ -16,10 +17,11 @@ namespace :api_docs do
|
|
16
17
|
readme_api_endpoint = "https://dash.readme.com/api/v1/api-specification"
|
17
18
|
|
18
19
|
# Read the OpenAPI schema file
|
19
|
-
|
20
|
-
|
20
|
+
schema_path = "#{ApiRegulator.configuration.docs_path}/openapi.json"
|
21
|
+
unless File.exist?(schema_path)
|
22
|
+
raise "OpenAPI schema file not found at #{schema_path}"
|
21
23
|
end
|
22
|
-
openapi_content = File.read(
|
24
|
+
openapi_content = File.read(schema_path)
|
23
25
|
|
24
26
|
# Upload to ReadMe
|
25
27
|
require 'net/http'
|
@@ -63,5 +65,134 @@ namespace :api_docs do
|
|
63
65
|
task publish: :environment do
|
64
66
|
Rake::Task["api_docs:generate"].invoke
|
65
67
|
Rake::Task["api_docs:upload"].invoke
|
68
|
+
Rake::Task["api_docs:upload_pages"].invoke
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "Upload custom pages to ReadMe"
|
72
|
+
task :upload_pages => :environment do
|
73
|
+
# Configuration
|
74
|
+
readme_api_key = ENV['RDME_API_KEY'] || raise("RDME_API_KEY is not set")
|
75
|
+
base_readme_api_endpoint = "https://dash.readme.com/api/v1/docs"
|
76
|
+
|
77
|
+
# Discover all documentation files
|
78
|
+
pages_directory = "#{ApiRegulator.configuration.docs_path}/**/*.md"
|
79
|
+
page_files = Dir.glob(pages_directory)
|
80
|
+
|
81
|
+
# Iterate through each file
|
82
|
+
page_files.each do |file_path|
|
83
|
+
# Extract metadata and body
|
84
|
+
metadata, body = parse_markdown_file(file_path)
|
85
|
+
raise "No metadata found in #{file_path}" unless metadata
|
86
|
+
|
87
|
+
# Use metadata to build the API request
|
88
|
+
slug = metadata["slug"] || File.basename(file_path, ".md").gsub("_", "-")
|
89
|
+
body = {
|
90
|
+
type: "basic",
|
91
|
+
categorySlug: "documentation",
|
92
|
+
hidden: false
|
93
|
+
}.merge(metadata)
|
94
|
+
body["slug"] ||= slug
|
95
|
+
|
96
|
+
raise("Title missing in #{file_path}") unless body["title"].present?
|
97
|
+
|
98
|
+
# Build the API request
|
99
|
+
if check_if_page_exists(slug)
|
100
|
+
uri = URI.parse("#{base_readme_api_endpoint}/#{slug}")
|
101
|
+
request = Net::HTTP::Put.new(uri)
|
102
|
+
else
|
103
|
+
uri = URI.parse("#{base_readme_api_endpoint}")
|
104
|
+
request = Net::HTTP::Post.new(uri)
|
105
|
+
end
|
106
|
+
request["Authorization"] = "Basic #{Base64.strict_encode64(readme_api_key)}"
|
107
|
+
request["Content-Type"] = "application/json"
|
108
|
+
request.body = body.compact.to_json
|
109
|
+
|
110
|
+
# Send the request
|
111
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
112
|
+
http.request(request)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Handle the response
|
116
|
+
case response.code.to_i
|
117
|
+
when 200
|
118
|
+
puts "Page '#{body["title"]}' successfully updated!"
|
119
|
+
when 201
|
120
|
+
puts "Page '#{body["title"]}' successfully created!"
|
121
|
+
else
|
122
|
+
puts "Failed to upload page '#{body["title"]}'!"
|
123
|
+
puts "Response Code: #{response.code}"
|
124
|
+
puts "Response Body: #{response.body}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
desc "View all categories on Readme.com"
|
130
|
+
task :fetch_categories => :environment do
|
131
|
+
# Configuration
|
132
|
+
readme_api_key = ENV['RDME_API_KEY'] || raise("RDME_API_KEY is not set")
|
133
|
+
readme_categories_api_endpoint = "https://dash.readme.com/api/v1/categories"
|
134
|
+
|
135
|
+
# Build the API request
|
136
|
+
uri = URI.parse(readme_categories_api_endpoint)
|
137
|
+
request = Net::HTTP::Get.new(uri)
|
138
|
+
request["Authorization"] = "Basic #{Base64.strict_encode64(readme_api_key)}"
|
139
|
+
request["Content-Type"] = "application/json"
|
140
|
+
|
141
|
+
# Send the request
|
142
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
143
|
+
http.request(request)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Handle the response
|
147
|
+
case response.code.to_i
|
148
|
+
when 200
|
149
|
+
puts "Categories"
|
150
|
+
pp JSON.parse(response.body)
|
151
|
+
else
|
152
|
+
puts "Failed to fetch categories"
|
153
|
+
puts "Response Code: #{response.code}"
|
154
|
+
puts "Response Body: #{response.body}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def parse_markdown_file(file_path)
|
159
|
+
content = File.read(file_path)
|
160
|
+
metadata = nil
|
161
|
+
body = ""
|
162
|
+
|
163
|
+
# Split metadata and body
|
164
|
+
if content =~ /\A---\s*\n(.*?)\n---\s*\n(.*)/m
|
165
|
+
metadata = YAML.safe_load($1)
|
166
|
+
body = $2
|
167
|
+
else
|
168
|
+
raise "YAML front matter missing in #{file_path}"
|
169
|
+
end
|
170
|
+
|
171
|
+
[metadata, body]
|
172
|
+
end
|
173
|
+
|
174
|
+
def check_if_page_exists(slug)
|
175
|
+
readme_api_key = ENV['RDME_API_KEY'] || raise("RDME_API_KEY is not set")
|
176
|
+
uri = URI.parse("https://dash.readme.com/api/v1/docs/#{slug}")
|
177
|
+
request = Net::HTTP::Get.new(uri)
|
178
|
+
request["Authorization"] = "Basic #{Base64.strict_encode64(readme_api_key)}"
|
179
|
+
request["Content-Type"] = "application/json"
|
180
|
+
|
181
|
+
# Send the request
|
182
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
183
|
+
http.request(request)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Handle the response
|
187
|
+
case response.code.to_i
|
188
|
+
when 200
|
189
|
+
true
|
190
|
+
when 404
|
191
|
+
false
|
192
|
+
else
|
193
|
+
puts "Failed to serach for page"
|
194
|
+
puts "Response Code: #{response.code}"
|
195
|
+
puts "Response Body: #{response.body}"
|
196
|
+
end
|
66
197
|
end
|
67
198
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api-regulator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Geoff Massanek
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-12-
|
11
|
+
date: 2024-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -130,6 +130,7 @@ executables: []
|
|
130
130
|
extensions: []
|
131
131
|
extra_rdoc_files: []
|
132
132
|
files:
|
133
|
+
- ".github/workflows/spec.yml"
|
133
134
|
- ".gitignore"
|
134
135
|
- ".rspec"
|
135
136
|
- ".ruby-version"
|
@@ -151,10 +152,12 @@ files:
|
|
151
152
|
- lib/api_regulator/formats.rb
|
152
153
|
- lib/api_regulator/open_api_generator.rb
|
153
154
|
- lib/api_regulator/param.rb
|
155
|
+
- lib/api_regulator/security.rb
|
154
156
|
- lib/api_regulator/shared_schema.rb
|
155
157
|
- lib/api_regulator/validation_error.rb
|
156
158
|
- lib/api_regulator/validator.rb
|
157
159
|
- lib/api_regulator/version.rb
|
160
|
+
- lib/api_regulator/webhook.rb
|
158
161
|
- lib/tasks/api_regulator_tasks.rake
|
159
162
|
homepage: https://github.com/Stellarcred/stellar-gears
|
160
163
|
licenses:
|