aspera-cli 4.26.1 → 4.27.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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +300 -22
  4. data/CONTRIBUTING.md +100 -12
  5. data/bin/ascli +5 -5
  6. data/docs/README.md +10757 -0
  7. data/docs/test-mcp-with-ai.md +202 -0
  8. data/lib/aspera/agent/base.rb +20 -6
  9. data/lib/aspera/agent/connect.rb +53 -16
  10. data/lib/aspera/agent/desktop.rb +52 -17
  11. data/lib/aspera/agent/direct.rb +27 -25
  12. data/lib/aspera/agent/factory.rb +4 -0
  13. data/lib/aspera/agent/httpgw.rb +2 -2
  14. data/lib/aspera/agent/node.rb +34 -2
  15. data/lib/aspera/agent/transferd.rb +39 -3
  16. data/lib/aspera/api/alee.rb +1 -1
  17. data/lib/aspera/api/aoc.rb +56 -51
  18. data/lib/aspera/api/cos_node.rb +2 -2
  19. data/lib/aspera/api/faspex.rb +22 -20
  20. data/lib/aspera/api/httpgw.rb +4 -4
  21. data/lib/aspera/api/node.rb +33 -30
  22. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  23. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  24. data/lib/aspera/ascmd.rb +25 -23
  25. data/lib/aspera/ascp/installation.rb +43 -40
  26. data/lib/aspera/ascp/management.rb +2 -3
  27. data/lib/aspera/cli/ascp_actions.rb +155 -0
  28. data/lib/aspera/cli/async_transfer_store.rb +81 -0
  29. data/lib/aspera/cli/bootstrapper.rb +197 -0
  30. data/lib/aspera/cli/command_registry.rb +145 -0
  31. data/lib/aspera/cli/command_spec.rb +138 -0
  32. data/lib/aspera/cli/context.rb +71 -0
  33. data/lib/aspera/cli/error.rb +16 -0
  34. data/lib/aspera/cli/extended_value.rb +5 -3
  35. data/lib/aspera/cli/formatter.rb +122 -209
  36. data/lib/aspera/cli/gem_checker.rb +65 -0
  37. data/lib/aspera/cli/hints.rb +2 -2
  38. data/lib/aspera/cli/http.rb +218 -0
  39. data/lib/aspera/cli/info.rb +2 -0
  40. data/lib/aspera/cli/mailer.rb +97 -0
  41. data/lib/aspera/cli/mcp_tool.rb +198 -0
  42. data/lib/aspera/cli/options.schema.yaml +364 -7
  43. data/lib/aspera/cli/{manager.rb → parser.rb} +337 -144
  44. data/lib/aspera/cli/plugins/alee.rb +20 -22
  45. data/lib/aspera/cli/plugins/aoc.rb +1173 -849
  46. data/lib/aspera/cli/plugins/ats.rb +200 -161
  47. data/lib/aspera/cli/plugins/base.rb +470 -107
  48. data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
  49. data/lib/aspera/cli/plugins/config.rb +434 -1032
  50. data/lib/aspera/cli/plugins/console.rb +106 -64
  51. data/lib/aspera/cli/plugins/cos.rb +44 -32
  52. data/lib/aspera/cli/plugins/factory.rb +7 -4
  53. data/lib/aspera/cli/plugins/faspex.rb +296 -259
  54. data/lib/aspera/cli/plugins/faspex5.rb +592 -344
  55. data/lib/aspera/cli/plugins/faspio.rb +49 -51
  56. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  57. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  58. data/lib/aspera/cli/plugins/node.rb +1001 -797
  59. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  60. data/lib/aspera/cli/plugins/orchestrator.rb +111 -139
  61. data/lib/aspera/cli/plugins/preview.rb +183 -142
  62. data/lib/aspera/cli/plugins/server.rb +125 -80
  63. data/lib/aspera/cli/plugins/shares.rb +301 -107
  64. data/lib/aspera/cli/preset_actions.rb +139 -0
  65. data/lib/aspera/cli/preset_manager.rb +236 -0
  66. data/lib/aspera/cli/result.rb +360 -0
  67. data/lib/aspera/cli/runner.rb +473 -0
  68. data/lib/aspera/cli/secret_finder.rb +40 -0
  69. data/lib/aspera/cli/special_values.rb +1 -0
  70. data/lib/aspera/cli/sync_actions.rb +84 -56
  71. data/lib/aspera/cli/terminal_formatter.rb +65 -0
  72. data/lib/aspera/cli/transfer_actions.rb +83 -0
  73. data/lib/aspera/cli/transfer_agent.rb +116 -51
  74. data/lib/aspera/cli/transfer_progress.rb +9 -9
  75. data/lib/aspera/cli/vault_manager.rb +57 -0
  76. data/lib/aspera/cli/version.rb +1 -1
  77. data/lib/aspera/cli/wizard.rb +26 -26
  78. data/lib/aspera/command_line_builder.rb +23 -23
  79. data/lib/aspera/data_repository.rb +6 -1
  80. data/lib/aspera/dot_container.rb +5 -5
  81. data/lib/aspera/environment.rb +39 -13
  82. data/lib/aspera/exec_spec.rb +13 -0
  83. data/lib/aspera/faspex_gw.rb +2 -2
  84. data/lib/aspera/faspex_postproc.rb +4 -3
  85. data/lib/aspera/graphql.rb +35 -0
  86. data/lib/aspera/hash_ext.rb +6 -0
  87. data/lib/aspera/json_rpc/client.rb +62 -0
  88. data/lib/aspera/json_rpc/version.rb +7 -0
  89. data/lib/aspera/keychain/base.rb +1 -1
  90. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  91. data/lib/aspera/keychain/factory.rb +4 -4
  92. data/lib/aspera/keychain/macos_security.rb +4 -4
  93. data/lib/aspera/link_header.rb +82 -0
  94. data/lib/aspera/log.rb +23 -5
  95. data/lib/aspera/markdown.rb +85 -2
  96. data/lib/aspera/nagios.rb +2 -2
  97. data/lib/aspera/node_simulator.rb +3 -1
  98. data/lib/aspera/oauth/base.rb +10 -10
  99. data/lib/aspera/oauth/boot.rb +43 -0
  100. data/lib/aspera/oauth/factory.rb +41 -15
  101. data/lib/aspera/oauth/jwt.rb +4 -4
  102. data/lib/aspera/oauth/web.rb +4 -4
  103. data/lib/aspera/oauth.rb +1 -0
  104. data/lib/aspera/persistency_action_once.rb +10 -10
  105. data/lib/aspera/persistency_folder.rb +10 -3
  106. data/lib/aspera/preview/file_types.rb +7 -3
  107. data/lib/aspera/preview/generator.rb +3 -3
  108. data/lib/aspera/preview/utils.rb +1 -1
  109. data/lib/aspera/products/connect.rb +4 -1
  110. data/lib/aspera/products/other.rb +1 -1
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -9
  113. data/lib/aspera/rest.rb +18 -40
  114. data/lib/aspera/rest_error_analyzer.rb +7 -3
  115. data/lib/aspera/rest_errors_aspera.rb +0 -10
  116. data/lib/aspera/rest_list.rb +13 -6
  117. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +14 -4
  118. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +2730 -648
  119. data/lib/aspera/schema/async_tables.yaml +361 -0
  120. data/lib/aspera/schema/documentation.rb +57 -26
  121. data/lib/aspera/schema/reader.rb +60 -11
  122. data/lib/aspera/schema/registry.rb +50 -10
  123. data/lib/aspera/secret_hider.rb +11 -2
  124. data/lib/aspera/ssl.rb +4 -4
  125. data/lib/aspera/sync/conf.schema.yaml +2 -2
  126. data/lib/aspera/sync/database.rb +14 -10
  127. data/lib/aspera/sync/operations.rb +15 -18
  128. data/lib/aspera/temp_file_manager.rb +5 -1
  129. data/lib/aspera/timer_limiter.rb +1 -1
  130. data/lib/aspera/transfer/faux_file.rb +24 -11
  131. data/lib/aspera/transfer/parameters.rb +27 -25
  132. data/lib/aspera/transfer/result.rb +74 -0
  133. data/lib/aspera/transfer/resumer.rb +7 -7
  134. data/lib/aspera/transfer/spec.rb +18 -1
  135. data/lib/aspera/transfer/spec.schema.yaml +26 -8
  136. data/lib/aspera/transfer/uri.rb +1 -1
  137. data/lib/aspera/uri_reader.rb +2 -2
  138. data/lib/aspera/web_auth.rb +2 -2
  139. data/lib/aspera/web_server_simple.rb +8 -5
  140. data/lib/aspera/yaml.rb +2 -1
  141. data.tar.gz.sig +0 -0
  142. metadata +45 -16
  143. metadata.gz.sig +0 -0
  144. data/lib/aspera/cli/main.rb +0 -480
  145. data/lib/aspera/json_rpc.rb +0 -51
@@ -0,0 +1,361 @@
1
+ # Schema for the SQLite tables inside the Async snap database (snap.db).
2
+ # Sources:
3
+ # include/async/session/Sqlite3_SSDB.h (SYNC_SQLITE3_META_SCHEMA, SYNC_SQLITE3_COUNTERS_SCHEMA, SYNC_SQLITE3_SCHEMA_1_0)
4
+ # include/async/common/Sync_types.h (SSSTATE values)
5
+ # Verified against a live snap.db using: ascli config sync admin overview <path>
6
+ $schema: https://json-schema.org/draft/2020-12/schema
7
+ $id: https://github.com/IBM/aspera-cli/tree/main/lib/aspera/schema/async_tables.yaml
8
+ title: Async snap database tables
9
+ description: Schemas for the SQLite tables stored inside the Async snap database (`snap.db`).
10
+ type: object
11
+ properties:
12
+ meta:
13
+ title: sync_snapmeta_table
14
+ description: One row per session, written at start and updated at stop.
15
+ type: object
16
+ properties:
17
+ dbversion:
18
+ description: Database schema version.
19
+ type: string
20
+ pairname:
21
+ description: Sync session name.
22
+ type: string
23
+ ldir:
24
+ description: Local directory path.
25
+ type: string
26
+ rhost:
27
+ description: Remote host.
28
+ type: string
29
+ rdir:
30
+ description: Remote directory path.
31
+ type: string
32
+ direction:
33
+ description: Sync direction.
34
+ type: string
35
+ enum:
36
+ - push
37
+ - pull
38
+ - bidi
39
+ inode_format:
40
+ description: Inode format used by the session.
41
+ type: string
42
+ checksum:
43
+ description: Checksum algorithm.
44
+ type: string
45
+ enum:
46
+ - sha1
47
+ - md5
48
+ - sha1_sparse
49
+ - md5_sparse
50
+ - none
51
+ start_time:
52
+ description: Session start timestamp.
53
+ type: string
54
+ format: date-time
55
+ stop_time:
56
+ description: Session stop timestamp (written when session ends).
57
+ type: string
58
+ format: date-time
59
+ condition:
60
+ description: Last known session condition.
61
+ type: string
62
+ enum:
63
+ - running
64
+ - stopped
65
+ scn_true_val:
66
+ description: Scan counter true value (internal).
67
+ type: integer
68
+ sync_point:
69
+ description: Synchronization point sequence number.
70
+ type: integer
71
+ max_recid:
72
+ description: Highest record ID allocated in the snapshot table.
73
+ type: integer
74
+ sync_uuid:
75
+ description: UUID identifying this sync pair.
76
+ type: string
77
+ cip_true_val:
78
+ description: Candidate-in-progress counter true value (internal).
79
+ type: integer
80
+ exclude_dirs_older_than:
81
+ description: Exclude directories with modification time older than this epoch value (0 = disabled).
82
+ type: integer
83
+ path_synced:
84
+ description: Number of paths successfully synchronized.
85
+ type: integer
86
+ byte_synced:
87
+ description: Number of bytes successfully synchronized.
88
+ type: integer
89
+ filters:
90
+ description: Active include/exclude filter patterns.
91
+ type: string
92
+ path_peerxfer:
93
+ description: Number of paths transferred by the peer.
94
+ type: integer
95
+ byte_peerxfer:
96
+ description: Number of bytes transferred by the peer.
97
+ type: integer
98
+ path_xferres:
99
+ description: Number of paths for which transfer was resumed.
100
+ type: integer
101
+ byte_xferres:
102
+ description: Number of bytes for which transfer was resumed.
103
+ type: integer
104
+ file_synced:
105
+ description: Number of files successfully synchronized.
106
+ type: integer
107
+ file_peerxfer:
108
+ description: Number of files transferred by the peer.
109
+ type: integer
110
+ file_xferres:
111
+ description: Number of files for which transfer was resumed.
112
+ type: integer
113
+ ascp_sids:
114
+ description: Last known FASP (ascp) session IDs.
115
+ type: string
116
+ counters:
117
+ title: sync_snap_counters_table
118
+ description: One row, updated live during the session with path and file counts per state.
119
+ type: object
120
+ properties:
121
+ syncd:
122
+ description: Number of paths in the `Syncd` state.
123
+ type: integer
124
+ conflict:
125
+ description: Number of paths in the `Conflict` state.
126
+ type: integer
127
+ potential_conflict:
128
+ description: Number of paths in the `Potential conflict` state.
129
+ type: integer
130
+ error:
131
+ description: Number of paths in the `Error` state.
132
+ type: integer
133
+ pending:
134
+ description: Number of paths still pending synchronization.
135
+ type: integer
136
+ unused:
137
+ description: Reserved (unused counter slot).
138
+ type: integer
139
+ files_syncd:
140
+ description: Number of files in the `Syncd` state.
141
+ type: integer
142
+ files_conflict:
143
+ description: Number of files in the `Conflict` state.
144
+ type: integer
145
+ files_potential_conflict:
146
+ description: Number of files in the `Potential conflict` state.
147
+ type: integer
148
+ files_error:
149
+ description: Number of files in the `Error` state.
150
+ type: integer
151
+ files_pending:
152
+ description: Number of files still pending synchronization.
153
+ type: integer
154
+ file_info:
155
+ title: sync_snapdb_table
156
+ description: One row per tracked file or directory. Primary source for the `file_info` command.
157
+ type: object
158
+ properties:
159
+ record_id:
160
+ description: Primary key (auto-increment).
161
+ type: integer
162
+ nodeid:
163
+ description: Local filesystem node ID.
164
+ type: integer
165
+ peer_rid:
166
+ description: Peer record ID for this entry.
167
+ type: integer
168
+ state:
169
+ description: |
170
+ Sync state of the entry.
171
+
172
+ | Value | Name |
173
+ |-------|-------------------|
174
+ | 0 | Nil |
175
+ | 1–18 | Pending (variant) |
176
+ | 19 | Syncd |
177
+ | 20 | Error |
178
+ | 21 | Conflict |
179
+ | 22 | PotentialConflict |
180
+ | 23–24 | Pending (variant) |
181
+ type: integer
182
+ version:
183
+ description: Schema version field (not used).
184
+ type: integer
185
+ c_meta_type:
186
+ description: Current metadata — file type (file, directory, …).
187
+ type: integer
188
+ c_meta_parentrid:
189
+ description: Current metadata — parent record ID.
190
+ type: integer
191
+ c_meta_path:
192
+ description: Current metadata — file path as known to the snapshot.
193
+ type: string
194
+ c_meta_attrs:
195
+ description: Current metadata — file attributes.
196
+ type: string
197
+ c_meta_uid:
198
+ description: Current metadata — user ID (Unix).
199
+ type: integer
200
+ c_meta_gid:
201
+ description: Current metadata — group ID (Unix).
202
+ type: integer
203
+ c_meta_mtime:
204
+ description: Current metadata — modification time (epoch).
205
+ type: integer
206
+ c_meta_atime:
207
+ description: Current metadata — access time (epoch).
208
+ type: integer
209
+ c_meta_ctime:
210
+ description: Current metadata — change time (epoch).
211
+ type: integer
212
+ c_meta_xattrs:
213
+ description: Current metadata — extended attributes (not used).
214
+ type: integer
215
+ c_meta_xattrs_cksum:
216
+ description: Current metadata — checksum of extended attributes.
217
+ type: string
218
+ c_meta_ol_legal_hold:
219
+ description: Current metadata — object lock legal hold flag.
220
+ type: integer
221
+ c_meta_ol_mode:
222
+ description: Current metadata — object lock mode.
223
+ type: integer
224
+ c_meta_ol_retain_until:
225
+ description: Current metadata — object lock retain-until date (epoch).
226
+ type: integer
227
+ c_meta_object_metadata:
228
+ description: Current metadata — object metadata (JSON).
229
+ type: string
230
+ c_cont_size:
231
+ description: Current content — file size in bytes.
232
+ type: integer
233
+ c_cont_csum:
234
+ description: Current content — checksum.
235
+ type: string
236
+ c_cont_mtime:
237
+ description: Current content — modification time (epoch).
238
+ type: integer
239
+ c_cont_timestamp:
240
+ description: Current content — internal timestamp.
241
+ type: integer
242
+ p_cont_size:
243
+ description: Pending content — file size in bytes.
244
+ type: integer
245
+ p_cont_csum:
246
+ description: Pending content — checksum.
247
+ type: string
248
+ p_cont_mtime:
249
+ description: Pending content — modification time (epoch).
250
+ type: integer
251
+ p_cont_timestamp:
252
+ description: Pending content — internal timestamp.
253
+ type: integer
254
+ f_meta_type:
255
+ description: Filesystem metadata — file type.
256
+ type: integer
257
+ f_meta_parentrid:
258
+ description: Filesystem metadata — parent record ID.
259
+ type: integer
260
+ f_meta_path:
261
+ description: Filesystem metadata — file path as seen on disk (displayed by `file_info`).
262
+ type: string
263
+ f_meta_attrs:
264
+ description: Filesystem metadata — file attributes.
265
+ type: string
266
+ f_meta_uid:
267
+ description: Filesystem metadata — user ID (Unix).
268
+ type: integer
269
+ f_meta_gid:
270
+ description: Filesystem metadata — group ID (Unix).
271
+ type: integer
272
+ f_meta_mtime:
273
+ description: Filesystem metadata — modification time (epoch).
274
+ type: integer
275
+ f_meta_atime:
276
+ description: Filesystem metadata — access time (epoch).
277
+ type: integer
278
+ f_meta_ctime:
279
+ description: Filesystem metadata — change time (epoch).
280
+ type: integer
281
+ f_meta_xattrs:
282
+ description: Filesystem metadata — extended attributes (not used).
283
+ type: integer
284
+ f_meta_xattrs_cksum:
285
+ description: Filesystem metadata — checksum of extended attributes.
286
+ type: string
287
+ f_meta_ol_legal_hold:
288
+ description: Filesystem metadata — object lock legal hold flag.
289
+ type: integer
290
+ f_meta_ol_mode:
291
+ description: Filesystem metadata — object lock mode.
292
+ type: integer
293
+ f_meta_ol_retain_until:
294
+ description: Filesystem metadata — object lock retain-until date (epoch).
295
+ type: integer
296
+ f_meta_object_metadata:
297
+ description: Filesystem metadata — object metadata (JSON).
298
+ type: string
299
+ f_cont_size:
300
+ description: Filesystem content — file size in bytes.
301
+ type: integer
302
+ f_cont_csum:
303
+ description: Filesystem content — checksum.
304
+ type: string
305
+ f_cont_mtime:
306
+ description: Filesystem content — modification time (epoch).
307
+ type: integer
308
+ f_cont_timestamp:
309
+ description: Filesystem content — internal timestamp.
310
+ type: integer
311
+ pcr_reqno:
312
+ description: Peer content request sequence number.
313
+ type: integer
314
+ pp_cont_size:
315
+ description: Peer pending content — file size in bytes.
316
+ type: integer
317
+ pp_cont_csum:
318
+ description: Peer pending content — checksum.
319
+ type: string
320
+ pp_cont_mtime:
321
+ description: Peer pending content — modification time (epoch).
322
+ type: integer
323
+ pp_cont_timestamp:
324
+ description: Peer pending content — internal timestamp.
325
+ type: integer
326
+ tobe_deleted:
327
+ description: Set to 1 when the record is flagged for deletion (garbage collection pending).
328
+ type: integer
329
+ scn_not_visited:
330
+ description: Set to 1 when the scanner has not yet visited this entry in the current scan pass.
331
+ type: integer
332
+ scn_required:
333
+ description: Set to 1 when a rescan of this entry is required.
334
+ type: integer
335
+ scn_completed:
336
+ description: Set to 1 when the scan of this directory entry has completed.
337
+ type: integer
338
+ candidate_in_prog:
339
+ description: Set to 1 when this record is currently in the candidate transfer list.
340
+ type: integer
341
+ meta_lastreq_no:
342
+ description: Sequence number of the last metadata request sent.
343
+ type: integer
344
+ meta_lastresp_no:
345
+ description: Sequence number of the last metadata response received.
346
+ type: integer
347
+ commit_version:
348
+ description: Local commit version for this entry.
349
+ type: integer
350
+ peer_commit_version:
351
+ description: Peer commit version for this entry.
352
+ type: integer
353
+ peer_nodeid:
354
+ description: Peer filesystem node ID.
355
+ type: integer
356
+ recursive_mtime:
357
+ description: Most recent modification time across all descendants (directories only).
358
+ type: integer
359
+ message:
360
+ description: Error or status message associated with this entry.
361
+ type: string
@@ -7,6 +7,15 @@ module Aspera
7
7
  module Schema
8
8
  # Generate documentation from Schema, for Transfer Spec, or async Conf spec
9
9
  class Documentation
10
+ # Map JSON Schema type names to user-friendly display names
11
+ JSON_TYPE_TO_DOC = {
12
+ 'string' => 'String',
13
+ 'integer' => 'Integer',
14
+ 'number' => 'Number',
15
+ 'boolean' => 'Bool',
16
+ 'array' => 'Array',
17
+ 'object' => 'Hash'
18
+ }.freeze
10
19
  # @param formatter [Cli::Formatter] Formatter instance with methods: markdown_text, tick, check_row
11
20
  # @param schema [Reader]
12
21
  # @param include_option [Boolean] `true`: include CLI options (switches, env vars) in descriptions
@@ -18,25 +27,27 @@ module Aspera
18
27
  @include_option = include_option
19
28
  @agent_columns = agent_columns
20
29
  @code_highlight = code_highlight
21
- @columns = %i[name type description]
22
- @columns.insert(-2, *Agent::Factory::ALL.values.map{ |i| i[:short]}.sort) if @agent_columns
23
- # @type [Array<Hash<Symbol,String>>]
24
- @rows = []
30
+ @columns = %w[name type description]
31
+ @columns.insert(-2, *Agent::Factory::ALL.values.map{ |i| i[:short].to_s}.sort) if @agent_columns
32
+ # Sections: each entry is {header: row_or_nil, rows: []}
33
+ # A flat schema produces a single section with no header.
34
+ @sections = [{header: nil, rows: []}]
25
35
  end
26
36
 
27
37
  def rows
28
- @rows.sort_by{ |i| i[:name]}
38
+ @sections.flat_map do |section|
39
+ sorted = section[:rows].sort_by{ |i| i['name']}
40
+ section[:header] ? [section[:header]] + sorted : sorted
41
+ end
29
42
  end
30
43
 
31
44
  # @return [Array<String>]
32
- def columns
33
- @columns.map(&:to_s)
34
- end
45
+ attr_reader :columns
35
46
 
36
- # First row is the titles
47
+ # First row is the titles (for Markdown table generation)
37
48
  # @return [Array<Array<String>>]
38
49
  def table
39
- [@columns.map(&:to_s)] + @rows.sort_by{ |i| i[:name]}.map{ |row| @columns.map{ |field| row[field]}}
50
+ [@columns] + rows.map{ |row| @columns.map{ |field| row[field]}}
40
51
  end
41
52
 
42
53
  # Generate a documentation table from a JSON schema for transfer specifications
@@ -49,37 +60,57 @@ module Aspera
49
60
  def build(schema = nil)
50
61
  code = @code_highlight ? ->(c){"`#{c}`"} : ->(c){c}
51
62
  schema ||= @schema
52
- schema.each_property do |property_schema, _name, property_full_name|
63
+ render_title = ->(title){title.gsub(Markdown::FORMATS){@formatter.markdown_text(Regexp.last_match)}}
64
+ on_variant = ->(variant_reader, discriminant_property, discriminant_value) do
65
+ title = variant_reader.current['title'] || variant_reader.current['description']
66
+ header =
67
+ if discriminant_property && discriminant_value
68
+ desc = render_title.call("`#{discriminant_value}`")
69
+ desc += ": #{render_title.call(title)}" if title
70
+ @formatter.check_row({
71
+ 'name' => render_title.call("**#{discriminant_property}**"),
72
+ 'type' => code.call(JSON_TYPE_TO_DOC['string']),
73
+ 'description' => desc
74
+ })
75
+ elsif title
76
+ @formatter.check_row({'name' => "**#{render_title.call(title)}**", 'type' => '&nbsp;', 'description' => '&nbsp;'})
77
+ end
78
+ @sections.push({header: header, rows: []})
79
+ end
80
+ schema.each_property(on_variant: on_variant) do |property_schema, _name, property_full_name|
53
81
  node = property_schema.current
54
82
  # Manual table
83
+ item_type = JSON_TYPE_TO_DOC.fetch(node['type'], node['type'])
84
+ item_type = "#{item_type}[#{JSON_TYPE_TO_DOC.fetch(node.dig('items', 'type'), node.dig('items', 'type'))}]" if node['type'].eql?('array') && node.dig('items', 'type').is_a?(String)
55
85
  item = {
56
- name: code.call(property_full_name),
57
- type: code.call(node['type']),
58
- description: []
86
+ 'name' => code.call(property_full_name),
87
+ 'type' => code.call(item_type),
88
+ 'description' => []
59
89
  }
60
90
  # Render Markdown formatting and split lines
61
- item[:description] =
91
+ item['description'] =
62
92
  node['description']
63
93
  .gsub(Markdown::FORMATS){@formatter.markdown_text(Regexp.last_match)}
64
94
  .split("\n") if node.key?('description')
65
- item[:description].unshift("DEPRECATED: #{node['x-deprecation']}") if node.key?('x-deprecation')
95
+ item['description'].unshift("DEPRECATED: #{node['x-deprecation']}") if node.key?('x-deprecation')
66
96
  # Add flags for supported agents in doc
67
97
  agents = []
68
98
  Agent::Factory::ALL.each_key do |sym|
69
99
  agents.push(sym) if node['x-agents'].nil? || node['x-agents'].include?(sym.to_s)
70
100
  end
71
- Aspera.assert(agents.include?(:direct)){"#{name}: x-cli-option requires agent direct (or nil)"} if node['x-cli-option']
101
+ Aspera.assert(agents.include?(:direct)){"#{property_full_name}: x-cli-option requires agent direct (or nil)"} if node['x-cli-option']
72
102
  if @agent_columns
73
103
  Agent::Factory::ALL.each do |sym, names|
74
- item[names[:short]] = @formatter.tick(agents.include?(sym))
104
+ item[names[:short].to_s] = @formatter.tick(agents.include?(sym))
75
105
  end
76
106
  else
77
- item[:description].push("(#{agents.map{ |i| Agent::Factory::ALL[i][:short].to_s.upcase}.sort.join(', ')})") unless agents.length.eql?(Agent::Factory::ALL.length)
107
+ item['description'].push("(#{agents.map{ |i| Agent::Factory::ALL[i][:short].to_s.upcase}.sort.join(', ')})") unless agents.length.eql?(Agent::Factory::ALL.length)
78
108
  end
79
109
  # Only keep lines that are usable in supported agents
80
- next false if agents.empty?
81
- item[:description].push("Allowed values: #{node['enum'].map{ |v| @formatter.markdown_text("`#{v}`")}.join(', ')}.") if node.key?('enum')
82
- item[:description].push("Default: #{code.call(node['default'])}.") if node.key?('default')
110
+ next if agents.empty?
111
+ item['description'].push("Allowed values: #{node['enum'].map{ |v| @formatter.markdown_text("`#{v}`")}.join(', ')}.") if node.key?('enum')
112
+ item['description'].push("Default: #{@formatter.markdown_text("`#{node['default']}`")}.") if node.key?('default')
113
+ item['description'].push("Example: #{@formatter.markdown_text("`#{node['example']}`")}.") if node.key?('example')
83
114
  if @include_option
84
115
  envvar_prefix = ''
85
116
  cli_option =
@@ -87,18 +118,18 @@ module Aspera
87
118
  envvar_prefix = 'env:'
88
119
  node['x-cli-envvar']
89
120
  elsif node['x-cli-switch']
90
- node['x-cli-option']
121
+ false_part = node.key?('x-cli-false') ? " / #{node['x-cli-false']}" : ''
122
+ "#{node['x-cli-option']}#{false_part}"
91
123
  elsif node['x-cli-option']
92
124
  arg_type = node.key?('enum') ? '{enum}' : "{#{[node['type']].flatten.join('|')}}"
93
- # conversion_tag = node['x-cli-convert']
94
125
  conversion_tag = node.key?('x-cli-convert') ? 'conversion' : nil
95
126
  sep = node['x-cli-option'].start_with?('--') ? '=' : ' '
96
127
  "#{node['x-cli-option']}#{sep}#{"(#{conversion_tag})" if conversion_tag}#{arg_type}"
97
128
  end
98
129
  short = node.key?('x-cli-short') ? "(#{node['x-cli-short']})" : nil
99
- item[:description].push("(#{'special:' if node['x-cli-special']}#{envvar_prefix}#{@formatter.markdown_text("`#{cli_option}`")})#{short}") if cli_option
130
+ item['description'].push("(#{'special:' if node['x-cli-special']}#{envvar_prefix}#{@formatter.markdown_text("`#{cli_option}`")})#{short}") if cli_option
100
131
  end
101
- @rows.push(@formatter.check_row(item))
132
+ @sections.last[:rows].push(@formatter.check_row(item))
102
133
  end
103
134
  self
104
135
  end
@@ -7,11 +7,34 @@ module Aspera
7
7
  class Reader
8
8
  attr_reader :current
9
9
 
10
+ class << self
11
+ # Build a synthetic Reader from an OAS `parameters` array (entries with `in: query`).
12
+ # Produces a JSON Schema object whose `properties` map each query param name to its schema,
13
+ # with the OAS-level `description` and `required` merged in.
14
+ # @param params [Array<Hash>] raw OAS parameter objects (may contain path/header params too)
15
+ # @return [Reader]
16
+ def from_query_params(params)
17
+ properties = {}
18
+ required_names = []
19
+ params.each do |param|
20
+ next unless param['in'] == 'query'
21
+ name = param['name']
22
+ prop = (param['schema'] || {}).dup
23
+ prop['description'] = param['description'] if param['description'] && !prop.key?('description')
24
+ properties[name] = prop
25
+ required_names << name if param['required']
26
+ end
27
+ synthetic = {'type' => 'object', 'properties' => properties}
28
+ synthetic['required'] = required_names unless required_names.empty?
29
+ new(synthetic)
30
+ end
31
+ end
32
+
10
33
  # Shortcut to access current value at path
11
- # @param x [String] path element
34
+ # @param key [String] path element
12
35
  # @return [Hash, Array, String, Integer] current value at path
13
- def [](x)
14
- @current[x]
36
+ def [](key)
37
+ @current[key]
15
38
  end
16
39
 
17
40
  # Find sub path relative to current
@@ -24,13 +47,19 @@ module Aspera
24
47
  Aspera.assert_type(current, Hash){'schema'}
25
48
  if current.key?('$ref')
26
49
  ref = current['$ref']
27
- Aspera.assert(ref.start_with?('#/'))
50
+ Aspera.assert(ref.start_with?('#/')){"schema $ref must start with '#/': #{ref}"}
28
51
  current = @root.dig(*ref[2..].split('/'))
29
52
  end
30
53
  end
31
54
  Reader.new(@root, current)
32
55
  end
33
56
 
57
+ # Resolve a $ref string to a Reader
58
+ def resolve_ref(ref)
59
+ Aspera.assert(ref.start_with?('#/')){"schema $ref must start with '#/': #{ref}"}
60
+ Reader.new(@root, @root.dig(*ref[2..].split('/')))
61
+ end
62
+
34
63
  # Read schema from file or from cache
35
64
  # @param root [Hash] root schema
36
65
  # @param current [Hash, nil] current position in
@@ -40,15 +69,35 @@ module Aspera
40
69
  @current = current || root
41
70
  end
42
71
 
43
- # Recursively traverse schema properties with a block
44
- # Handles nested objects and arrays automatically
45
- # @param prefix [String] Prefix for property names (e.g., 'parent.child.')
72
+ # Recursively traverse schema properties with a block.
73
+ # If the current node has `oneOf`, each variant is traversed in turn and
74
+ # `on_variant` is called (if given) before each variant's properties.
75
+ # @param prefix [String] Prefix for property names (e.g., 'parent.child.')
76
+ # @param on_variant [Proc, nil] Called with the variant Reader before its properties
46
77
  # @yield [property_schema, name, full_name] Yields property info to block
47
- # @yieldparam property_schema [Reader] Schema reader for this property (use .current to get node hash)
78
+ # @yieldparam property_schema [Reader] Schema reader for this property
48
79
  # @yieldparam name [String] Property name
49
80
  # @yieldparam full_name [String] Full property name with prefix
50
81
  # @return [nil]
51
- def each_property(prefix = '', &block)
82
+ def each_property(prefix = '', on_variant: nil, &block)
83
+ if @current.key?('oneOf')
84
+ # Build reverse map: $ref -> discriminant value, from discriminator.mapping if present
85
+ discriminant_by_ref = {}
86
+ if @current.dig('discriminator', 'mapping').is_a?(Hash)
87
+ @current['discriminator']['mapping'].each do |value, ref|
88
+ discriminant_by_ref[ref] = value
89
+ end
90
+ end
91
+ discriminant_property = @current.dig('discriminator', 'propertyName')
92
+ @current['oneOf'].each do |variant_node|
93
+ ref = variant_node['$ref']
94
+ variant_reader = ref ? resolve_ref(ref) : Reader.new(@root, variant_node)
95
+ discriminant_value = ref ? discriminant_by_ref[ref] : nil
96
+ on_variant&.call(variant_reader, discriminant_property, discriminant_value)
97
+ variant_reader.each_property(prefix, on_variant: on_variant, &block)
98
+ end
99
+ return
100
+ end
52
101
  properties = dig('properties')
53
102
  properties.current.each_key do |name|
54
103
  property_full_name = "#{prefix}#{name}"
@@ -61,11 +110,11 @@ module Aspera
61
110
  # Recursively process nested structures
62
111
  case node['type']
63
112
  when 'object'
64
- property_schema.each_property("#{property_full_name}.", &block) if node['properties']
113
+ property_schema.each_property("#{property_full_name}.", on_variant: on_variant, &block) if node['properties']
65
114
  when 'array'
66
115
  if node['items']
67
116
  array_item_schema = property_schema.dig('items')
68
- array_item_schema.each_property("#{property_full_name}[].", &block) if array_item_schema.current['properties']
117
+ array_item_schema.each_property("#{property_full_name}[].", on_variant: on_variant, &block) if array_item_schema.current['properties']
69
118
  end
70
119
  end
71
120
  end