jbuilder-schema 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.standard.yml +5 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +200 -0
- data/LICENSE.txt +21 -0
- data/README.md +234 -0
- data/Rakefile +14 -0
- data/lib/jbuilder/jbuilder.rb +23 -0
- data/lib/jbuilder/schema/builder.rb +79 -0
- data/lib/jbuilder/schema/configuration.rb +29 -0
- data/lib/jbuilder/schema/renderer.rb +57 -0
- data/lib/jbuilder/schema/resolver.rb +39 -0
- data/lib/jbuilder/schema/template.rb +360 -0
- data/lib/jbuilder/schema/version.rb +5 -0
- data/lib/jbuilder/schema.rb +12 -0
- data/sig/jbuilder/schema.rbs +4 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1a468c68f0ab10f9d02f4b38e5b42ecfaa7a9570d8cc95e4f1a586bcdcd4b8d8
|
4
|
+
data.tar.gz: 9cfbdbc84f0411af2945277c534ad86d3167bc276da4055630a75b9aab56f668
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5a45615f7ff8b2733fb20a21057ac66b46c8efcb40149dbf54e61d5aafc9d343e8977d3569806e1f5e5d17207db7378941c2b1f8199598faa8fe212886057320
|
7
|
+
data.tar.gz: 516e22d82465a265605237ed8e777ebdfa4a39e6bc3edf49ad2f7bc83d26ad5129a55edc9f4c616c40f49c33ded8cbc0134a192fcb996fd0b1462692106321ab
|
data/.standard.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in jbuilder-schema.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem "rake", "~> 13.0"
|
9
|
+
|
10
|
+
gem "factory_bot"
|
11
|
+
gem "faker"
|
12
|
+
|
13
|
+
gem "standard", "~> 1.3"
|
14
|
+
|
15
|
+
gem "mocha"
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
jbuilder-schema (1.0.0)
|
5
|
+
jbuilder
|
6
|
+
rails (>= 5.0.0)
|
7
|
+
safe_parser
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
actioncable (7.0.3.1)
|
13
|
+
actionpack (= 7.0.3.1)
|
14
|
+
activesupport (= 7.0.3.1)
|
15
|
+
nio4r (~> 2.0)
|
16
|
+
websocket-driver (>= 0.6.1)
|
17
|
+
actionmailbox (7.0.3.1)
|
18
|
+
actionpack (= 7.0.3.1)
|
19
|
+
activejob (= 7.0.3.1)
|
20
|
+
activerecord (= 7.0.3.1)
|
21
|
+
activestorage (= 7.0.3.1)
|
22
|
+
activesupport (= 7.0.3.1)
|
23
|
+
mail (>= 2.7.1)
|
24
|
+
net-imap
|
25
|
+
net-pop
|
26
|
+
net-smtp
|
27
|
+
actionmailer (7.0.3.1)
|
28
|
+
actionpack (= 7.0.3.1)
|
29
|
+
actionview (= 7.0.3.1)
|
30
|
+
activejob (= 7.0.3.1)
|
31
|
+
activesupport (= 7.0.3.1)
|
32
|
+
mail (~> 2.5, >= 2.5.4)
|
33
|
+
net-imap
|
34
|
+
net-pop
|
35
|
+
net-smtp
|
36
|
+
rails-dom-testing (~> 2.0)
|
37
|
+
actionpack (7.0.3.1)
|
38
|
+
actionview (= 7.0.3.1)
|
39
|
+
activesupport (= 7.0.3.1)
|
40
|
+
rack (~> 2.0, >= 2.2.0)
|
41
|
+
rack-test (>= 0.6.3)
|
42
|
+
rails-dom-testing (~> 2.0)
|
43
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
44
|
+
actiontext (7.0.3.1)
|
45
|
+
actionpack (= 7.0.3.1)
|
46
|
+
activerecord (= 7.0.3.1)
|
47
|
+
activestorage (= 7.0.3.1)
|
48
|
+
activesupport (= 7.0.3.1)
|
49
|
+
globalid (>= 0.6.0)
|
50
|
+
nokogiri (>= 1.8.5)
|
51
|
+
actionview (7.0.3.1)
|
52
|
+
activesupport (= 7.0.3.1)
|
53
|
+
builder (~> 3.1)
|
54
|
+
erubi (~> 1.4)
|
55
|
+
rails-dom-testing (~> 2.0)
|
56
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
57
|
+
activejob (7.0.3.1)
|
58
|
+
activesupport (= 7.0.3.1)
|
59
|
+
globalid (>= 0.3.6)
|
60
|
+
activemodel (7.0.3.1)
|
61
|
+
activesupport (= 7.0.3.1)
|
62
|
+
activerecord (7.0.3.1)
|
63
|
+
activemodel (= 7.0.3.1)
|
64
|
+
activesupport (= 7.0.3.1)
|
65
|
+
activestorage (7.0.3.1)
|
66
|
+
actionpack (= 7.0.3.1)
|
67
|
+
activejob (= 7.0.3.1)
|
68
|
+
activerecord (= 7.0.3.1)
|
69
|
+
activesupport (= 7.0.3.1)
|
70
|
+
marcel (~> 1.0)
|
71
|
+
mini_mime (>= 1.1.0)
|
72
|
+
activesupport (7.0.3.1)
|
73
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
74
|
+
i18n (>= 1.6, < 2)
|
75
|
+
minitest (>= 5.1)
|
76
|
+
tzinfo (~> 2.0)
|
77
|
+
ast (2.4.2)
|
78
|
+
builder (3.2.4)
|
79
|
+
concurrent-ruby (1.1.10)
|
80
|
+
crass (1.0.6)
|
81
|
+
erubi (1.11.0)
|
82
|
+
factory_bot (6.2.1)
|
83
|
+
activesupport (>= 5.0.0)
|
84
|
+
faker (2.22.0)
|
85
|
+
i18n (>= 1.8.11, < 2)
|
86
|
+
globalid (1.0.0)
|
87
|
+
activesupport (>= 5.0)
|
88
|
+
i18n (1.12.0)
|
89
|
+
concurrent-ruby (~> 1.0)
|
90
|
+
jbuilder (2.11.5)
|
91
|
+
actionview (>= 5.0.0)
|
92
|
+
activesupport (>= 5.0.0)
|
93
|
+
json (2.6.2)
|
94
|
+
loofah (2.19.0)
|
95
|
+
crass (~> 1.0.2)
|
96
|
+
nokogiri (>= 1.5.9)
|
97
|
+
mail (2.7.1)
|
98
|
+
mini_mime (>= 0.1.1)
|
99
|
+
marcel (1.0.2)
|
100
|
+
method_source (1.0.0)
|
101
|
+
mini_mime (1.1.2)
|
102
|
+
mini_portile2 (2.8.0)
|
103
|
+
minitest (5.16.2)
|
104
|
+
mocha (1.14.0)
|
105
|
+
net-imap (0.3.1)
|
106
|
+
net-protocol
|
107
|
+
net-pop (0.1.2)
|
108
|
+
net-protocol
|
109
|
+
net-protocol (0.1.3)
|
110
|
+
timeout
|
111
|
+
net-smtp (0.3.2)
|
112
|
+
net-protocol
|
113
|
+
nio4r (2.5.8)
|
114
|
+
nokogiri (1.13.8)
|
115
|
+
mini_portile2 (~> 2.8.0)
|
116
|
+
racc (~> 1.4)
|
117
|
+
parallel (1.22.1)
|
118
|
+
parser (3.1.2.0)
|
119
|
+
ast (~> 2.4.1)
|
120
|
+
racc (1.6.0)
|
121
|
+
rack (2.2.4)
|
122
|
+
rack-test (2.0.2)
|
123
|
+
rack (>= 1.3)
|
124
|
+
rails (7.0.3.1)
|
125
|
+
actioncable (= 7.0.3.1)
|
126
|
+
actionmailbox (= 7.0.3.1)
|
127
|
+
actionmailer (= 7.0.3.1)
|
128
|
+
actionpack (= 7.0.3.1)
|
129
|
+
actiontext (= 7.0.3.1)
|
130
|
+
actionview (= 7.0.3.1)
|
131
|
+
activejob (= 7.0.3.1)
|
132
|
+
activemodel (= 7.0.3.1)
|
133
|
+
activerecord (= 7.0.3.1)
|
134
|
+
activestorage (= 7.0.3.1)
|
135
|
+
activesupport (= 7.0.3.1)
|
136
|
+
bundler (>= 1.15.0)
|
137
|
+
railties (= 7.0.3.1)
|
138
|
+
rails-dom-testing (2.0.3)
|
139
|
+
activesupport (>= 4.2.0)
|
140
|
+
nokogiri (>= 1.6)
|
141
|
+
rails-html-sanitizer (1.4.3)
|
142
|
+
loofah (~> 2.3)
|
143
|
+
railties (7.0.3.1)
|
144
|
+
actionpack (= 7.0.3.1)
|
145
|
+
activesupport (= 7.0.3.1)
|
146
|
+
method_source
|
147
|
+
rake (>= 12.2)
|
148
|
+
thor (~> 1.0)
|
149
|
+
zeitwerk (~> 2.5)
|
150
|
+
rainbow (3.1.1)
|
151
|
+
rake (13.0.6)
|
152
|
+
regexp_parser (2.5.0)
|
153
|
+
rexml (3.2.5)
|
154
|
+
rubocop (1.32.0)
|
155
|
+
json (~> 2.3)
|
156
|
+
parallel (~> 1.10)
|
157
|
+
parser (>= 3.1.0.0)
|
158
|
+
rainbow (>= 2.2.2, < 4.0)
|
159
|
+
regexp_parser (>= 1.8, < 3.0)
|
160
|
+
rexml (>= 3.2.5, < 4.0)
|
161
|
+
rubocop-ast (>= 1.19.1, < 2.0)
|
162
|
+
ruby-progressbar (~> 1.7)
|
163
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
164
|
+
rubocop-ast (1.19.1)
|
165
|
+
parser (>= 3.1.1.0)
|
166
|
+
rubocop-performance (1.14.3)
|
167
|
+
rubocop (>= 1.7.0, < 2.0)
|
168
|
+
rubocop-ast (>= 0.4.0)
|
169
|
+
ruby-progressbar (1.11.0)
|
170
|
+
ruby_parser (3.8.4)
|
171
|
+
sexp_processor (~> 4.1)
|
172
|
+
safe_parser (1.0.0)
|
173
|
+
ruby_parser (~> 3.8.4)
|
174
|
+
sexp_processor (4.16.1)
|
175
|
+
standard (1.14.0)
|
176
|
+
rubocop (= 1.32.0)
|
177
|
+
rubocop-performance (= 1.14.3)
|
178
|
+
thor (1.2.1)
|
179
|
+
timeout (0.3.0)
|
180
|
+
tzinfo (2.0.5)
|
181
|
+
concurrent-ruby (~> 1.0)
|
182
|
+
unicode-display_width (2.2.0)
|
183
|
+
websocket-driver (0.7.5)
|
184
|
+
websocket-extensions (>= 0.1.0)
|
185
|
+
websocket-extensions (0.1.5)
|
186
|
+
zeitwerk (2.6.1)
|
187
|
+
|
188
|
+
PLATFORMS
|
189
|
+
ruby
|
190
|
+
|
191
|
+
DEPENDENCIES
|
192
|
+
factory_bot
|
193
|
+
faker
|
194
|
+
jbuilder-schema!
|
195
|
+
mocha
|
196
|
+
rake (~> 13.0)
|
197
|
+
standard (~> 1.3)
|
198
|
+
|
199
|
+
BUNDLED WITH
|
200
|
+
2.3.16
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 Yuri Sidorov
|
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,234 @@
|
|
1
|
+
# JbuilderSchema
|
2
|
+
|
3
|
+
Generate JSON Schema compatible with OpenAPI 3 specs from Jbuilder files
|
4
|
+
|
5
|
+
[![Tests](https://github.com/bullet-train-co/jbuilder-schema/actions/workflows/tests.yml/badge.svg)](https://github.com/bullet-train-co/jbuilder-schema/actions)
|
6
|
+
[![Standard](https://github.com/bullet-train-co/jbuilder-schema/actions/workflows/standard.yml/badge.svg)](https://github.com/bullet-train-co/jbuilder-schema/actions)
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
In Gemfile put `gem "jbuilder-schema"` **before** `gem "jbuilder"`:
|
11
|
+
|
12
|
+
gem "jbuilder-schema", require: "jbuilder/schema"
|
13
|
+
gem "jbuilder"
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install jbuilder-schema
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Wherever you want to generate schemas, you should extend `JbuilderSchema`:
|
26
|
+
|
27
|
+
extend JbuilderSchema
|
28
|
+
|
29
|
+
Then you can use `jbuilder_schema` helper:
|
30
|
+
|
31
|
+
jbuilder_schema('api/v1/articles/_article',
|
32
|
+
title: 'Article',
|
33
|
+
description: 'Article in the blog',
|
34
|
+
format: :yaml,
|
35
|
+
paths: view_paths.map(&:path),
|
36
|
+
model: Article,
|
37
|
+
locals: {
|
38
|
+
article: @article,
|
39
|
+
current_user: @user
|
40
|
+
})
|
41
|
+
|
42
|
+
`jbuilder_schema` helper takes `path` to Jbuilder template as a first argument and several optional arguments:
|
43
|
+
|
44
|
+
- `title` and `description`: Title and description of schema, if not passed then they will be grabbed from locale files (see *[Titles & Descriptions](#titles--descriptions)*);
|
45
|
+
- `format`: Desired output format, can be either `:yaml` or `:json`. If no `format` option is passed, the output will be the Ruby Hash object;
|
46
|
+
- `paths`: If you need to scope any other paths than `app/views`, pass them as an array here;
|
47
|
+
- `model`: Model described in template, this is needed to populate `required` field in schema;
|
48
|
+
- `locals`: Here you should pass all the locals which are met in the jbuilder template. Those could be any objects as long as they respond to methods called on them in template.
|
49
|
+
|
50
|
+
Notice that partial templates should be prepended with an underscore just like in the name of the file (i.e. `_article` but not `article` an when using Jbuilder).
|
51
|
+
|
52
|
+
### Output
|
53
|
+
|
54
|
+
JbuilderSchema automatically sets `description`, `type`, and `required` options in JSON-Schema.
|
55
|
+
|
56
|
+
For example, if we have `_articles.json.jbilder` file:
|
57
|
+
|
58
|
+
json.extract! article, :id, :title, :body, :created_at
|
59
|
+
|
60
|
+
The output for it will be:
|
61
|
+
|
62
|
+
type: object
|
63
|
+
title: Article
|
64
|
+
description: Article in the blog
|
65
|
+
required:
|
66
|
+
- id
|
67
|
+
- title
|
68
|
+
- body
|
69
|
+
properties:
|
70
|
+
id:
|
71
|
+
description: ID of an article
|
72
|
+
type: integer
|
73
|
+
title:
|
74
|
+
description: Title of an article
|
75
|
+
type: string
|
76
|
+
body:
|
77
|
+
description: Contents of an article
|
78
|
+
type: string
|
79
|
+
created_at:
|
80
|
+
description: Timestamp when article was created
|
81
|
+
type: string
|
82
|
+
format: date-time
|
83
|
+
|
84
|
+
### Customization
|
85
|
+
|
86
|
+
#### Simple
|
87
|
+
|
88
|
+
Sometimes you would want to set you own data in generated JSON-Schema. All you need to do is just pass hash with it under `schema` keyword in your jbuilder template:
|
89
|
+
|
90
|
+
json.id article.id, schema: { type: :number, description: "Custom ID description" }
|
91
|
+
json.title article.title, schema: { minLength: 5, maxLength: 20 }
|
92
|
+
json.body article.body, schema: { type: :text, maxLength: 500 }
|
93
|
+
json.created_at article.created_at.strftime('%d/%m/%Y'), schema: { format: :date, pattern: /^(3[01]|[12][0-9]|0[1-9])\/(1[0-2]|0[1-9])\/[0-9]{4}$/ }
|
94
|
+
|
95
|
+
This will produce the following:
|
96
|
+
|
97
|
+
...
|
98
|
+
properties:
|
99
|
+
id:
|
100
|
+
description: Custom ID description
|
101
|
+
type: number
|
102
|
+
title:
|
103
|
+
description: Title of an article
|
104
|
+
type: string
|
105
|
+
minLength: 5
|
106
|
+
maxLength: 20
|
107
|
+
body:
|
108
|
+
description: Contents of an article
|
109
|
+
type: string
|
110
|
+
maxLength: 500
|
111
|
+
created_at:
|
112
|
+
description: Timestamp when article was created
|
113
|
+
type: string
|
114
|
+
format: date
|
115
|
+
pattern: "^(3[01]|[12][0-9]|0[1-9])\/(1[0-2]|0[1-9])\/[0-9]{4}$"
|
116
|
+
|
117
|
+
#### Bulk
|
118
|
+
|
119
|
+
You can customize output for multiple fields at once:
|
120
|
+
|
121
|
+
json.extract! user, :id, :name, :email, schema: {id: {type: :string}, email: {type: :email, pattern: /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/}}
|
122
|
+
|
123
|
+
### Nested objects
|
124
|
+
|
125
|
+
When you have nested objects in jbuilder template, you have to pass it to `schema: {object: <nested_object>}` when the block starts:
|
126
|
+
|
127
|
+
json.extract! article
|
128
|
+
json.author schema: {object: article.user, object_title: "Author", object_description: "Authors are users who write articles"} do
|
129
|
+
json.extract! article.user
|
130
|
+
end
|
131
|
+
|
132
|
+
This will help JbuilderSchema to process those fields right.
|
133
|
+
|
134
|
+
### Collections
|
135
|
+
|
136
|
+
If an object or an array of objects is generated in template, either in root or in some field through Jbuilder partials, JSON-Schema `$ref` is generated pointing to object with the same name as partial. By default those schemas should appear in `"#/components/schemas/"`.
|
137
|
+
|
138
|
+
For example, if we have:
|
139
|
+
|
140
|
+
json.user do
|
141
|
+
json.partial! 'api/v1/users/user', user: user
|
142
|
+
end
|
143
|
+
|
144
|
+
json.articles do
|
145
|
+
json.array! user.articles, partial: "api/v1/articles/article", as: :article
|
146
|
+
end
|
147
|
+
|
148
|
+
The result would be:
|
149
|
+
|
150
|
+
user:
|
151
|
+
type: object
|
152
|
+
$ref: #/components/schemas/user
|
153
|
+
articles:
|
154
|
+
type: array
|
155
|
+
items:
|
156
|
+
$ref: #/components/schemas/article
|
157
|
+
|
158
|
+
The path to component schemas can be configured with `components_path` variable, which defaults to `components/schemas`. See *[Configuration](#configuration)* for more info.
|
159
|
+
|
160
|
+
### Titles & Descriptions
|
161
|
+
|
162
|
+
Custom titles and descriptions for objects can be specified when calling `jbuilder-schema` helper (see *[Usage](#usage)*), for fields and nested objects within `schema` attributes (see *[Customization](#simple)* and *[Nested objects](#nested-objects)*). If not set, they will be searched in locale files.
|
163
|
+
|
164
|
+
Titles and descriptions for the models are supposed to be found in locale files under `<underscored_plural_model_name>.<title_name>` and `<underscored_plural_model_name>.<description_name>`, for example:
|
165
|
+
|
166
|
+
en:
|
167
|
+
articles:
|
168
|
+
title: Article
|
169
|
+
description: The main object on the blog
|
170
|
+
|
171
|
+
Descriptions for the fields are supposed to be found in locale files under `<underscored_plural_model_name>.fields.<field_name>.<description_name>`, for example:
|
172
|
+
|
173
|
+
en:
|
174
|
+
articles:
|
175
|
+
fields:
|
176
|
+
title:
|
177
|
+
description: The title of an article
|
178
|
+
|
179
|
+
`<title_name>` and `<description_name>` can be configured (see *[Configuration](#configuration)*), it defaults to `title` and `description`.
|
180
|
+
|
181
|
+
### Configuration
|
182
|
+
|
183
|
+
You can configure some variables that JbuilderSchema uses (for example, in `config/initializers/jbuilder_schema.rb`):
|
184
|
+
|
185
|
+
JbuilderSchema.configure do |config|
|
186
|
+
config.components_path = "components/schemas" # could be "definitions/schemas"
|
187
|
+
config.title_name = "title" # could be "label"
|
188
|
+
config.description_name = "description" # could be "heading"
|
189
|
+
end
|
190
|
+
|
191
|
+
### RSwag
|
192
|
+
|
193
|
+
It's super easy to use JbuilderSchema with RSwag: just add `jbuilder_schema` helper in `swagger_helper.rb` like this:
|
194
|
+
|
195
|
+
RSpec.configure do |config|
|
196
|
+
extend JbuilderSchema
|
197
|
+
|
198
|
+
...
|
199
|
+
|
200
|
+
config.swagger_docs = {
|
201
|
+
|
202
|
+
...
|
203
|
+
|
204
|
+
components: {
|
205
|
+
schemas: {
|
206
|
+
article: jbuilder_schema('api/v1/articles/_article',
|
207
|
+
format: :yaml,
|
208
|
+
model: Article,
|
209
|
+
title: 'Article',
|
210
|
+
description: 'Article in the blog',
|
211
|
+
locals: {
|
212
|
+
article: FactoryBot.build(:article, id: 1),
|
213
|
+
current_user: FactoryBot.build(:user, admin: true)
|
214
|
+
})
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
...
|
219
|
+
|
220
|
+
}
|
221
|
+
|
222
|
+
...
|
223
|
+
|
224
|
+
## Contributing
|
225
|
+
|
226
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/bullet-train-co/jbuilder-schema.
|
227
|
+
|
228
|
+
## License
|
229
|
+
|
230
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
231
|
+
|
232
|
+
## Open-source development sponsored by:
|
233
|
+
|
234
|
+
<a href="https://www.clickfunnels.com"><img src="https://images.clickfunnel.com/uploads/digital_asset/file/176632/clickfunnels-dark-logo.svg" width="575" /></a>
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
require "standard/rake"
|
6
|
+
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs << "test"
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
10
|
+
t.verbose = true
|
11
|
+
t.warning = false
|
12
|
+
end
|
13
|
+
|
14
|
+
task default: %i[test standard]
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jbuilder"
|
4
|
+
|
5
|
+
# Patches for Jbuilder to make it ignore schema metadata
|
6
|
+
class Jbuilder
|
7
|
+
alias_method :original_method_missing, :method_missing
|
8
|
+
|
9
|
+
def method_missing(*args, &block)
|
10
|
+
args = _extract_schema_meta!(*args)
|
11
|
+
original_method_missing(*args, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def respond_to_missing?(method_name, include_private = false)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def _extract_schema_meta!(*args)
|
21
|
+
args.delete_if { |a| a.is_a?(::Hash) && a.key?(:schema) }
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jbuilder/schema/resolver"
|
4
|
+
require "jbuilder/schema/renderer"
|
5
|
+
|
6
|
+
module JbuilderSchema
|
7
|
+
# Class that builds schema object from path
|
8
|
+
class Builder
|
9
|
+
attr_reader :path, :template, :model, :locals, :format, :paths
|
10
|
+
|
11
|
+
def initialize(path, **options)
|
12
|
+
@path = path
|
13
|
+
# TODO: Need this for `required`, make it simpler:
|
14
|
+
@model = options[:model]
|
15
|
+
@locals = options[:locals] || {}
|
16
|
+
@format = options[:format]
|
17
|
+
@paths = options[:paths] || ["app/views"]
|
18
|
+
@template = _render_template(**options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def schema!
|
22
|
+
return {} unless template
|
23
|
+
|
24
|
+
case format
|
25
|
+
when :yaml
|
26
|
+
_yaml_schema
|
27
|
+
when :json
|
28
|
+
_json_schema
|
29
|
+
else
|
30
|
+
_schema
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def _schema
|
37
|
+
template.schema!
|
38
|
+
end
|
39
|
+
|
40
|
+
def _stringified_schema
|
41
|
+
_schema.deep_stringify_keys
|
42
|
+
.deep_transform_values { |v| v.is_a?(Symbol) ? v.to_s : v }
|
43
|
+
.deep_transform_values { |v| v.is_a?(Regexp) ? v.source : v }
|
44
|
+
end
|
45
|
+
|
46
|
+
def _yaml_schema
|
47
|
+
YAML.dump(_stringified_schema).html_safe
|
48
|
+
end
|
49
|
+
|
50
|
+
def _json_schema
|
51
|
+
JSON.dump(_stringified_schema).html_safe
|
52
|
+
end
|
53
|
+
|
54
|
+
def _find_template
|
55
|
+
prefix, controller, action, partial = _resolve_path
|
56
|
+
found = nil
|
57
|
+
paths.each do |path|
|
58
|
+
found = Resolver.new("#{path}/#{prefix}").find_all(action, controller, partial)
|
59
|
+
break if found
|
60
|
+
end
|
61
|
+
found
|
62
|
+
end
|
63
|
+
|
64
|
+
def _resolve_path
|
65
|
+
action = path.split("/").last
|
66
|
+
controller = path.split("/")[-2]
|
67
|
+
prefix = path.delete_suffix("/#{controller}/#{action}")
|
68
|
+
partial = action[0] == "_"
|
69
|
+
|
70
|
+
action.delete_prefix!("_") if action[0] == "_"
|
71
|
+
|
72
|
+
[prefix, controller, action, partial]
|
73
|
+
end
|
74
|
+
|
75
|
+
def _render_template(**options)
|
76
|
+
Renderer.new(**options).render(_find_template)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Configuration
|
4
|
+
module JbuilderSchema
|
5
|
+
class << self
|
6
|
+
def configuration
|
7
|
+
@configuration ||= Configuration.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def reset
|
11
|
+
@configuration = Configuration.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def configure
|
15
|
+
yield(configuration)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Configuration class with defaults
|
20
|
+
class Configuration
|
21
|
+
attr_accessor :components_path, :title_name, :description_name
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@components_path = "components/schemas"
|
25
|
+
@title_name = "title"
|
26
|
+
@description_name = "description"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jbuilder/schema/template"
|
4
|
+
# TODO: Find a better way to load main app's helpers:
|
5
|
+
# Helpers don't work in Jbuilder itself, so no need to include them here!
|
6
|
+
# ActionController::Base.all_helpers_from_path('app/helpers').each { |helper| require "./app/helpers/#{helper}_helper" }
|
7
|
+
|
8
|
+
module JbuilderSchema
|
9
|
+
# Here we initialize all the variables needed for template and pass them to it
|
10
|
+
class Renderer
|
11
|
+
# TODO: Find a better way to load main app's helpers:
|
12
|
+
# Helpers don't work in Jbuilder itself, so no need to include them here!
|
13
|
+
# ActionController::Base.all_helpers_from_path('app/helpers').each { |helper| include Object.const_get("::#{helper.camelize}Helper") }
|
14
|
+
|
15
|
+
attr_reader :locals, :options
|
16
|
+
|
17
|
+
def initialize(**options)
|
18
|
+
@locals = options.delete(:locals) || {}
|
19
|
+
@options = options
|
20
|
+
_define_locals!
|
21
|
+
end
|
22
|
+
|
23
|
+
def render(source)
|
24
|
+
Template.new(**options) do |json|
|
25
|
+
# TODO: Get rid of 'eval'
|
26
|
+
eval source.to_s # standard:disable Security/Eval
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(method, *args)
|
31
|
+
if method.to_s.end_with?("_path", "_url")
|
32
|
+
method.to_s
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def respond_to_missing?(method_name, include_private = false)
|
39
|
+
method_name.to_s.end_with?("_path", "_url") || super
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def _define_locals!
|
45
|
+
locals.each do |k, v|
|
46
|
+
# Setting instance variables (`@article`):
|
47
|
+
instance_variable_set("@#{k}", v)
|
48
|
+
|
49
|
+
# Setting local variables (`article`):
|
50
|
+
# We can define method:
|
51
|
+
# define_singleton_method(k) { v }
|
52
|
+
# or set attr_reader on an instance, this feels better:
|
53
|
+
singleton_class.instance_eval { attr_reader k }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jbuilder/schema/template"
|
4
|
+
|
5
|
+
module JbuilderSchema
|
6
|
+
# Resolver finds and returns Jbuilder template.
|
7
|
+
# It basically inherits from ActionView::FileSystemResolver as it does all the job for us.
|
8
|
+
# We're just building our own template in the end of the search.
|
9
|
+
class Resolver < ::ActionView::FileSystemResolver
|
10
|
+
attr_reader :template
|
11
|
+
|
12
|
+
def find_all(name, prefix = nil, partial = false)
|
13
|
+
_find_all(name, prefix, partial)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def _find_all(name, prefix, partial)
|
19
|
+
path = ActionView::TemplatePath.build(name, prefix, partial)
|
20
|
+
templates_from_path(path).first
|
21
|
+
end
|
22
|
+
|
23
|
+
def templates_from_path(path)
|
24
|
+
return [] if path.name.include?(".")
|
25
|
+
|
26
|
+
# Instead of checking for every possible path, as our other globs would
|
27
|
+
# do, scan the directory for files with the right prefix.
|
28
|
+
paths = template_glob("#{escape_entry(path.to_s)}*")
|
29
|
+
|
30
|
+
paths.map do |p|
|
31
|
+
_source(p)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def _source(template)
|
36
|
+
source_for_template(template)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,360 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jbuilder/jbuilder_template"
|
4
|
+
require "active_support/inflections"
|
5
|
+
require "active_support/core_ext/hash/deep_transform_values"
|
6
|
+
require "safe_parser"
|
7
|
+
|
8
|
+
module JbuilderSchema
|
9
|
+
# Template parser class
|
10
|
+
class Template < ::JbuilderTemplate
|
11
|
+
attr_reader :attributes, :type, :models, :titles, :descriptions
|
12
|
+
|
13
|
+
def initialize(*args, **options)
|
14
|
+
@type = :object
|
15
|
+
@inline_array = false
|
16
|
+
@collection = false
|
17
|
+
|
18
|
+
@models = [options.delete(:model)]
|
19
|
+
@titles = [options.delete(:title)]
|
20
|
+
@descriptions = [options.delete(:description)]
|
21
|
+
|
22
|
+
super(nil, *args)
|
23
|
+
|
24
|
+
@ignore_nil = false
|
25
|
+
end
|
26
|
+
|
27
|
+
def schema!
|
28
|
+
{type: type}.merge(type == :object ? _object(**attributes.merge) : attributes)
|
29
|
+
end
|
30
|
+
|
31
|
+
def set!(key, value = BLANK, *args, **schema_options, &block)
|
32
|
+
result = if block
|
33
|
+
if !_blank?(value)
|
34
|
+
# OBJECTS ARRAY:
|
35
|
+
# json.comments @article.comments { |comment| ... }
|
36
|
+
# { "comments": [ { ... }, { ... } ] }
|
37
|
+
_scope { array! value, &block }
|
38
|
+
else
|
39
|
+
# BLOCK:
|
40
|
+
# json.comments { ... }
|
41
|
+
# { "comments": ... }
|
42
|
+
@inline_array = true
|
43
|
+
if schema_options.key?(:object)
|
44
|
+
models << schema_options[:object].class
|
45
|
+
titles << schema_options[:object_title] || nil
|
46
|
+
descriptions << schema_options[:object_description] || nil
|
47
|
+
end
|
48
|
+
r = _merge_block(key) { yield self }
|
49
|
+
if schema_options.key?(:object)
|
50
|
+
models.pop
|
51
|
+
titles.pop
|
52
|
+
descriptions.pop
|
53
|
+
end
|
54
|
+
r
|
55
|
+
end
|
56
|
+
elsif args.empty?
|
57
|
+
if ::Jbuilder === value
|
58
|
+
# ATTRIBUTE1:
|
59
|
+
# json.age 32
|
60
|
+
# json.person another_jbuilder
|
61
|
+
# { "age": 32, "person": { ... }
|
62
|
+
_schema(key, _format_keys(value.attributes!), **schema_options)
|
63
|
+
elsif _is_collection_array?(value)
|
64
|
+
# ATTRIBUTE2:
|
65
|
+
_scope { array! value }
|
66
|
+
# json.articles @articles
|
67
|
+
else
|
68
|
+
# json.age 32
|
69
|
+
# { "age": 32 }
|
70
|
+
_schema(key, _format_keys(value), **schema_options)
|
71
|
+
end
|
72
|
+
elsif _is_collection?(value)
|
73
|
+
# COLLECTION:
|
74
|
+
# json.comments @article.comments, :content, :created_at
|
75
|
+
# { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
|
76
|
+
@inline_array = true
|
77
|
+
@collection = true
|
78
|
+
|
79
|
+
_scope { array! value, *args }
|
80
|
+
elsif schema_options.key?(:object)
|
81
|
+
# EXTRACT!:
|
82
|
+
# json.author @article.creator, :name, :email_address
|
83
|
+
# { "author": { "name": "David", "email_address": "david@loudthinking.com" } }
|
84
|
+
|
85
|
+
models << schema_options.delete(:object).class
|
86
|
+
titles << schema_options.delete(:object_title) || nil
|
87
|
+
descriptions << schema_options.delete(:object_description) || nil
|
88
|
+
r = _merge_block(key) { extract! value, *args, **schema_options }
|
89
|
+
models.pop
|
90
|
+
titles.pop
|
91
|
+
descriptions.pop
|
92
|
+
r
|
93
|
+
else
|
94
|
+
_merge_block(key) { extract! value, *args, **schema_options }
|
95
|
+
end
|
96
|
+
|
97
|
+
result = _set_description key, result if models.any?
|
98
|
+
_set_value key, result
|
99
|
+
end
|
100
|
+
|
101
|
+
def extract!(object, *attributes, **schema_options)
|
102
|
+
schema_options = schema_options[:schema] if schema_options.key?(:schema)
|
103
|
+
|
104
|
+
if ::Hash === object
|
105
|
+
_extract_hash_values(object, attributes, **schema_options)
|
106
|
+
else
|
107
|
+
_extract_method_values(object, attributes, **schema_options)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def array!(collection = [], *args, &block)
|
112
|
+
args, schema_options = _args_and_schema_options(*args)
|
113
|
+
options = args.first
|
114
|
+
|
115
|
+
if args.one? && _partial_options?(options)
|
116
|
+
@collection = true
|
117
|
+
_set_ref(options[:partial].split("/").last)
|
118
|
+
else
|
119
|
+
array = _make_array(collection, *args, **schema_options, &block)
|
120
|
+
|
121
|
+
if @inline_array
|
122
|
+
@attributes = {}
|
123
|
+
_set_value(:type, :array)
|
124
|
+
_set_value(:items, array)
|
125
|
+
elsif _is_collection_array?(array)
|
126
|
+
@attributes = {}
|
127
|
+
@inline_array = true
|
128
|
+
@collection = true
|
129
|
+
array! array, *array.first&.attribute_names(&:to_sym)
|
130
|
+
else
|
131
|
+
@type = :array
|
132
|
+
@attributes = {}
|
133
|
+
_set_value(:items, array)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def partial!(*args)
|
139
|
+
if args.one? && _is_active_model?(args.first)
|
140
|
+
# TODO: Find where it is being used
|
141
|
+
_render_active_model_partial args.first
|
142
|
+
elsif args.first.is_a?(Hash)
|
143
|
+
_set_ref(args.first[:partial].split("/").last)
|
144
|
+
else
|
145
|
+
@collection = true if args[1].key?(:collection)
|
146
|
+
_set_ref(args.first&.split("/")&.last)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def merge!(object)
|
151
|
+
hash_or_array = ::Jbuilder === object ? object.attributes! : object
|
152
|
+
hash_or_array = _format_keys(hash_or_array)
|
153
|
+
if hash_or_array.is_a?(Hash)
|
154
|
+
hash_or_array = hash_or_array.each_with_object({}) do |(key, value), a|
|
155
|
+
result = _schema(key, value)
|
156
|
+
result = _set_description(key, result) if models.any?
|
157
|
+
a[key] = result
|
158
|
+
end
|
159
|
+
end
|
160
|
+
@attributes = _merge_values(@attributes, hash_or_array)
|
161
|
+
end
|
162
|
+
|
163
|
+
def cache!(key = nil, **options)
|
164
|
+
yield
|
165
|
+
end
|
166
|
+
|
167
|
+
def method_missing(*args, &block)
|
168
|
+
args, schema_options = _args_and_schema_options(*args)
|
169
|
+
|
170
|
+
if block
|
171
|
+
set!(*args, **schema_options, &block)
|
172
|
+
else
|
173
|
+
set!(*args, **schema_options)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def respond_to_missing?(method_name, include_private = false)
|
178
|
+
super
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def _object(**attrs)
|
184
|
+
title = titles.last || ::I18n.t("#{models&.last&.name&.underscore&.pluralize}.#{JbuilderSchema.configuration.title_name}")
|
185
|
+
description = descriptions.last || ::I18n.t("#{models&.last&.name&.underscore&.pluralize}.#{JbuilderSchema.configuration.description_name}")
|
186
|
+
{
|
187
|
+
type: :object,
|
188
|
+
title: title,
|
189
|
+
description: description,
|
190
|
+
required: _required!(**attrs),
|
191
|
+
properties: attrs
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
def _args_and_schema_options(*args)
|
196
|
+
schema_options = args.extract! { |a| a.is_a?(::Hash) && a.key?(:schema) }.first.try(:[], :schema) || {}
|
197
|
+
[args, schema_options]
|
198
|
+
end
|
199
|
+
|
200
|
+
def _set_description(key, value)
|
201
|
+
unless value.key?(:description)
|
202
|
+
description = ::I18n.t("#{models.last&.name&.underscore&.pluralize}.fields.#{key}.#{JbuilderSchema.configuration.description_name}")
|
203
|
+
value = {description: description}.merge! value
|
204
|
+
end
|
205
|
+
value
|
206
|
+
end
|
207
|
+
|
208
|
+
def _set_ref(component)
|
209
|
+
if @inline_array
|
210
|
+
if @collection
|
211
|
+
_set_value(:type, :array)
|
212
|
+
_set_value(:items, {:$ref => _component_path(component)})
|
213
|
+
else
|
214
|
+
_set_value(:type, :object)
|
215
|
+
_set_value(:$ref, _component_path(component))
|
216
|
+
end
|
217
|
+
else
|
218
|
+
@type = :array
|
219
|
+
_set_value(:items, {:$ref => _component_path(component)})
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def _component_path(component)
|
224
|
+
"#/#{JbuilderSchema.configuration.components_path}/#{component}"
|
225
|
+
end
|
226
|
+
|
227
|
+
def _schema(key, value, **options)
|
228
|
+
options.merge!(_guess_type(value)) unless options[:type]
|
229
|
+
options.merge!(_set_enum(key.to_s)) if models.last&.defined_enums&.keys&.include?(key.to_s)
|
230
|
+
options
|
231
|
+
end
|
232
|
+
|
233
|
+
def _guess_type(value)
|
234
|
+
type = value.class.name&.downcase&.to_sym
|
235
|
+
|
236
|
+
case type
|
237
|
+
when :datetime, :"activesupport::timewithzone"
|
238
|
+
{type: :string, format: "date-time"}
|
239
|
+
when :time, :date
|
240
|
+
{type: :string, format: type.to_s}
|
241
|
+
when :array
|
242
|
+
_guess_array_types(value)
|
243
|
+
else
|
244
|
+
{type: _type(type)}
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def _guess_array_types(array)
|
249
|
+
hash = {type: :array}
|
250
|
+
types = array.map { |a| _type(a.class.name&.downcase&.to_sym) }.uniq
|
251
|
+
|
252
|
+
unless types.empty?
|
253
|
+
hash[:contains] = {type: types.size > 1 ? types : types.first}
|
254
|
+
hash[:minContains] = 0
|
255
|
+
end
|
256
|
+
|
257
|
+
hash
|
258
|
+
end
|
259
|
+
|
260
|
+
def _type(type)
|
261
|
+
case type
|
262
|
+
when :time, :date, :datetime, :"activesupport::timewithzone", nil, :text, :nilclass, :"actiontext::richtext"
|
263
|
+
:string
|
264
|
+
when :float, :bigdecimal
|
265
|
+
:number
|
266
|
+
when :trueclass, :falseclass
|
267
|
+
:boolean
|
268
|
+
else
|
269
|
+
type
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def _set_enum(key)
|
274
|
+
enums = models.last&.defined_enums[key].keys
|
275
|
+
{enum: enums}
|
276
|
+
end
|
277
|
+
|
278
|
+
def _make_array(collection, *args, **schema_options, &block)
|
279
|
+
if collection.nil?
|
280
|
+
[]
|
281
|
+
elsif block
|
282
|
+
_map_collection(collection, &block)
|
283
|
+
elsif args.any?
|
284
|
+
_map_collection(collection) { |element| extract! element, *args, **schema_options }
|
285
|
+
else
|
286
|
+
_format_keys(collection.to_a)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def _is_collection_array?(object)
|
291
|
+
# TODO: Find better way to determine if all array elements are models
|
292
|
+
object.is_a?(Array) && object.map { |a| _is_active_model?(a) }.uniq == [true]
|
293
|
+
end
|
294
|
+
|
295
|
+
def _required!(**attrs)
|
296
|
+
if Object.const_defined?('ActiveRecord')
|
297
|
+
attrs.keys.select { |attribute|
|
298
|
+
models.last&.validators.try(:grep, ::ActiveRecord::Validations::PresenceValidator)
|
299
|
+
.flat_map(&:attributes).unshift(_key(:id))
|
300
|
+
.include?(attribute.to_s.underscore.to_sym)
|
301
|
+
}.uniq
|
302
|
+
else
|
303
|
+
attrs.keys.include?(_key(:id)) ? [_key(:id)] : []
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
###
|
308
|
+
# Jbuilder methods
|
309
|
+
###
|
310
|
+
|
311
|
+
def _key(key)
|
312
|
+
@key_formatter ? @key_formatter.format(key).to_sym : key.to_sym
|
313
|
+
end
|
314
|
+
|
315
|
+
def _extract_hash_values(object, attributes, **schema_options)
|
316
|
+
attributes.each do |key|
|
317
|
+
result = _schema(key, _format_keys(object.fetch(key)), **schema_options[key] || {})
|
318
|
+
result = _set_description(key, result) if models.any?
|
319
|
+
_set_value key, result
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
def _extract_method_values(object, attributes, **schema_options)
|
324
|
+
attributes.each do |key|
|
325
|
+
result = _schema(key, _format_keys(object.public_send(key)), **schema_options[key] || {})
|
326
|
+
result = _set_description(key, result) if models.any?
|
327
|
+
_set_value key, result
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def _map_collection(collection)
|
332
|
+
super.first
|
333
|
+
end
|
334
|
+
|
335
|
+
def _merge_block(key)
|
336
|
+
current_value = _blank? ? BLANK : @attributes.fetch(_key(key), BLANK)
|
337
|
+
raise NullError.build(key) if current_value.nil?
|
338
|
+
|
339
|
+
new_value = _scope { yield self }
|
340
|
+
unless new_value.key?(:type) && new_value[:type] == :array || new_value.key?(:$ref)
|
341
|
+
new_value_properties = new_value
|
342
|
+
new_value = _object(**new_value_properties)
|
343
|
+
end
|
344
|
+
_merge_values(current_value, new_value)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
class Jbuilder
|
350
|
+
# Monkey-patch for Jbuilder::KeyFormatter to ignore schema keys
|
351
|
+
class KeyFormatter
|
352
|
+
alias_method :original_format, :format
|
353
|
+
|
354
|
+
def format(key)
|
355
|
+
return key if %i[type items properties].include?(key)
|
356
|
+
|
357
|
+
original_format(key)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jbuilder/schema/version"
|
4
|
+
require "jbuilder/schema/configuration"
|
5
|
+
require "jbuilder/schema/builder"
|
6
|
+
|
7
|
+
# Main gem module with configuration and helper methods
|
8
|
+
module JbuilderSchema
|
9
|
+
def jbuilder_schema(path, **options)
|
10
|
+
Builder.new(path, **options).schema!
|
11
|
+
end
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jbuilder-schema
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yuri Sidorov
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-10-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: jbuilder
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: safe_parser
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 5.0.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 5.0.0
|
55
|
+
description: Generate JSON Schema from Jbuilder files
|
56
|
+
email:
|
57
|
+
- hey@yurisidorov.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".standard.yml"
|
63
|
+
- Gemfile
|
64
|
+
- Gemfile.lock
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- lib/jbuilder/jbuilder.rb
|
69
|
+
- lib/jbuilder/schema.rb
|
70
|
+
- lib/jbuilder/schema/builder.rb
|
71
|
+
- lib/jbuilder/schema/configuration.rb
|
72
|
+
- lib/jbuilder/schema/renderer.rb
|
73
|
+
- lib/jbuilder/schema/resolver.rb
|
74
|
+
- lib/jbuilder/schema/template.rb
|
75
|
+
- lib/jbuilder/schema/version.rb
|
76
|
+
- sig/jbuilder/schema.rbs
|
77
|
+
homepage: https://github.com/newstler/jbuilder-schema
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata:
|
81
|
+
homepage_uri: https://github.com/newstler/jbuilder-schema
|
82
|
+
source_code_uri: https://github.com/newstler/jbuilder-schema
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: 2.6.0
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
requirements: []
|
98
|
+
rubygems_version: 3.3.7
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: Generate JSON Schema from Jbuilder files
|
102
|
+
test_files: []
|