elder_docs 0.1.4 → 0.1.5
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/README.md +19 -10
- data/elderdocs.yml.example +3 -3
- data/frontend/src/App.jsx +40 -39
- data/frontend/src/components/ApiExplorer.jsx +121 -8
- data/frontend/src/components/Sidebar.jsx +0 -10
- data/lib/elder_docs/cli.rb +15 -19
- data/lib/elder_docs/config.rb +5 -1
- data/lib/elder_docs/engine/api_controller.rb +117 -0
- data/lib/elder_docs/engine.rb +7 -10
- data/lib/elder_docs/generator.rb +20 -4
- data/lib/elder_docs/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ec656c7eb8b5c1d06212d3004fbf785b6e4d1a4a5364d66dcf2f637447465c4
|
|
4
|
+
data.tar.gz: 691d4f247a2afa096ba57510fc515e926b44fff596fc9c56982e8682bdfe76ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1601af0b736fced56e45a3a44a3727d63114c34a28378c83ae4db4b49febd65e3756c03e221358c35847012760f0e0f6e5c2943c7ffebc8d8dcbb78a28ff8e96
|
|
7
|
+
data.tar.gz: dd5209f2fbbed2e74a4a75cc10e2b43920640ad975e0d38d0b492714a747526e3624e4918cea5ba3b8016e615cf2428f36080a7f5ad320bb9ad409c968bf9c0a
|
data/README.md
CHANGED
|
@@ -5,10 +5,11 @@ Interactive API documentation for Rails. Convert your OpenAPI spec into a beauti
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- 🚀 **Interactive API Explorer** - Test endpoints directly from docs
|
|
8
|
-
- 🎨 **Customizable UI** -
|
|
8
|
+
- 🎨 **Customizable UI** - Configure via `elderdocs.yml`
|
|
9
9
|
- 📖 **Guides & Articles** - Add custom documentation
|
|
10
10
|
- 🔐 **Multiple Auth Types** - Bearer, API Key, Basic, OAuth2
|
|
11
|
-
- ⚡ **Zero
|
|
11
|
+
- ⚡ **Zero Compilation** - Edit JSON files, changes appear instantly!
|
|
12
|
+
- 📋 **Copy cURL** - One-click copy of API requests
|
|
12
13
|
|
|
13
14
|
## Installation
|
|
14
15
|
|
|
@@ -65,13 +66,16 @@ Create `articles.json`:
|
|
|
65
66
|
]
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
### 3.
|
|
69
|
+
### 3. Build frontend (one-time setup)
|
|
69
70
|
|
|
70
71
|
```bash
|
|
71
72
|
bundle exec elderdocs deploy
|
|
72
73
|
```
|
|
73
74
|
|
|
74
|
-
This builds the SPA into `public/elderdocs` (configurable via `output_path`)
|
|
75
|
+
This builds the frontend SPA into `public/elderdocs` (configurable via `output_path`).
|
|
76
|
+
|
|
77
|
+
**Note:** This is a one-time build. After this, you can edit `definitions.json` and `articles.json` - changes appear instantly without rebuilding!
|
|
78
|
+
|
|
75
79
|
### 4. Mount in routes
|
|
76
80
|
|
|
77
81
|
```ruby
|
|
@@ -83,11 +87,15 @@ mount ElderDocs::Engine, at: '/docs'
|
|
|
83
87
|
|
|
84
88
|
Open `http://localhost:3000/docs` 🎉
|
|
85
89
|
|
|
86
|
-
##
|
|
90
|
+
## Dynamic Updates
|
|
91
|
+
|
|
92
|
+
**No compilation needed!** After the initial build:
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
- ✏️ Edit `definitions.json` → Changes appear immediately
|
|
95
|
+
- ✏️ Edit `articles.json` → Changes appear immediately
|
|
96
|
+
- ✏️ Edit `elderdocs.yml` → Restart Rails server to see UI changes
|
|
89
97
|
|
|
90
|
-
|
|
98
|
+
The frontend fetches data dynamically from your JSON files at runtime.
|
|
91
99
|
|
|
92
100
|
## Configuration
|
|
93
101
|
|
|
@@ -111,7 +119,7 @@ auth_types:
|
|
|
111
119
|
- basic
|
|
112
120
|
- oauth2
|
|
113
121
|
|
|
114
|
-
# UI customization
|
|
122
|
+
# UI customization
|
|
115
123
|
ui:
|
|
116
124
|
font_heading: 'Syne'
|
|
117
125
|
font_body: 'IBM Plex Sans'
|
|
@@ -122,8 +130,9 @@ ui:
|
|
|
122
130
|
surface: '#ffffff'
|
|
123
131
|
corner_radius: '0px'
|
|
124
132
|
|
|
125
|
-
#
|
|
126
|
-
|
|
133
|
+
# Custom file paths (optional, defaults to definitions.json and articles.json)
|
|
134
|
+
definitions_file: definitions.json
|
|
135
|
+
articles_file: articles.json
|
|
127
136
|
|
|
128
137
|
# Where to write generated assets (relative paths are resolved from Rails.root)
|
|
129
138
|
output_path: ./public/elderdocs
|
data/elderdocs.yml.example
CHANGED
|
@@ -8,9 +8,9 @@ mount_path: /docs
|
|
|
8
8
|
# Output directory for generated assets (relative to your Rails root)
|
|
9
9
|
output_path: ./public/elderdocs
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# Custom file paths (optional, defaults to definitions.json and articles.json in Rails root)
|
|
12
|
+
# definitions_file: definitions.json
|
|
13
|
+
# articles_file: articles.json
|
|
14
14
|
|
|
15
15
|
# Default API server URL (overrides OpenAPI servers if set)
|
|
16
16
|
# Leave empty to use the first server from your OpenAPI spec
|
data/frontend/src/App.jsx
CHANGED
|
@@ -81,49 +81,50 @@ function App() {
|
|
|
81
81
|
}, [])
|
|
82
82
|
|
|
83
83
|
useEffect(() => {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const tryInitialize = () => {
|
|
98
|
-
if (window.ElderDocsData) {
|
|
99
|
-
initializeData(window.ElderDocsData)
|
|
100
|
-
return true
|
|
101
|
-
}
|
|
102
|
-
return false
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!tryInitialize()) {
|
|
106
|
-
const handleDataLoaded = () => {
|
|
107
|
-
if (window.ElderDocsData) {
|
|
108
|
-
initializeData(window.ElderDocsData)
|
|
84
|
+
const loadData = async () => {
|
|
85
|
+
try {
|
|
86
|
+
// Fetch all data in parallel
|
|
87
|
+
const [definitionsRes, articlesRes, configRes] = await Promise.all([
|
|
88
|
+
fetch('/docs/api/definitions'),
|
|
89
|
+
fetch('/docs/api/articles'),
|
|
90
|
+
fetch('/docs/api/config')
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
if (!definitionsRes.ok) {
|
|
94
|
+
throw new Error(`Failed to load definitions: ${definitionsRes.statusText}`)
|
|
109
95
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
96
|
+
|
|
97
|
+
const openapi = await definitionsRes.json()
|
|
98
|
+
const articles = articlesRes.ok ? await articlesRes.json() : []
|
|
99
|
+
const config = configRes.ok ? await configRes.json() : {}
|
|
100
|
+
|
|
101
|
+
const payload = {
|
|
102
|
+
openapi,
|
|
103
|
+
articles,
|
|
104
|
+
api_server: config.api_server || '',
|
|
105
|
+
api_servers: config.api_servers || [],
|
|
106
|
+
auth_types: config.auth_types || ['bearer', 'api_key', 'basic', 'oauth2'],
|
|
107
|
+
ui_config: config.ui_config || {},
|
|
108
|
+
generated_at: new Date().toISOString()
|
|
119
109
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
110
|
+
|
|
111
|
+
setData(payload)
|
|
112
|
+
setLoading(false)
|
|
113
|
+
|
|
114
|
+
if (payload.openapi?.paths) {
|
|
115
|
+
const firstPath = Object.keys(payload.openapi.paths)[0]
|
|
116
|
+
const firstMethod = Object.keys(payload.openapi.paths[firstPath])[0]
|
|
117
|
+
setSelectedItem({ type: 'endpoint', path: firstPath, method: firstMethod })
|
|
118
|
+
} else if (payload.articles?.length > 0) {
|
|
119
|
+
setSelectedItem({ type: 'article', id: payload.articles[0].id })
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Failed to load ElderDocs data:', error)
|
|
123
|
+
setLoading(false)
|
|
125
124
|
}
|
|
126
125
|
}
|
|
126
|
+
|
|
127
|
+
loadData()
|
|
127
128
|
}, [])
|
|
128
129
|
|
|
129
130
|
if (loading) {
|
|
@@ -11,6 +11,8 @@ const ApiExplorer = ({ data, selectedItem }) => {
|
|
|
11
11
|
const [loading, setLoading] = useState(false)
|
|
12
12
|
const [response, setResponse] = useState(null)
|
|
13
13
|
const [isOpen, setIsOpen] = useState(true)
|
|
14
|
+
const [copied, setCopied] = useState(false)
|
|
15
|
+
const [showCurlPreview, setShowCurlPreview] = useState(false)
|
|
14
16
|
|
|
15
17
|
const authTypes = data?.auth_types || ['bearer', 'api_key', 'basic', 'oauth2']
|
|
16
18
|
const currentAuthType = authConfig?.type || 'bearer'
|
|
@@ -81,6 +83,85 @@ const ApiExplorer = ({ data, selectedItem }) => {
|
|
|
81
83
|
return url
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
const generateCurl = () => {
|
|
87
|
+
const url = buildUrl()
|
|
88
|
+
const method = selectedMethod.toUpperCase()
|
|
89
|
+
const curlParts = [`curl -X ${method}`]
|
|
90
|
+
|
|
91
|
+
// Add URL
|
|
92
|
+
curlParts.push(`"${url}"`)
|
|
93
|
+
|
|
94
|
+
// Add headers
|
|
95
|
+
const requestHeaders = {
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
...headers
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Apply authentication based on type
|
|
101
|
+
if (authConfig?.value) {
|
|
102
|
+
switch (authConfig.type) {
|
|
103
|
+
case 'bearer':
|
|
104
|
+
requestHeaders['Authorization'] = `Bearer ${authConfig.value}`
|
|
105
|
+
break
|
|
106
|
+
case 'api_key':
|
|
107
|
+
requestHeaders['X-API-Key'] = authConfig.value
|
|
108
|
+
break
|
|
109
|
+
case 'basic':
|
|
110
|
+
requestHeaders['Authorization'] = `Basic ${btoa(authConfig.value)}`
|
|
111
|
+
break
|
|
112
|
+
case 'oauth2':
|
|
113
|
+
requestHeaders['Authorization'] = `Bearer ${authConfig.value}`
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add all headers to curl command
|
|
119
|
+
Object.entries(requestHeaders).forEach(([key, value]) => {
|
|
120
|
+
curlParts.push(`-H "${key}: ${value}"`)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Add request body if present
|
|
124
|
+
if (['POST', 'PUT', 'PATCH'].includes(method) && body) {
|
|
125
|
+
try {
|
|
126
|
+
// Validate and format JSON
|
|
127
|
+
const jsonBody = JSON.parse(body)
|
|
128
|
+
const formattedBody = JSON.stringify(jsonBody)
|
|
129
|
+
curlParts.push(`-d '${formattedBody}'`)
|
|
130
|
+
} catch {
|
|
131
|
+
// If not valid JSON, add as-is
|
|
132
|
+
curlParts.push(`-d '${body}'`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return curlParts.join(' \\\n ')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const copyCurl = async () => {
|
|
140
|
+
try {
|
|
141
|
+
const curlCommand = generateCurl()
|
|
142
|
+
await navigator.clipboard.writeText(curlCommand)
|
|
143
|
+
setCopied(true)
|
|
144
|
+
setTimeout(() => setCopied(false), 2000)
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('Failed to copy:', err)
|
|
147
|
+
// Fallback for older browsers
|
|
148
|
+
const textArea = document.createElement('textarea')
|
|
149
|
+
textArea.value = generateCurl()
|
|
150
|
+
textArea.style.position = 'fixed'
|
|
151
|
+
textArea.style.opacity = '0'
|
|
152
|
+
document.body.appendChild(textArea)
|
|
153
|
+
textArea.select()
|
|
154
|
+
try {
|
|
155
|
+
document.execCommand('copy')
|
|
156
|
+
setCopied(true)
|
|
157
|
+
setTimeout(() => setCopied(false), 2000)
|
|
158
|
+
} catch (fallbackErr) {
|
|
159
|
+
console.error('Fallback copy failed:', fallbackErr)
|
|
160
|
+
}
|
|
161
|
+
document.body.removeChild(textArea)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
84
165
|
const executeRequest = async () => {
|
|
85
166
|
setLoading(true)
|
|
86
167
|
setResponse(null)
|
|
@@ -282,14 +363,46 @@ const ApiExplorer = ({ data, selectedItem }) => {
|
|
|
282
363
|
</div>
|
|
283
364
|
)}
|
|
284
365
|
|
|
285
|
-
<
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
366
|
+
<div className="reveal" style={{ animationDelay: '300ms' }}>
|
|
367
|
+
<button
|
|
368
|
+
onClick={() => setShowCurlPreview(!showCurlPreview)}
|
|
369
|
+
className="btn-secondary w-full mb-3 flex items-center justify-between"
|
|
370
|
+
>
|
|
371
|
+
<span className="flex items-center gap-2">
|
|
372
|
+
<span>💻</span>
|
|
373
|
+
<span>{showCurlPreview ? 'Hide' : 'Show'} cURL Command</span>
|
|
374
|
+
</span>
|
|
375
|
+
<span>{showCurlPreview ? '▲' : '▼'}</span>
|
|
376
|
+
</button>
|
|
377
|
+
|
|
378
|
+
{showCurlPreview && (
|
|
379
|
+
<div className="mb-3 surface border-2 border-black bg-white p-4">
|
|
380
|
+
<div className="flex items-center justify-between mb-2">
|
|
381
|
+
<p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 font-bold">cURL Command</p>
|
|
382
|
+
<button
|
|
383
|
+
onClick={copyCurl}
|
|
384
|
+
className="text-xs btn-secondary px-2 py-1"
|
|
385
|
+
title="Copy cURL command"
|
|
386
|
+
>
|
|
387
|
+
{copied ? '✓ Copied!' : '📋 Copy'}
|
|
388
|
+
</button>
|
|
389
|
+
</div>
|
|
390
|
+
<pre className="text-xs font-mono text-black overflow-x-auto whitespace-pre-wrap break-words">
|
|
391
|
+
{generateCurl()}
|
|
392
|
+
</pre>
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
<div className="reveal space-y-3" style={{ animationDelay: '320ms' }}>
|
|
398
|
+
<button
|
|
399
|
+
onClick={executeRequest}
|
|
400
|
+
disabled={loading}
|
|
401
|
+
className="btn-primary w-full"
|
|
402
|
+
>
|
|
403
|
+
{loading ? 'Sending...' : 'Send Request'}
|
|
404
|
+
</button>
|
|
405
|
+
</div>
|
|
293
406
|
|
|
294
407
|
{response && (
|
|
295
408
|
<div className="reveal" style={{ animationDelay: '320ms' }}>
|
|
@@ -100,16 +100,6 @@ const Sidebar = ({ data, activeView, setActiveView, selectedItem, setSelectedIte
|
|
|
100
100
|
<div className="p-6 border-b-0 border-black/10 relative" style={{ borderBottomWidth: '3px' }}>
|
|
101
101
|
<div className="pill text-[0.65rem] text-black mb-3 bg-white">ElderDocs</div>
|
|
102
102
|
<h1 className="font-black text-3xl tracking-tight text-black uppercase font-['Syne']">API Space</h1>
|
|
103
|
-
<div className="mt-4 mb-2">
|
|
104
|
-
<a
|
|
105
|
-
href="/docs/ui"
|
|
106
|
-
target="_blank"
|
|
107
|
-
rel="noopener noreferrer"
|
|
108
|
-
className="text-xs text-black/60 hover:text-black underline font-medium"
|
|
109
|
-
>
|
|
110
|
-
⚙️ Configure UI
|
|
111
|
-
</a>
|
|
112
|
-
</div>
|
|
113
103
|
<div className="flex gap-2 mt-6">
|
|
114
104
|
<button
|
|
115
105
|
onClick={() => {
|
data/lib/elder_docs/cli.rb
CHANGED
|
@@ -14,28 +14,21 @@ module ElderDocs
|
|
|
14
14
|
method_option :force_build, type: :boolean, default: false, desc: 'Force rebuilding frontend assets'
|
|
15
15
|
|
|
16
16
|
def deploy
|
|
17
|
-
say '🚀
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
unless File.exist?(definitions_path)
|
|
23
|
-
say "❌ Error: #{definitions_path} not found in current directory", :red
|
|
24
|
-
exit 1
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
unless File.exist?(articles_path)
|
|
28
|
-
say "⚠️ Warning: #{articles_path} not found. Creating empty articles file...", :yellow
|
|
29
|
-
File.write(articles_path, [].to_json)
|
|
30
|
-
end
|
|
17
|
+
say '🚀 Building ElderDocs frontend...', :green
|
|
18
|
+
say '📝 Note: definitions.json and articles.json are loaded dynamically at runtime', :cyan
|
|
19
|
+
say ' No need to rebuild when you update your API definitions!', :cyan
|
|
20
|
+
say ''
|
|
31
21
|
|
|
32
22
|
output_path = File.expand_path(options[:output] || default_output_path, Dir.pwd)
|
|
33
|
-
|
|
34
23
|
ElderDocs.config.output_path = output_path
|
|
35
24
|
|
|
25
|
+
# Reload config to get latest settings
|
|
26
|
+
ElderDocs.config.load_config_file
|
|
27
|
+
|
|
28
|
+
# Build frontend only (no data compilation needed)
|
|
36
29
|
generator = Generator.new(
|
|
37
|
-
definitions_path:
|
|
38
|
-
articles_path:
|
|
30
|
+
definitions_path: nil, # Not needed for dynamic mode
|
|
31
|
+
articles_path: nil, # Not needed for dynamic mode
|
|
39
32
|
output_path: output_path,
|
|
40
33
|
api_server: options[:api_server],
|
|
41
34
|
skip_build: options[:skip_build],
|
|
@@ -43,9 +36,12 @@ module ElderDocs
|
|
|
43
36
|
)
|
|
44
37
|
|
|
45
38
|
begin
|
|
46
|
-
generator.
|
|
47
|
-
say '✅
|
|
39
|
+
generator.build_frontend_only!
|
|
40
|
+
say '✅ Frontend built successfully!', :green
|
|
48
41
|
say "📦 Assets placed in: #{generator.output_path}", :cyan
|
|
42
|
+
say ''
|
|
43
|
+
say '✨ Your documentation is now live!', :green
|
|
44
|
+
say ' Edit definitions.json and articles.json - changes appear instantly!', :cyan
|
|
49
45
|
rescue Generator::ValidationError => e
|
|
50
46
|
say "❌ Validation Error: #{e.message}", :red
|
|
51
47
|
exit 1
|
data/lib/elder_docs/config.rb
CHANGED
|
@@ -5,7 +5,7 @@ require 'pathname'
|
|
|
5
5
|
|
|
6
6
|
module ElderDocs
|
|
7
7
|
class Config
|
|
8
|
-
attr_accessor :mount_path, :api_server, :auth_types, :ui_config, :admin_password, :output_path
|
|
8
|
+
attr_accessor :mount_path, :api_server, :auth_types, :ui_config, :admin_password, :output_path, :definitions_file, :articles_file
|
|
9
9
|
|
|
10
10
|
def initialize
|
|
11
11
|
@mount_path = nil
|
|
@@ -15,6 +15,8 @@ module ElderDocs
|
|
|
15
15
|
@admin_password = nil
|
|
16
16
|
@api_servers = []
|
|
17
17
|
@output_path = default_output_path
|
|
18
|
+
@definitions_file = nil
|
|
19
|
+
@articles_file = nil
|
|
18
20
|
load_config_file
|
|
19
21
|
end
|
|
20
22
|
|
|
@@ -40,6 +42,8 @@ module ElderDocs
|
|
|
40
42
|
@auth_types = config['auth_types'] if config['auth_types']
|
|
41
43
|
@ui_config = config['ui'] if config['ui'] # YAML uses 'ui' key, but we store as ui_config
|
|
42
44
|
@admin_password = config['admin_password'] if config['admin_password']
|
|
45
|
+
@definitions_file = config['definitions_file'] if config['definitions_file']
|
|
46
|
+
@articles_file = config['articles_file'] if config['articles_file']
|
|
43
47
|
if config['output_path']
|
|
44
48
|
@output_path = File.expand_path(config['output_path'], config_dir)
|
|
45
49
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module ElderDocs
|
|
6
|
+
class Engine
|
|
7
|
+
class ApiController < ActionController::API
|
|
8
|
+
include ActionController::MimeResponds
|
|
9
|
+
|
|
10
|
+
def definitions
|
|
11
|
+
definitions_path = find_definitions_file
|
|
12
|
+
if definitions_path && File.exist?(definitions_path)
|
|
13
|
+
render json: JSON.parse(File.read(definitions_path, encoding: 'UTF-8'))
|
|
14
|
+
else
|
|
15
|
+
render json: { error: 'definitions.json not found' }, status: :not_found
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def articles
|
|
20
|
+
articles_path = find_articles_file
|
|
21
|
+
if articles_path && File.exist?(articles_path)
|
|
22
|
+
render json: JSON.parse(File.read(articles_path, encoding: 'UTF-8'))
|
|
23
|
+
else
|
|
24
|
+
render json: []
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def config
|
|
29
|
+
# Reload config to get latest from elderdocs.yml
|
|
30
|
+
ElderDocs.config.load_config_file
|
|
31
|
+
|
|
32
|
+
config_data = {
|
|
33
|
+
api_server: ElderDocs.config.api_server,
|
|
34
|
+
api_servers: ElderDocs.config.api_servers || [],
|
|
35
|
+
auth_types: ElderDocs.config.auth_types || ['bearer', 'api_key', 'basic', 'oauth2'],
|
|
36
|
+
ui_config: ElderDocs.config.ui_config || {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render json: config_data
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def find_definitions_file
|
|
45
|
+
config = ElderDocs.config
|
|
46
|
+
|
|
47
|
+
# Check config file for definitions_file path
|
|
48
|
+
if config.respond_to?(:definitions_file) && config.definitions_file
|
|
49
|
+
config_path = find_config_file
|
|
50
|
+
if config_path
|
|
51
|
+
config_dir = File.dirname(config_path)
|
|
52
|
+
definitions_path = File.expand_path(config.definitions_file, config_dir)
|
|
53
|
+
return definitions_path if File.exist?(definitions_path)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Fallback to standard locations
|
|
58
|
+
paths = []
|
|
59
|
+
if defined?(Rails) && Rails.root
|
|
60
|
+
paths << Rails.root.join('definitions.json').to_s
|
|
61
|
+
paths << Rails.root.join('elderdocs', 'definitions.json').to_s
|
|
62
|
+
end
|
|
63
|
+
paths << File.join(Dir.pwd, 'definitions.json')
|
|
64
|
+
paths << File.join(Dir.pwd, 'elderdocs', 'definitions.json')
|
|
65
|
+
|
|
66
|
+
paths.find { |path| File.exist?(path) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def find_articles_file
|
|
70
|
+
config = ElderDocs.config
|
|
71
|
+
|
|
72
|
+
# Check config file for articles_file path
|
|
73
|
+
if config.respond_to?(:articles_file) && config.articles_file
|
|
74
|
+
config_path = find_config_file
|
|
75
|
+
if config_path
|
|
76
|
+
config_dir = File.dirname(config_path)
|
|
77
|
+
articles_path = File.expand_path(config.articles_file, config_dir)
|
|
78
|
+
return articles_path if File.exist?(articles_path)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Fallback to standard locations
|
|
83
|
+
paths = []
|
|
84
|
+
if defined?(Rails) && Rails.root
|
|
85
|
+
paths << Rails.root.join('articles.json').to_s
|
|
86
|
+
paths << Rails.root.join('elderdocs', 'articles.json').to_s
|
|
87
|
+
end
|
|
88
|
+
paths << File.join(Dir.pwd, 'articles.json')
|
|
89
|
+
paths << File.join(Dir.pwd, 'elderdocs', 'articles.json')
|
|
90
|
+
|
|
91
|
+
paths.find { |path| File.exist?(path) } || create_empty_articles_file
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def create_empty_articles_file
|
|
95
|
+
# Create empty articles.json if it doesn't exist
|
|
96
|
+
default_path = if defined?(Rails) && Rails.root
|
|
97
|
+
Rails.root.join('articles.json').to_s
|
|
98
|
+
else
|
|
99
|
+
File.join(Dir.pwd, 'articles.json')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
File.write(default_path, [].to_json) unless File.exist?(default_path)
|
|
103
|
+
default_path
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def find_config_file
|
|
107
|
+
config_paths = []
|
|
108
|
+
if defined?(Rails) && Rails.root
|
|
109
|
+
config_paths << Rails.root.join('elderdocs.yml').to_s
|
|
110
|
+
end
|
|
111
|
+
config_paths << File.join(Dir.pwd, 'elderdocs.yml')
|
|
112
|
+
config_paths.find { |path| File.exist?(path) }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
data/lib/elder_docs/engine.rb
CHANGED
|
@@ -8,15 +8,12 @@ module ElderDocs
|
|
|
8
8
|
|
|
9
9
|
# Define routes inline
|
|
10
10
|
routes do
|
|
11
|
-
#
|
|
12
|
-
get '/
|
|
13
|
-
get '/
|
|
14
|
-
|
|
15
|
-
post '/ui/logout', to: 'engine/ui_config#logout'
|
|
16
|
-
post '/ui/config', to: 'engine/ui_config#update'
|
|
11
|
+
# API endpoints for dynamic data
|
|
12
|
+
get '/api/definitions', to: 'engine/api#definitions'
|
|
13
|
+
get '/api/articles', to: 'engine/api#articles'
|
|
14
|
+
get '/api/config', to: 'engine/api#config'
|
|
17
15
|
|
|
18
|
-
# Serve
|
|
19
|
-
get '/data.js', to: 'engine/docs#show', defaults: { path: 'data.js' }
|
|
16
|
+
# Serve static assets
|
|
20
17
|
root 'engine/docs#show', defaults: { path: 'index.html' }
|
|
21
18
|
get '/*path', to: 'engine/docs#show', defaults: { path: 'index.html' }
|
|
22
19
|
end
|
|
@@ -40,8 +37,8 @@ module ElderDocs
|
|
|
40
37
|
end
|
|
41
38
|
end
|
|
42
39
|
|
|
43
|
-
# Load
|
|
44
|
-
require_relative 'engine/
|
|
40
|
+
# Load API controller
|
|
41
|
+
require_relative 'engine/api_controller'
|
|
45
42
|
|
|
46
43
|
# Create a simple controller to serve the static files
|
|
47
44
|
# Use API base to avoid CSRF protection
|
data/lib/elder_docs/generator.rb
CHANGED
|
@@ -40,6 +40,20 @@ module ElderDocs
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def build_frontend_only!
|
|
44
|
+
# Build frontend without data compilation (for dynamic mode)
|
|
45
|
+
# Check if assets already exist
|
|
46
|
+
assets_exist = File.exist?(File.join(output_path, 'index.html'))
|
|
47
|
+
|
|
48
|
+
if skip_build || (assets_exist && !force_build)
|
|
49
|
+
say '⏭️ Skipping frontend build (assets already exist)', :yellow
|
|
50
|
+
say '💡 Run with --force-build to rebuild', :cyan
|
|
51
|
+
else
|
|
52
|
+
build_frontend!
|
|
53
|
+
copy_assets!
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
43
57
|
private
|
|
44
58
|
|
|
45
59
|
def validate_definitions!
|
|
@@ -207,10 +221,12 @@ module ElderDocs
|
|
|
207
221
|
|
|
208
222
|
frontend_dir = File.expand_path(frontend_dir)
|
|
209
223
|
|
|
210
|
-
# Write compiled data to frontend public directory
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
224
|
+
# Write compiled data to frontend public directory (only if in static mode)
|
|
225
|
+
if @compiled_data_js
|
|
226
|
+
public_dir = File.join(frontend_dir, 'public')
|
|
227
|
+
FileUtils.mkdir_p(public_dir)
|
|
228
|
+
File.write(File.join(public_dir, 'data.js'), @compiled_data_js)
|
|
229
|
+
end
|
|
214
230
|
|
|
215
231
|
# Check if node_modules exists, if not, run npm install
|
|
216
232
|
node_modules = File.join(frontend_dir, 'node_modules')
|
data/lib/elder_docs/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: elder_docs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ElderDocs
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|
|
@@ -131,10 +131,11 @@ files:
|
|
|
131
131
|
- lib/elder_docs/cli.rb
|
|
132
132
|
- lib/elder_docs/config.rb
|
|
133
133
|
- lib/elder_docs/engine.rb
|
|
134
|
+
- lib/elder_docs/engine/api_controller.rb
|
|
134
135
|
- lib/elder_docs/engine/ui_config_controller.rb
|
|
135
136
|
- lib/elder_docs/generator.rb
|
|
136
137
|
- lib/elder_docs/version.rb
|
|
137
|
-
homepage: https://github.com/yashdave31/
|
|
138
|
+
homepage: https://github.com/yashdave31/elderdocs
|
|
138
139
|
licenses:
|
|
139
140
|
- MIT
|
|
140
141
|
metadata: {}
|