explicit 0.2.1 → 0.2.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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +75 -67
  3. data/app/helpers/explicit/application_helper.rb +32 -0
  4. data/app/views/explicit/documentation/_attribute.html.erb +38 -0
  5. data/app/views/explicit/documentation/_page.html.erb +166 -0
  6. data/app/views/explicit/documentation/_request.html.erb +87 -0
  7. data/app/views/explicit/documentation/request/_examples.html.erb +50 -0
  8. data/app/views/explicit/documentation/type/_agreement.html.erb +7 -0
  9. data/app/views/explicit/documentation/type/_array.html.erb +3 -0
  10. data/app/views/explicit/documentation/type/_big_decimal.html.erb +4 -0
  11. data/app/views/explicit/documentation/type/_boolean.html.erb +7 -0
  12. data/app/views/explicit/documentation/type/_date_time_iso8601.html.erb +3 -0
  13. data/app/views/explicit/documentation/type/_date_time_posix.html.erb +3 -0
  14. data/app/views/explicit/documentation/type/_enum.html.erb +7 -0
  15. data/app/views/explicit/documentation/type/_file.html.erb +9 -0
  16. data/app/views/explicit/documentation/type/_hash.html.erb +4 -0
  17. data/app/views/explicit/documentation/type/_integer.html.erb +25 -0
  18. data/app/views/explicit/documentation/type/_one_of.html.erb +11 -0
  19. data/app/views/explicit/documentation/type/_record.html.erb +9 -0
  20. data/app/views/explicit/documentation/type/_string.html.erb +21 -0
  21. data/config/locales/en.yml +27 -11
  22. data/lib/explicit/configuration.rb +1 -1
  23. data/lib/explicit/documentation/builder.rb +80 -0
  24. data/lib/explicit/documentation/markdown.rb +2 -13
  25. data/lib/explicit/documentation/output/swagger.rb +176 -0
  26. data/lib/explicit/documentation/output/webpage.rb +31 -0
  27. data/lib/explicit/documentation/page/partial.rb +20 -0
  28. data/lib/explicit/documentation/page/request.rb +27 -0
  29. data/lib/explicit/documentation/section.rb +9 -0
  30. data/lib/explicit/documentation.rb +12 -145
  31. data/lib/explicit/request/example.rb +50 -1
  32. data/lib/explicit/request/invalid_params_error.rb +1 -3
  33. data/lib/explicit/request/invalid_response_error.rb +2 -15
  34. data/lib/explicit/request/route.rb +18 -0
  35. data/lib/explicit/request.rb +43 -24
  36. data/lib/explicit/test_helper/example_recorder.rb +7 -2
  37. data/lib/explicit/test_helper.rb +25 -7
  38. data/lib/explicit/type/agreement.rb +39 -0
  39. data/lib/explicit/type/array.rb +56 -0
  40. data/lib/explicit/type/big_decimal.rb +58 -0
  41. data/lib/explicit/type/boolean.rb +47 -0
  42. data/lib/explicit/type/date_time_iso8601.rb +41 -0
  43. data/lib/explicit/type/date_time_posix.rb +44 -0
  44. data/lib/explicit/type/enum.rb +41 -0
  45. data/lib/explicit/type/file.rb +60 -0
  46. data/lib/explicit/type/hash.rb +57 -0
  47. data/lib/explicit/type/integer.rb +79 -0
  48. data/lib/explicit/type/literal.rb +45 -0
  49. data/lib/explicit/type/modifiers/default.rb +24 -0
  50. data/lib/explicit/type/modifiers/description.rb +11 -0
  51. data/lib/explicit/type/modifiers/nilable.rb +19 -0
  52. data/lib/explicit/type/modifiers/param_location.rb +11 -0
  53. data/lib/explicit/type/one_of.rb +46 -0
  54. data/lib/explicit/type/record.rb +96 -0
  55. data/lib/explicit/type/string.rb +68 -0
  56. data/lib/explicit/type.rb +112 -0
  57. data/lib/explicit/version.rb +1 -1
  58. data/lib/explicit.rb +28 -18
  59. metadata +47 -25
  60. data/app/views/explicit/application/_documentation.html.erb +0 -136
  61. data/app/views/explicit/application/_request.html.erb +0 -37
  62. data/lib/explicit/documentation/property.rb +0 -19
  63. data/lib/explicit/spec/agreement.rb +0 -17
  64. data/lib/explicit/spec/array.rb +0 -28
  65. data/lib/explicit/spec/bigdecimal.rb +0 -27
  66. data/lib/explicit/spec/boolean.rb +0 -30
  67. data/lib/explicit/spec/date_time_iso8601.rb +0 -17
  68. data/lib/explicit/spec/date_time_posix.rb +0 -21
  69. data/lib/explicit/spec/default.rb +0 -20
  70. data/lib/explicit/spec/error.rb +0 -63
  71. data/lib/explicit/spec/hash.rb +0 -30
  72. data/lib/explicit/spec/inclusion.rb +0 -15
  73. data/lib/explicit/spec/integer.rb +0 -53
  74. data/lib/explicit/spec/literal.rb +0 -15
  75. data/lib/explicit/spec/nilable.rb +0 -15
  76. data/lib/explicit/spec/one_of.rb +0 -40
  77. data/lib/explicit/spec/record.rb +0 -33
  78. data/lib/explicit/spec/string.rb +0 -50
  79. data/lib/explicit/spec.rb +0 -72
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0ccbe69649ce5ca8fac5e0419943c1357206950a3436e279670cb62f5a359fe
4
- data.tar.gz: 556861f1b18e7b900753ddc3aba962501a4991047dae5e1260a120d7de6c15e7
3
+ metadata.gz: 5adb170708747adfb160afcf5c4248892418ec518302defa17c692fa34f4cfff
4
+ data.tar.gz: 10258e426028dcbce19a9825b5541edc75d5e288c8846f7d21f36f2a239ce9b8
5
5
  SHA512:
6
- metadata.gz: 50d01f40937fe47d381d5807bc823b1abbb63e6929a09215da81167bad53276e96584ee581ff945eebf25568ca57c3234827102ab0e73fb4cfc62eb846631d3a
7
- data.tar.gz: 4219bebcffce9aac144a739137aa3670f036f7224991fda4acc2db973cc8f11fa25532c604c9c26bf164cb77a3dc18174605b589b0afe9fe031a8ceee67bd1b9
6
+ metadata.gz: 86e5c040ecc814b16d6a8b7b43d9f7b6ee4764a36b5b21e56d504cde8f7229db0ffa1a2eb66e7c2621f8c1a2a453d59823577bf17bac2485551f0ed567b6edb0
7
+ data.tar.gz: a246c4136d476c61ef8bb59cd09b904918f2ec01ac4812862a6a02f6d051d7982e0ba37bc9577b6d045bb9c39185f274c535c0b9f3e7cfceb21671663117dc68
data/README.md CHANGED
@@ -1,16 +1,18 @@
1
1
  # Explicit
2
2
 
3
- Explicit is a validation and documentation library for JSON APIs that enforces
4
- documented specs at runtime.
3
+ Explicit is a validation and documentation library for REST APIs that enforces
4
+ documented types at runtime.
5
+
6
+ ![Documentation example screenshot](https://raw.githubusercontent.com/luizpvas/explicit/refs/heads/main/assets/webapp_screenshot.png)
5
7
 
6
8
  1. [Installation](#installation)
7
9
  2. [Defining requests](#defining-requests)
8
- 3. [Reusing specs](#reusing-specs)
10
+ 3. [Reusing types](#reusing-types)
9
11
  4. [Reusing requests](#reusing-requests)
10
12
  5. [Writing tests](#writing-tests)
11
13
  6. [Publishing documentation](#publishing-documentation)
12
14
  - [Adding request examples](#adding-request-examples)
13
- 7. Specs
15
+ 7. Types
14
16
  - [Agreement](#agreement)
15
17
  - [Array](#array)
16
18
  - [BigDecimal](#bigdecimal)
@@ -19,8 +21,9 @@ documented specs at runtime.
19
21
  - [Date Time Posix](#date-time-posix)
20
22
  - [Default](#default)
21
23
  - [Description](#description)
24
+ - [Enum](#enum)
25
+ - [File](#file)
22
26
  - [Hash](#hash)
23
- - [Inclusion](#inclusion)
24
27
  - [Integer](#integer)
25
28
  - [Literal](#literal)
26
29
  - [Nilable](#nilable)
@@ -31,7 +34,6 @@ documented specs at runtime.
31
34
  - [Changing examples file path](#changing-examples-file-path)
32
35
  - [Customizing error messages](#customizing-error-messages)
33
36
  - [Customizing error serialization](#customizing-error-serialization)
34
- 9. [Performance benchmark](#performance-benchmark)
35
37
 
36
38
  # Installation
37
39
 
@@ -53,10 +55,10 @@ available:
53
55
  - `title(text)` - Adds a title to the request. Displayed in documentation.
54
56
  - `description(text)` - Adds a description to the endpoint. Displayed in
55
57
  documentation. Markdown supported.
56
- - `header(name, spec)` - Adds a spec to the request header.
57
- - `param(name, spec, options = {})` - Adds a spec to the request param.
58
+ - `header(name, type)` - Adds a type to the request header.
59
+ - `param(name, type, options = {})` - Adds a type to the request param.
58
60
  It works for params in the request body, query string and path params.
59
- - `response(status, spec)` - Adds a response spec. You can add multiple
61
+ - `response(status, type)` - Adds a response type. You can add multiple
60
62
  responses with different formats.
61
63
  - `add_example(params:, headers:, response:)` - Adds an example to the
62
64
  documentation. [See more details here](#adding-request-examples).
@@ -72,14 +74,14 @@ class RegistrationsController < ActionController::API
72
74
  Request = Explicit::Request.new do
73
75
  post "/api/registrations"
74
76
 
75
- description <<-MD
77
+ description <<~MD
76
78
  Attempts to register a new user in the system. If `payment_type` is not
77
79
  specified a trial period of 30 days is started.
78
80
  MD
79
81
 
80
82
  param :name, [:string, empty: false]
81
83
  param :email, [:string, format: URI::MailTo::EMAIL_REGEXP, strip: true]
82
- param :payment_type, [:inclusion, ["free_trial", "credit_card"]], default: "free_trial"
84
+ param :payment_type, [:enum, ["free_trial", "credit_card"]], default: "free_trial"
83
85
  param :terms_of_use, :agreement
84
86
 
85
87
  response 200, { user: { id: :integer, email: :string } }
@@ -98,13 +100,13 @@ class RegistrationsController < ActionController::API
98
100
  end
99
101
  ```
100
102
 
101
- # Reusing specs
103
+ # Reusing types
102
104
 
103
- Specs are just data. You can share specs the same way you reuse constants or
105
+ Types are just data. You can share types the same way you reuse constants or
104
106
  configs in your app. For example:
105
107
 
106
108
  ```ruby
107
- module MyApp::Spec
109
+ module MyApp::Type
108
110
  UUID = [:string, format: /^\h{8}-(\h{4}-){3}\h{12}$/].freeze
109
111
  EMAIL = [:string, format: URI::MailTo::EMAIL_REGEXP, strip: true].freeze
110
112
 
@@ -114,11 +116,11 @@ module MyApp::Spec
114
116
  }.freeze
115
117
  end
116
118
 
117
- # ... and then reference the shared specs when needed
119
+ # ... and then reference the shared types when needed
118
120
  Request = Explicit::Request.new do
119
- param :customer_uuid, MyApp::Spec::UUID
120
- param :email, MyApp::Spec::EMAIL
121
- param :address, MyApp::Spec::ADDRESS
121
+ param :customer_uuid, MyApp::Type::UUID
122
+ param :email, MyApp::Type::EMAIL
123
+ param :address, MyApp::Type::ADDRESS
122
124
  end
123
125
  ```
124
126
 
@@ -147,7 +149,7 @@ end
147
149
  Include `Explicit::TestHelper` in your `test/test_helper.rb` or
148
150
  `spec/rails_helper.rb`. This module provides the method
149
151
  `fetch(request, **options)` that let's you verify the endpoint works as
150
- expected and that it responds with a valid response according to the spec.
152
+ expected and that it responds with a valid response according to the docs.
151
153
 
152
154
  <details open>
153
155
  <summary>For Minitest users, add the following line to your <code>test/test_helper.rb</code></summary>
@@ -179,8 +181,8 @@ end
179
181
  </details>
180
182
 
181
183
  To test your controller, call `fetch(request, **options)` and write
182
- assertions against the response. If the response is invalid according to the
183
- spec the test fails with `Explicit::Request::InvalidResponseError`.
184
+ assertions against the response. If the response is invalid the test fails with
185
+ `Explicit::Request::InvalidResponseError`.
184
186
 
185
187
  The response object has a `status`, an integer value for the http status, and
186
188
  `data`, a hash with the response data. It also provides `dig` for a
@@ -190,7 +192,7 @@ slighly shorter syntax when accessing nested attributes.
190
192
  > `put "/customers/:customer_id"` you must call as
191
193
  > `fetch(CustomerController::UpdateRequest, { customer_id: 123 })`.
192
194
 
193
- > Note: Response specs are only verified in test environment with no
195
+ > Note: Response types are only verified in test environment with no
194
196
  > performance penalty when running in production.
195
197
 
196
198
  <details open>
@@ -243,6 +245,8 @@ Call `Explicit::Documentation.new` to group, organize and publish the
243
245
  documentation for your API. The following methods are available:
244
246
 
245
247
  - `page_title(text)` - Sets the web page title.
248
+ - `company_logo_url(url)` - Shows the company logo above the navigation menu.
249
+ - `version(semver)` - Sets the version of the API. Default: "1.0"
246
250
  - `section(name, &block)` - Adds a section to the navigation menu.
247
251
  - `add(request)` - Adds a request to the section
248
252
  - `add(title:, partial:)` - Adds a partial to the section
@@ -253,6 +257,8 @@ For example:
253
257
  module MyApp::API::V1
254
258
  Documentation = Explicit::Documentation.new do
255
259
  page_title "Acme API Docs"
260
+ company_logo_url "https://my-app.com/logo.png"
261
+ version "1.0.5"
256
262
 
257
263
  section "Introduction" do
258
264
  add title: "About", partial: "api/v1/introduction/about"
@@ -293,7 +299,7 @@ You can add request examples in two different ways:
293
299
 
294
300
  In a request, call `add_example(params:, headers:, response:)` after declaring
295
301
  params and responses. It's important the example comes after params and
296
- responses to make sure it actually follows the spec.
302
+ responses to make sure it actually follows the type definition.
297
303
 
298
304
  For example:
299
305
 
@@ -360,37 +366,36 @@ end
360
366
  ```
361
367
 
362
368
  Whenever you wish to refresh the examples file run the test suite with the ENV
363
- `EXPLICIT_PERSIST_EXAMPLES` set. For example
364
- `EXPLICIT_PERSIST_EXAMPLES=true bin/rails test` or
365
- `EXPLICIT_PERSIST_EXAMPLES=true bundle exec rspec`. The examples file is located
366
- at `#{Rails.root}/public/explicit_request_examples.json` by default, but you can
369
+ `UPDATE_REQUEST_EXAMPLES` set. For example
370
+ `UPDATE_REQUEST_EXAMPLES=true bin/rails test` or
371
+ `UPDATE_REQUEST_EXAMPLES=true bundle exec rspec`. The file is located at
372
+ `#{Rails.root}/public/explicit_request_examples.json` by default, but you can
367
373
  [change it here](#request-examples-file-path).
368
374
 
369
375
  **Important: be careful not to leak any sensitive data when persisting
370
376
  examples from tests**
371
377
 
372
- # Specs
378
+ # Types
373
379
 
374
380
  ### Agreement
375
381
 
376
382
  ```ruby
377
383
  :agreement
378
- [:agreement, parse: true]
379
384
  ```
380
385
 
381
386
  A boolean that must always be true. Useful for terms of use or agreement
382
- acceptances. If `parse: true` is specified then the following
383
- values are accepted: `true`, `"true"`, `"on"`, `"1"` and `1`.
387
+ acceptances. The following values are accepted: `true`, `"true"`, `"on"`, `"1"`
388
+ and `1`.
384
389
 
385
390
  ### Array
386
391
 
387
392
  ```ruby
388
- [:array, subspec, options = {}]
393
+ [:array, subtype, options = {}]
389
394
  [:array, :string]
390
395
  [:array, :integer, empty: false]
391
396
  ```
392
397
 
393
- All items in the array must be valid according to the subspec. If at least one
398
+ All items in the array must be valid according to the subtype. If at least one
394
399
  value is invalid then the array is invalid.
395
400
 
396
401
  ### BigDecimal
@@ -402,19 +407,16 @@ value is invalid then the array is invalid.
402
407
  ```
403
408
 
404
409
  Value must be an integer or a string like `"0.2"` to avoid rounding errors.
405
-
406
410
  [Reference](https://ruby-doc.org/stdlib-3.1.0/libdoc/bigdecimal/rdoc/BigDecimal.html)
407
411
 
408
412
  ### Boolean
409
413
 
410
414
  ```ruby
411
415
  :boolean
412
- [:boolean, parse: true]
413
416
  ```
414
417
 
415
- If `parse: true` is specified then the following values are converted to `true`:
416
- `"true"`, `"on"`, `"1"` and `1`, and the following values are converted to
417
- `false`: `"false"`, `"off"`, `"0"` and `0`.
418
+ The following values are true: `true`, `"true"`, `"on"`, `"1"` and `1`, and the
419
+ following values are false: `false`, `"false"`, `"off"`, `"0"` and `0`.
418
420
 
419
421
  ### Date Time ISO8601
420
422
 
@@ -437,7 +439,7 @@ example: `1733923153`
437
439
  ### Default
438
440
 
439
441
  ```ruby
440
- [:default, default_value, subspec]
442
+ [:default, default_value, subtype]
441
443
  [:default, "USD", :string]
442
444
  [:default, 0, :integer]
443
445
  [:default, -> { Time.current.iso8601 }, :date_time_iso8601]
@@ -453,52 +455,59 @@ called.
453
455
  ### Description
454
456
 
455
457
  ```ruby
456
- [:description, markdown_text, subspec]
458
+ [:description, markdown_text, subtype]
457
459
  [:description, "Customer full name", :string]
458
460
  [:description, "Rating score from 0 (bad) to 5 (good)", :integer]
459
461
  ```
460
462
 
461
- Adds a description to the spec. Descriptions are displayed in documentation
463
+ Adds a description to the type. Descriptions are displayed in documentation
462
464
  and do not affect validation in any way with. There is no overhead at runtime.
463
465
  Markdown supported.
464
466
 
465
467
  ### Hash
466
468
 
467
469
  ```ruby
468
- [:hash, keyspec, valuespec, options = {}]
470
+ [:hash, keytype, valuetype, options = {}]
469
471
  [:hash, :string, :string]
470
472
  [:hash, :string, :integer]
471
473
  [:hash, :string, :integer, empty: false]
472
474
  [:hash, :string, [:array, :date_time_iso8601]]
473
475
  ```
474
476
 
475
- Hashes are key value pairs where all keys must match keyspec and all values must
476
- match valuespec. If you are expecting a hash with a specific set of keys use a
477
+ Hashes are key value pairs where all keys must match keytype and all values must
478
+ match valuetype. If you are expecting a hash with a specific set of keys use a
477
479
  [record](#record) instead.
478
480
 
479
- ### Inclusion
481
+ ### Enum
480
482
 
481
483
  ```ruby
482
- [:inclusion, allowed_values]
483
- [:inclusion, ["user", "admin"]]
484
- [:inclusion, [10, 20, 30, 40, 50]]
484
+ [:enum, allowed_values]
485
+ [:enum, ["user", "admin"]]
486
+ [:enum, [10, 20, 30, 40, 50]]
485
487
  ```
486
488
 
487
- Value must be present in the set of allowed values.
489
+ ### File
490
+
491
+ ```ruby
492
+ :file
493
+ [:file, max_size: 2.megabytes]
494
+ [:file, content_types: %w[image/png image/jpeg]]
495
+ ```
496
+
497
+ Value must be an uploaded file using "multipart/form-data" encoding.
488
498
 
489
499
  ### Integer
490
500
 
491
501
  ```ruby
492
502
  :integer
493
- [:integer, parse: true]
494
503
  [:integer, negative: false]
495
504
  [:integer, positive: false]
496
505
  [:integer, min: 0] # inclusive
497
506
  [:integer, max: 10] # inclusive
498
507
  ```
499
508
 
500
- If `parse: true` is specified then integer encoded string values such as "10" or
501
- "-2" are automatically converted to integer.
509
+ Integer encoded string values such as "10" or "-2" are automatically converted
510
+ to integer.
502
511
 
503
512
  ### Literal
504
513
 
@@ -506,48 +515,48 @@ If `parse: true` is specified then integer encoded string values such as "10" or
506
515
  [:literal, value]
507
516
  [:literal, 6379]
508
517
  [:literal, "value"]
509
- "value" # strings work like a literal specs, so you can use this shorter syntax.
518
+ "value" # strings are literal types, so you can use the shorter syntax.
510
519
  ```
511
520
 
512
- A literal value behaves similar to inclusion with a single value. Useful for
513
- matching against multiple specs in [`one_of`](#one-of).
521
+ A literal value behaves similar to an enum with a single value. Useful for
522
+ matching against multiple types in [`one_of`](#one-of).
514
523
 
515
524
  ### Nilable
516
525
 
517
526
  ```ruby
518
- [:nilable, subspec]
527
+ [:nilable, subtype]
519
528
  [:nilable, :string]
520
529
  [:nilable, [:array, :integer]]
521
530
  ```
522
531
 
523
- Value must either match the subspec or be nil.
532
+ Value must either match the subtype or be nil.
524
533
 
525
534
  ### One of
526
535
 
527
536
  ```ruby
528
- [:one_of, spec1, spec2, ..., specN]
537
+ [:one_of, type1, type2, ..., typeN]
529
538
  [:one_of, :string, :integer]
530
539
  [:one_of, { email: :string }, { phone_number: :string }]
531
540
  ```
532
541
 
533
- Attempts to validate against each spec in order stopping at the first spec that
534
- successfully matches the value. If none of the specs match, an error is
542
+ Attempts to validate against each type in order stopping at the first type that
543
+ successfully matches the value. If none of the types match, an error is
535
544
  returned.
536
545
 
537
546
  ### Record
538
547
 
539
548
  ```ruby
540
- user_spec = {
549
+ user_type = {
541
550
  name: :string,
542
551
  email: [:string, format: URI::MailTo::EMAIL_REGEXP]
543
552
  }
544
553
 
545
- address_spec = {
554
+ address_type = {
546
555
  country_name: :string,
547
556
  zipcode: [:string, { format: /\d{6}-\d{3}/, strip: true }]
548
557
  }
549
558
 
550
- payment_spec = {
559
+ payment_type = {
551
560
  currency: [:nilable, :string], # use :nilable for optional attribute
552
561
  amount: :bigdecimal
553
562
  }
@@ -561,8 +570,9 @@ records with array of records, etc.
561
570
 
562
571
  ```ruby
563
572
  :string
564
- [:string, strip: true]
573
+ [:string, strip: true] # " foo " gets transformed to "foo"
565
574
  [:string, empty: false]
575
+ [:string, downcase: true] # "FOO" gets transformed to "foo"
566
576
  [:string, format: URI::MailTo::EMAIL_REGEXP]
567
577
  [:string, minlength: 8] # inclusive
568
578
  [:string, maxlength: 20] # inclusive
@@ -604,9 +614,7 @@ your base controller. Use the following code as a starting point:
604
614
  ```ruby
605
615
  class ApplicationController < ActionController::API
606
616
  rescue_from Explicit::Request::InvalidParamsError do |err|
607
- params = Explicit::Spec::Error.translate(err.errors)
608
-
609
- render json: { error: "invalid_params", params: }, status: 422
617
+ render json: { error: "invalid_params", params: err.errors }, status: 422
610
618
  end
611
619
  end
612
620
  ```
@@ -2,5 +2,37 @@
2
2
 
3
3
  module Explicit
4
4
  module ApplicationHelper
5
+ def type_render(type)
6
+ render partial: type.partial, locals: { type: }
7
+ end
8
+
9
+ def type_attribute_render(name:, type:)
10
+ render partial: "explicit/documentation/attribute", locals: { name:, type: }
11
+ end
12
+
13
+ def type_constraints(&block)
14
+ content_tag(:div, class: "flex flex-wrap gap-2", &block)
15
+ end
16
+
17
+ def type_constraint(name, value)
18
+ content_tag(:div, class: "bg-neutral-200 px-1 text-sm") do
19
+ content_tag(:span, name) + " " + content_tag(:span, value)
20
+ end
21
+ end
22
+
23
+ def type_has_details?(type)
24
+ type.description.present? || type.has_details?
25
+ end
26
+
27
+ def format_request_example(request:, example:)
28
+ line_break = '<span class="text-white">\</span>'
29
+
30
+ <<~BASH.html_safe
31
+ #{example.to_curl_lines.join(" #{line_break}\n")}
32
+
33
+ # #{example.response.status} #{Rack::Utils::HTTP_STATUS_CODES[example.response.status]}
34
+ #{JSON.pretty_generate(example.response.data)}
35
+ BASH
36
+ end
5
37
  end
6
38
  end
@@ -0,0 +1,38 @@
1
+ <div class="p-3" x-data="{ expanded: false }">
2
+ <div class="flex items-center gap-2" x-on:click="expanded = !expanded">
3
+ <span class="font-mono font-bold">
4
+ <%= name %>
5
+ </span>
6
+
7
+ <span class="text-sm text-neutral-500">
8
+ <%= type.summary %>
9
+ </span>
10
+
11
+ <% if type.param_location_path? %>
12
+ <span class="bg-neutral-800 text-white text-xs font-bold rounded px-1">in URL</span>
13
+ <% end %>
14
+
15
+ <% if type.has_details? || type.description.present? %>
16
+ <div class="record__summary__expand">
17
+ <span x-show="expanded">&#9650;</span>
18
+ <span x-show="!expanded">&#9660;</span>
19
+ </div>
20
+ <% end %>
21
+ </div>
22
+
23
+ <% if type.has_details? || type.description.present? %>
24
+ <div class="mt-2" x-show="expanded">
25
+ <% if (description = type.description) %>
26
+ <div class="text-neutral-600 markdown">
27
+ <%= Explicit::Documentation::Markdown.to_html(description) %>
28
+ </div>
29
+ <% end %>
30
+
31
+ <% if type.has_details? %>
32
+ <div class="record__subtype">
33
+ <%= type_render type %>
34
+ </div>
35
+ <% end %>
36
+ </div>
37
+ <% end %>
38
+ </div>
@@ -0,0 +1,166 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= local_assigns[:page_title] || "API Documentation" %></title>
5
+
6
+ <style>
7
+ html, body {
8
+ font-family: sans-serif;
9
+ font-size: 14px;
10
+ margin: 0;
11
+ padding: 0;
12
+
13
+ --color-neutral-50: #fafafa;
14
+ --color-neutral-100: #f5f5f5;
15
+ --color-neutral-200: #e5e5e5;
16
+ --color-neutral-300: #d4d4d8;
17
+ --color-neutral-400: #a3a3a3;
18
+ --color-neutral-500: #737373;
19
+ --color-neutral-600: #525252;
20
+ }
21
+
22
+ .page:not(:first-of-type) {
23
+ border-top: 1px solid var(--color-neutral-200);
24
+ }
25
+ .page__url {
26
+ background: var(--color-neutral-100);
27
+ border: 1px solid var(--color-neutral-300);
28
+ font-family: monospace;
29
+ padding: 0.8rem;
30
+ }
31
+ .page__url__shared {
32
+ color: var(--color-neutral-500);
33
+ }
34
+ .page__url__path {
35
+ font-weight: bold;
36
+ }
37
+ .page__container {
38
+ display: flex;
39
+ gap: 1rem;
40
+ margin-top: 1rem;
41
+ }
42
+ .page__request {
43
+ width: 50%;
44
+ }
45
+ .page__response {
46
+ width: 50%;
47
+ }
48
+
49
+ .markdown code {
50
+ background: var(--color-neutral-100);
51
+ color: var(--color-neutral-900);
52
+ }
53
+ .markdown p:first-of-type {
54
+ margin-block-start: 0em;
55
+ }
56
+ .markdown p:last-of-type {
57
+ margin-block-end: 0em;
58
+ }
59
+ .markdown ul {
60
+ list-style-type: disc;
61
+ }
62
+ .markdown li {
63
+ margin-left: 2rem;
64
+ }
65
+
66
+ .record__param {
67
+ padding: 0.8rem;
68
+ }
69
+ .record__param:not(:last-of-type) {
70
+ border-bottom: 1px solid var(--color-neutral-200);
71
+ }
72
+ .record__summary {
73
+ display: flex;
74
+ align-items: end;
75
+ gap: 1rem;
76
+ }
77
+ .record__summary__expand {
78
+ margin-left: auto;
79
+ color: var(--color-neutral-400);
80
+ }
81
+ .record__constraint {
82
+ font-size: 12px;
83
+ background: var(--color-neutral-200);
84
+ padding: 1px 4px;
85
+ border-radius: 1px;
86
+ }
87
+ .record__subtype {
88
+ margin-top: 0.5rem;
89
+ }
90
+
91
+ .responses__tabs {
92
+ display: flex;
93
+ }
94
+ .responses__tab {
95
+ padding: 10px;
96
+ }
97
+ .responses__tab.active {
98
+ border-left: 1px solid var(--color-neutral-300);
99
+ border-top: 1px solid var(--color-neutral-300);
100
+ border-right: 1px solid var(--color-neutral-300);
101
+ border-bottom: 1px solid #FFF;
102
+ margin-bottom: -1px;
103
+ }
104
+ .responses__types {
105
+ border: 1px solid var(--color-neutral-300);
106
+ }
107
+ </style>
108
+
109
+ <script src="https://cdn.tailwindcss.com"></script>
110
+ <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
111
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
112
+ </head>
113
+
114
+ <body>
115
+ <div class="flex" x-data="{ activeLink: null }">
116
+ <section class="bg-neutral-100 border-r w-[280px] shrink-0 h-screen overflow-y-auto">
117
+ <% if company_logo_url.present? %>
118
+ <div class="flex items-center justify-center mt-4 mb-8">
119
+ <img src="<%= company_logo_url %>" class="max-w-full h-auto" />
120
+ </div>
121
+ <% end %>
122
+
123
+ <div class="text-center text-xs border-y mb-4 flex divide-x">
124
+ <div class="p-1 text-neutral-500 w-1/2">
125
+ Version <%= version %>
126
+ </div>
127
+ <div class="p-1 w-1/2">
128
+ <%= link_to url_helpers.explicit_documentation_swagger_path, target: "_blank", class: "flex items-center justify-center gap-1 text-neutral-900" do %>
129
+ <span>Swagger</span>
130
+
131
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4">
132
+ <path d="M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z" />
133
+ <path d="M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z" />
134
+ </svg>
135
+ <% end %>
136
+ </div>
137
+ </div>
138
+
139
+ <div class="space-y-4" >
140
+ <% sections.each do |section| %>
141
+ <details class="px-4" open>
142
+ <summary class="font-bold"><%= section.name %></summary>
143
+
144
+ <% section.pages.each do |page| %>
145
+ <%= link_to page.title,
146
+ "##{page.anchor}",
147
+ class: "block pl-4 hover:bg-neutral-200",
148
+ "x-bind:class" => "{ 'bg-neutral-200': activeLink == '#{page.anchor}' }" %>
149
+ <% end %>
150
+ </details>
151
+ <% end %>
152
+ </div>
153
+ </section>
154
+
155
+ <main class="relative grow h-screen overflow-auto">
156
+ <% sections.each do |section| %>
157
+ <% section.pages.each do |page| %>
158
+ <div class="p-8 border-t">
159
+ <%= render partial: page.partial, locals: { page: } %>
160
+ </div>
161
+ <% end %>
162
+ <% end %>
163
+ </main>
164
+ </div>
165
+ </body>
166
+ </html>