muno-claude-plugin 1.8.0 → 1.10.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,416 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Swagger/OpenAPI JSON을 Markdown 문서로 변환하는 스크립트
4
+
5
+ Usage:
6
+ python swagger-to-markdown.py <swagger_url> [output_file]
7
+ python swagger-to-markdown.py http://localhost:8080/v3/api-docs
8
+ python swagger-to-markdown.py http://localhost:8080/v3/api-docs api-docs.md
9
+
10
+ Requirements:
11
+ pip install requests
12
+ """
13
+
14
+ import sys
15
+ import json
16
+ import requests
17
+ from typing import Dict, List, Any, Optional
18
+ from datetime import datetime
19
+ from urllib.parse import urlparse
20
+
21
+ def fetch_swagger(url: str) -> Dict[str, Any]:
22
+ """Swagger JSON 가져오기"""
23
+ try:
24
+ response = requests.get(url, timeout=10)
25
+ response.raise_for_status()
26
+ return response.json()
27
+ except requests.exceptions.RequestException as e:
28
+ print(f"❌ Failed to fetch Swagger docs from {url}")
29
+ print(f"Error: {e}")
30
+ sys.exit(1)
31
+ except json.JSONDecodeError as e:
32
+ print(f"❌ Invalid JSON response from {url}")
33
+ print(f"Error: {e}")
34
+ sys.exit(1)
35
+
36
+ def get_base_url(swagger: Dict[str, Any]) -> str:
37
+ """Base URL 추출"""
38
+ # OpenAPI 3.0
39
+ if 'servers' in swagger and swagger['servers']:
40
+ return swagger['servers'][0].get('url', '')
41
+
42
+ # Swagger 2.0
43
+ if 'host' in swagger:
44
+ scheme = swagger.get('schemes', ['http'])[0]
45
+ host = swagger['host']
46
+ base_path = swagger.get('basePath', '')
47
+ return f"{scheme}://{host}{base_path}"
48
+
49
+ return ''
50
+
51
+ def resolve_ref(ref: str, swagger: Dict[str, Any]) -> Dict[str, Any]:
52
+ """$ref 참조 해석"""
53
+ if not ref.startswith('#/'):
54
+ return {}
55
+
56
+ parts = ref.replace('#/', '').split('/')
57
+ result = swagger
58
+
59
+ for part in parts:
60
+ if part in result:
61
+ result = result[part]
62
+ else:
63
+ return {}
64
+
65
+ return result
66
+
67
+ def get_schema_type(schema: Dict[str, Any], swagger: Dict[str, Any]) -> str:
68
+ """스키마 타입 문자열 생성"""
69
+ if '$ref' in schema:
70
+ ref_name = schema['$ref'].split('/')[-1]
71
+ return ref_name
72
+
73
+ schema_type = schema.get('type', 'object')
74
+
75
+ if schema_type == 'array':
76
+ items = schema.get('items', {})
77
+ item_type = get_schema_type(items, swagger)
78
+ return f"array<{item_type}>"
79
+
80
+ if 'enum' in schema:
81
+ return f"enum: [{', '.join(map(str, schema['enum']))}]"
82
+
83
+ format_type = schema.get('format')
84
+ if format_type:
85
+ return f"{schema_type} ({format_type})"
86
+
87
+ return schema_type
88
+
89
+ def generate_example(schema: Dict[str, Any], swagger: Dict[str, Any]) -> Any:
90
+ """스키마로부터 예제 데이터 생성"""
91
+ if 'example' in schema:
92
+ return schema['example']
93
+
94
+ if 'examples' in schema:
95
+ return list(schema['examples'].values())[0] if schema['examples'] else None
96
+
97
+ if '$ref' in schema:
98
+ resolved = resolve_ref(schema['$ref'], swagger)
99
+ return generate_example(resolved, swagger)
100
+
101
+ schema_type = schema.get('type', 'object')
102
+
103
+ if schema_type == 'object':
104
+ properties = schema.get('properties', {})
105
+ example = {}
106
+ for prop_name, prop_schema in properties.items():
107
+ example[prop_name] = generate_example(prop_schema, swagger)
108
+ return example
109
+
110
+ if schema_type == 'array':
111
+ items = schema.get('items', {})
112
+ item_example = generate_example(items, swagger)
113
+ return [item_example] if item_example is not None else []
114
+
115
+ if schema_type == 'string':
116
+ if schema.get('format') == 'date-time':
117
+ return "2025-01-01T00:00:00Z"
118
+ if schema.get('format') == 'date':
119
+ return "2025-01-01"
120
+ if 'enum' in schema:
121
+ return schema['enum'][0]
122
+ return "string"
123
+
124
+ if schema_type == 'integer':
125
+ return 1
126
+
127
+ if schema_type == 'number':
128
+ return 1.0
129
+
130
+ if schema_type == 'boolean':
131
+ return True
132
+
133
+ return None
134
+
135
+ def convert_to_markdown(swagger: Dict[str, Any], base_url: str) -> str:
136
+ """Swagger JSON을 Markdown으로 변환"""
137
+ info = swagger.get('info', {})
138
+ title = info.get('title', 'API Documentation')
139
+ version = info.get('version', '1.0.0')
140
+ description = info.get('description', '')
141
+
142
+ md = f"""# {title} API 문서
143
+
144
+ > Generated from Swagger/OpenAPI
145
+ > Version: {version}
146
+ > Generated Date: {datetime.now().strftime('%Y-%m-%d')}
147
+
148
+ ---
149
+
150
+ ## 📋 목차
151
+
152
+ - [개요](#개요)
153
+ - [서버 정보](#서버-정보)
154
+ - [인증](#인증)
155
+ - [API 엔드포인트](#api-엔드포인트)
156
+ - [데이터 모델](#데이터-모델)
157
+ - [에러 코드](#에러-코드)
158
+
159
+ ---
160
+
161
+ ## 개요
162
+
163
+ **서비스명**: {title}
164
+ **버전**: {version}
165
+ **설명**: {description if description else 'No description provided'}
166
+
167
+ ---
168
+
169
+ ## 서버 정보
170
+
171
+ **Base URL**: `{base_url}`
172
+
173
+ """
174
+
175
+ # 인증 정보
176
+ security_schemes = {}
177
+ if 'components' in swagger and 'securitySchemes' in swagger['components']:
178
+ security_schemes = swagger['components']['securitySchemes']
179
+ elif 'securityDefinitions' in swagger:
180
+ security_schemes = swagger['securityDefinitions']
181
+
182
+ if security_schemes:
183
+ md += "---\n\n## 인증\n\n"
184
+ for scheme_name, scheme_def in security_schemes.items():
185
+ scheme_type = scheme_def.get('type', '')
186
+ if scheme_type == 'http' and scheme_def.get('scheme') == 'bearer':
187
+ md += f"""### Bearer Token
188
+
189
+ 이 API는 Bearer Token 인증을 사용합니다.
190
+
191
+ **Header**:
192
+ ```
193
+ Authorization: Bearer {{token}}
194
+ ```
195
+
196
+ **Example**:
197
+ ```bash
198
+ curl -X GET "{base_url}/api/endpoint" \\
199
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
200
+ ```
201
+
202
+ """
203
+ elif scheme_type == 'apiKey':
204
+ key_name = scheme_def.get('name', 'X-API-Key')
205
+ in_location = scheme_def.get('in', 'header')
206
+ md += f"""### API Key
207
+
208
+ **{key_name}** ({in_location})
209
+
210
+ **Example**:
211
+ ```bash
212
+ curl -X GET "{base_url}/api/endpoint" \\
213
+ -H "{key_name}: your-api-key"
214
+ ```
215
+
216
+ """
217
+
218
+ # API 엔드포인트
219
+ md += "---\n\n## API 엔드포인트\n\n"
220
+
221
+ paths = swagger.get('paths', {})
222
+
223
+ # 태그별로 그룹핑
224
+ endpoints_by_tag = {}
225
+ for path, path_item in paths.items():
226
+ for method, operation in path_item.items():
227
+ if method.upper() not in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']:
228
+ continue
229
+
230
+ tags = operation.get('tags', ['Default'])
231
+ tag = tags[0] if tags else 'Default'
232
+
233
+ if tag not in endpoints_by_tag:
234
+ endpoints_by_tag[tag] = []
235
+
236
+ endpoints_by_tag[tag].append({
237
+ 'path': path,
238
+ 'method': method.upper(),
239
+ 'operation': operation
240
+ })
241
+
242
+ # 태그별로 문서 생성
243
+ for tag, endpoints in sorted(endpoints_by_tag.items()):
244
+ md += f"### {tag}\n\n"
245
+
246
+ for endpoint in endpoints:
247
+ path = endpoint['path']
248
+ method = endpoint['method']
249
+ operation = endpoint['operation']
250
+
251
+ summary = operation.get('summary', '')
252
+ description_text = operation.get('description', '')
253
+
254
+ md += f"#### `{method} {path}`\n\n"
255
+
256
+ if summary:
257
+ md += f"{summary}\n\n"
258
+
259
+ if description_text:
260
+ md += f"**Description**: {description_text}\n\n"
261
+
262
+ # Parameters
263
+ parameters = operation.get('parameters', [])
264
+ if parameters:
265
+ md += "**Parameters**:\n\n"
266
+ md += "| Name | In | Type | Required | Description |\n"
267
+ md += "|------|-----|------|----------|-------------|\n"
268
+
269
+ for param in parameters:
270
+ name = param.get('name', '')
271
+ in_location = param.get('in', '')
272
+ required = '✅' if param.get('required', False) else '❌'
273
+ param_description = param.get('description', '')
274
+
275
+ param_schema = param.get('schema', {})
276
+ param_type = get_schema_type(param_schema, swagger)
277
+
278
+ md += f"| {name} | {in_location} | {param_type} | {required} | {param_description} |\n"
279
+
280
+ md += "\n"
281
+
282
+ # Request Body
283
+ request_body = operation.get('requestBody', {})
284
+ if request_body:
285
+ content = request_body.get('content', {})
286
+ if 'application/json' in content:
287
+ schema = content['application/json'].get('schema', {})
288
+ example = generate_example(schema, swagger)
289
+
290
+ md += "**Request Body**:\n\n"
291
+ md += "```json\n"
292
+ md += json.dumps(example, indent=2, ensure_ascii=False)
293
+ md += "\n```\n\n"
294
+
295
+ # Responses
296
+ responses = operation.get('responses', {})
297
+ if responses:
298
+ md += "**Responses**:\n\n"
299
+
300
+ for status_code, response in sorted(responses.items()):
301
+ response_description = response.get('description', '')
302
+ md += f"- **{status_code}** {response_description}\n\n"
303
+
304
+ content = response.get('content', {})
305
+ if 'application/json' in content:
306
+ schema = content['application/json'].get('schema', {})
307
+ example = generate_example(schema, swagger)
308
+
309
+ md += " ```json\n"
310
+ md += " " + json.dumps(example, indent=2, ensure_ascii=False).replace("\n", "\n ")
311
+ md += "\n ```\n\n"
312
+
313
+ md += "---\n\n"
314
+
315
+ # 데이터 모델
316
+ md += "## 데이터 모델\n\n"
317
+
318
+ schemas = {}
319
+ if 'components' in swagger and 'schemas' in swagger['components']:
320
+ schemas = swagger['components']['schemas']
321
+ elif 'definitions' in swagger:
322
+ schemas = swagger['definitions']
323
+
324
+ for schema_name, schema_def in sorted(schemas.items()):
325
+ md += f"### {schema_name}\n\n"
326
+
327
+ description_text = schema_def.get('description', '')
328
+ if description_text:
329
+ md += f"{description_text}\n\n"
330
+
331
+ example = generate_example(schema_def, swagger)
332
+ md += "```json\n"
333
+ md += json.dumps(example, indent=2, ensure_ascii=False)
334
+ md += "\n```\n\n"
335
+
336
+ properties = schema_def.get('properties', {})
337
+ required_fields = schema_def.get('required', [])
338
+
339
+ if properties:
340
+ md += "**Properties**:\n\n"
341
+ md += "| Field | Type | Required | Description |\n"
342
+ md += "|-------|------|----------|-------------|\n"
343
+
344
+ for prop_name, prop_schema in properties.items():
345
+ prop_type = get_schema_type(prop_schema, swagger)
346
+ is_required = '✅' if prop_name in required_fields else '❌'
347
+ prop_description = prop_schema.get('description', '')
348
+
349
+ md += f"| {prop_name} | {prop_type} | {is_required} | {prop_description} |\n"
350
+
351
+ md += "\n"
352
+
353
+ md += "---\n\n"
354
+
355
+ # 에러 코드
356
+ md += """## 에러 코드
357
+
358
+ ### HTTP Status Codes
359
+
360
+ | Code | Name | Description |
361
+ |------|------|-------------|
362
+ | 200 | OK | Request succeeded |
363
+ | 201 | Created | Resource created successfully |
364
+ | 204 | No Content | Request succeeded with no response body |
365
+ | 400 | Bad Request | Invalid request parameters |
366
+ | 401 | Unauthorized | Missing or invalid authentication |
367
+ | 403 | Forbidden | Insufficient permissions |
368
+ | 404 | Not Found | Resource not found |
369
+ | 500 | Internal Server Error | Server error |
370
+
371
+ ---
372
+
373
+ ## 링크
374
+
375
+ - **Swagger UI**: {base_url.replace('/v3/api-docs', '/swagger-ui.html')}
376
+ - **API Docs JSON**: {base_url}
377
+
378
+ ---
379
+
380
+ > 📝 이 문서는 Swagger/OpenAPI 명세로부터 자동 생성되었습니다.
381
+ > 마지막 업데이트: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
382
+ """
383
+
384
+ return md
385
+
386
+ def main():
387
+ """메인 함수"""
388
+ if len(sys.argv) < 2:
389
+ print("Usage: python swagger-to-markdown.py <swagger_url> [output_file]")
390
+ print("Example: python swagger-to-markdown.py http://localhost:8080/v3/api-docs")
391
+ sys.exit(1)
392
+
393
+ swagger_url = sys.argv[1]
394
+ output_file = sys.argv[2] if len(sys.argv) > 2 else None
395
+
396
+ print(f"🔍 Fetching Swagger docs from: {swagger_url}")
397
+ swagger = fetch_swagger(swagger_url)
398
+
399
+ base_url = get_base_url(swagger)
400
+ if not base_url:
401
+ # URL에서 base path 추출
402
+ parsed = urlparse(swagger_url)
403
+ base_url = f"{parsed.scheme}://{parsed.netloc}"
404
+
405
+ print(f"📝 Generating Markdown documentation...")
406
+ markdown = convert_to_markdown(swagger, base_url)
407
+
408
+ if output_file:
409
+ with open(output_file, 'w', encoding='utf-8') as f:
410
+ f.write(markdown)
411
+ print(f"✅ Documentation saved to: {output_file}")
412
+ else:
413
+ print(markdown)
414
+
415
+ if __name__ == '__main__':
416
+ main()