prauga-flexdoc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prauga
4
+ module FlexDoc
5
+ Config = Data.define(:path, :spec_url, :title, :theme, :try_it_enabled) do
6
+ def initialize(path: "/docs", spec_url: "/openapi.json", title: "API Reference", theme: "system", try_it_enabled: true)
7
+ normalized = "/#{path.to_s.gsub(%r{\A/+|/+$}, "")}"
8
+ normalized = "/docs" if normalized == "/"
9
+ raise ArgumentError, "FlexDoc theme must be system, light, or dark" unless %w[system light dark].include?(theme)
10
+
11
+ super(path: normalized, spec_url:, title:, theme:, try_it_enabled: !!try_it_enabled)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "digest"
5
+ require "json"
6
+
7
+ module Prauga
8
+ module FlexDoc
9
+ class Host
10
+ IMMUTABLE_CACHE = "public, max-age=31536000, immutable"
11
+
12
+ attr_reader :config, :fingerprint
13
+
14
+ def initialize(config = Config.new, assets_dir: nil)
15
+ @config = config
16
+ root = assets_dir || File.expand_path("../../../assets", __dir__)
17
+ @javascript = File.binread(File.join(root, "flexdoc.standalone.js"))
18
+ @css = File.binread(File.join(root, "flexdoc.standalone.css"))
19
+ @fingerprint = Digest::SHA256.hexdigest(@javascript + "\0" + @css)[0, 16]
20
+ end
21
+
22
+ def response_for_path(path)
23
+ return documentation if path == config.path || path == "#{config.path}/"
24
+ return renderer_javascript if path == "#{config.path}/__flexdoc/renderer.js"
25
+ return renderer_css if path == "#{config.path}/__flexdoc/renderer.css"
26
+
27
+ Response.new(status: 404, content_type: "text/plain; charset=utf-8", body: "Not Found", cache_control: nil)
28
+ end
29
+
30
+ def documentation
31
+ options = {
32
+ contractVersion: "1",
33
+ title: config.title,
34
+ theme: config.theme,
35
+ tryIt: { enabled: config.try_it_enabled }
36
+ }
37
+ title = CGI.escapeHTML(config.title.to_s)
38
+ path = CGI.escapeHTML(config.path)
39
+ spec_url = safe_json(config.spec_url)
40
+ options_json = safe_json(options)
41
+ body = <<~HTML.delete("\n")
42
+ <!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><title>#{title}</title><link rel="stylesheet" href="#{path}/__flexdoc/renderer.css?v=#{fingerprint}"></head><body><div id="flexdoc-root"></div><script>window.__FLEXDOC_SPEC_URL__=#{spec_url};window.__FLEXDOC_OPTIONS__=#{options_json};</script><script src="#{path}/__flexdoc/renderer.js?v=#{fingerprint}"></script><script>(async function(){const root=document.getElementById('flexdoc-root');try{const baseUri=new URL(window.__FLEXDOC_SPEC_URL__,window.location.href).toString();const response=await fetch(baseUri);if(!response.ok)throw new Error('Unable to load OpenAPI specification: HTTP '+response.status);const spec=await response.json();const config={spec:spec,options:window.__FLEXDOC_OPTIONS__||{},baseUri:baseUri};if(window.FlexDocStandalone.mountAsync)await window.FlexDocStandalone.mountAsync(root,config);else window.FlexDocStandalone.mount(root,config);}catch(error){root.textContent=error instanceof Error?error.message:String(error);}})();</script></body></html>
43
+ HTML
44
+ Response.new(status: 200, content_type: "text/html; charset=utf-8", body:, cache_control: "no-cache")
45
+ end
46
+
47
+ def renderer_javascript
48
+ Response.new(status: 200, content_type: "application/javascript; charset=utf-8", body: @javascript, cache_control: IMMUTABLE_CACHE)
49
+ end
50
+
51
+ def renderer_css
52
+ Response.new(status: 200, content_type: "text/css; charset=utf-8", body: @css, cache_control: IMMUTABLE_CACHE)
53
+ end
54
+
55
+ private
56
+
57
+ def safe_json(value)
58
+ JSON.generate(value)
59
+ .gsub("<", "\\u003c")
60
+ .gsub(">", "\\u003e")
61
+ .gsub("&", "\\u0026")
62
+ .gsub("\u2028", "\\u2028")
63
+ .gsub("\u2029", "\\u2029")
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prauga
4
+ module FlexDoc
5
+ class RackApp
6
+ def initialize(host = Host.new)
7
+ @host = host
8
+ end
9
+
10
+ def call(env)
11
+ path = "#{env.fetch("SCRIPT_NAME", "")}#{env.fetch("PATH_INFO", "")}"
12
+ @host.response_for_path(path).rack
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prauga
4
+ module FlexDoc
5
+ module Rails
6
+ module_function
7
+
8
+ def mount(mapper, host: Host.new, at: nil, as: :flexdoc)
9
+ mount_path = at || host.config.path
10
+ normalized_mount_path = Config.new(path: mount_path).path
11
+ if normalized_mount_path != host.config.path
12
+ raise ArgumentError,
13
+ "Rails mount path #{normalized_mount_path.inspect} must match FlexDoc host path #{host.config.path.inspect}"
14
+ end
15
+
16
+ mapper.mount RackApp.new(host), at: normalized_mount_path, as: as
17
+ host
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prauga
4
+ module FlexDoc
5
+ Response = Data.define(:status, :content_type, :body, :cache_control) do
6
+ def headers
7
+ result = {
8
+ "content-type" => content_type,
9
+ "content-length" => body.bytesize.to_s
10
+ }
11
+ result["cache-control"] = cache_control if cache_control
12
+ result
13
+ end
14
+
15
+ def rack
16
+ [status, headers, [body]]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prauga
4
+ module FlexDoc
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "flexdoc/version"
4
+ require_relative "flexdoc/config"
5
+ require_relative "flexdoc/response"
6
+ require_relative "flexdoc/host"
7
+ require_relative "flexdoc/rack_app"
8
+ require_relative "flexdoc/rails"
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prauga-flexdoc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Prauga
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: actionpack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '7.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9'
33
+ - !ruby/object:Gem::Dependency
34
+ name: minitest
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.25'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.25'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rack
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.1'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '4'
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '3.1'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '4'
67
+ - !ruby/object:Gem::Dependency
68
+ name: rake
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '13.2'
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: '14'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '13.2'
84
+ - - "<"
85
+ - !ruby/object:Gem::Version
86
+ version: '14'
87
+ description: Framework-neutral Ruby host for the canonical FlexDoc OpenAPI renderer
88
+ with thin Rack and Rails integrations.
89
+ email:
90
+ executables: []
91
+ extensions: []
92
+ extra_rdoc_files: []
93
+ files:
94
+ - LICENSE
95
+ - README.md
96
+ - assets/flexdoc.standalone.css
97
+ - assets/flexdoc.standalone.js
98
+ - lib/prauga/flexdoc.rb
99
+ - lib/prauga/flexdoc/config.rb
100
+ - lib/prauga/flexdoc/host.rb
101
+ - lib/prauga/flexdoc/rack_app.rb
102
+ - lib/prauga/flexdoc/rails.rb
103
+ - lib/prauga/flexdoc/response.rb
104
+ - lib/prauga/flexdoc/version.rb
105
+ homepage: https://github.com/prauga/flexdoc
106
+ licenses:
107
+ - AGPL-3.0-or-later
108
+ metadata:
109
+ source_code_uri: https://github.com/prauga/flexdoc
110
+ bug_tracker_uri: https://github.com/prauga/flexdoc/issues
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '3.2'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.5.22
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Self-hosted FlexDoc integration for Rack and Rails
130
+ test_files: []