bug_bunny 4.8.0 → 4.8.1
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/.agents/skills/documentation-writer/SKILL.md +45 -0
- data/.agents/skills/gem-release/SKILL.md +114 -0
- data/.agents/skills/quality-code/SKILL.md +51 -0
- data/.agents/skills/sentry/SKILL.md +135 -0
- data/.agents/skills/sentry/references/api-endpoints.md +147 -0
- data/.agents/skills/sentry/scripts/sentry.rb +194 -0
- data/.agents/skills/skill-builder/SKILL.md +232 -0
- data/.agents/skills/skill-manager/SKILL.md +172 -0
- data/.agents/skills/skill-manager/scripts/sync.rb +310 -0
- data/.agents/skills/yard/SKILL.md +311 -0
- data/.agents/skills/yard/references/tipos.md +144 -0
- data/CHANGELOG.md +8 -0
- data/CLAUDE.md +28 -231
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +230 -0
- data/skill/references/client-middleware.md +144 -0
- data/skill/references/consumer.md +104 -0
- data/skill/references/controller.md +105 -0
- data/skill/references/errores.md +97 -0
- data/skill/references/resource.md +116 -0
- data/skill/references/routing.md +82 -0
- data/skill/references/testing.md +138 -0
- data/skills.lock +24 -0
- data/skills.yml +19 -0
- metadata +24 -28
- data/.claude/commands/gem-ai-setup.md +0 -174
- data/.claude/commands/pr.md +0 -53
- data/.claude/commands/release.md +0 -52
- data/.claude/commands/rubocop.md +0 -22
- data/.claude/commands/service-ai-setup.md +0 -168
- data/.claude/commands/test.md +0 -28
- data/.claude/commands/yard.md +0 -46
- data/docs/_index.md +0 -50
- data/docs/ai/_index.md +0 -56
- data/docs/ai/antipatterns.md +0 -166
- data/docs/ai/api.md +0 -251
- data/docs/ai/architecture.md +0 -92
- data/docs/ai/errors.md +0 -158
- data/docs/ai/faq_external.md +0 -133
- data/docs/ai/faq_internal.md +0 -86
- data/docs/ai/glossary.md +0 -45
- data/docs/concepts.md +0 -140
- data/docs/howto/controller.md +0 -194
- data/docs/howto/middleware_client.md +0 -119
- data/docs/howto/middleware_consumer.md +0 -127
- data/docs/howto/rails.md +0 -214
- data/docs/howto/resource.md +0 -200
- data/docs/howto/routing.md +0 -133
- data/docs/howto/testing.md +0 -259
- data/docs/howto/tracing.md +0 -119
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'net/http'
|
|
8
|
+
require 'uri'
|
|
9
|
+
|
|
10
|
+
module SkillsSync
|
|
11
|
+
SKILLS_YML = 'skills.yml'
|
|
12
|
+
SKILLS_LOCK = 'skills.lock'
|
|
13
|
+
SKILLS_DIR = File.join('.agents', 'skills')
|
|
14
|
+
|
|
15
|
+
GLOBAL_SKILL_PATHS = [
|
|
16
|
+
File.join(Dir.home, '.agents', 'skills'),
|
|
17
|
+
File.join(Dir.home, '.claude', '.agents', 'skills'),
|
|
18
|
+
File.join(Dir.home, '.claude', 'skills')
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def run
|
|
23
|
+
config_path = File.join(Dir.pwd, SKILLS_YML)
|
|
24
|
+
|
|
25
|
+
unless File.exist?(config_path)
|
|
26
|
+
puts "skill-manager sync — ERROR: #{SKILLS_YML} no encontrado en #{Dir.pwd}"
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
config = YAML.safe_load_file(config_path)
|
|
31
|
+
@local_skills_path = File.join(Dir.pwd, SKILLS_DIR)
|
|
32
|
+
FileUtils.mkdir_p(@local_skills_path)
|
|
33
|
+
|
|
34
|
+
@use_gh = !gh_available?.nil?
|
|
35
|
+
@token = ENV['GITHUB_TOKEN']
|
|
36
|
+
|
|
37
|
+
unless @use_gh || @token
|
|
38
|
+
puts "skill-manager sync — WARNING: ni 'gh' CLI ni GITHUB_TOKEN disponibles. Solo se sincronizarán gemas locales."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@previous_lock = load_lock
|
|
42
|
+
@current_lock = []
|
|
43
|
+
|
|
44
|
+
sync_gems(config['gems'] || [])
|
|
45
|
+
sync_services(config['services'] || [])
|
|
46
|
+
sync_skills(config['skills'] || [])
|
|
47
|
+
|
|
48
|
+
cleanup_removed_skills
|
|
49
|
+
save_lock
|
|
50
|
+
|
|
51
|
+
puts 'skill-manager sync — OK.'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# --- Lock file ---
|
|
57
|
+
|
|
58
|
+
def load_lock
|
|
59
|
+
lock_path = File.join(Dir.pwd, SKILLS_LOCK)
|
|
60
|
+
return [] unless File.exist?(lock_path)
|
|
61
|
+
|
|
62
|
+
data = YAML.safe_load_file(lock_path)
|
|
63
|
+
data['skills'] || []
|
|
64
|
+
rescue StandardError
|
|
65
|
+
[]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def save_lock
|
|
69
|
+
lock_path = File.join(Dir.pwd, SKILLS_LOCK)
|
|
70
|
+
data = {
|
|
71
|
+
'synced_at' => Time.now.strftime('%Y-%m-%d %H:%M:%S'),
|
|
72
|
+
'skills' => @current_lock.sort_by { |s| s['name'] }
|
|
73
|
+
}
|
|
74
|
+
File.write(lock_path, YAML.dump(data))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def record_lock(name, scope, path)
|
|
78
|
+
@current_lock << { 'name' => name, 'scope' => scope || 'local', 'path' => path }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def cleanup_removed_skills
|
|
82
|
+
previous_names = @previous_lock.map { |s| s['name'] }
|
|
83
|
+
current_names = @current_lock.map { |s| s['name'] }
|
|
84
|
+
removed = previous_names - current_names
|
|
85
|
+
|
|
86
|
+
removed.each do |name|
|
|
87
|
+
entry = @previous_lock.find { |s| s['name'] == name }
|
|
88
|
+
path = entry['path']
|
|
89
|
+
|
|
90
|
+
if File.exist?(path)
|
|
91
|
+
puts " #{name} — eliminado (ya no está en #{SKILLS_YML})"
|
|
92
|
+
FileUtils.rm_rf(path)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# --- Scope resolution ---
|
|
98
|
+
|
|
99
|
+
def resolve_dest(name, scope)
|
|
100
|
+
case scope
|
|
101
|
+
when 'global'
|
|
102
|
+
global_path = find_global_path(name) || default_global_path
|
|
103
|
+
dest = File.join(global_path, name)
|
|
104
|
+
local_path = File.join(@local_skills_path, name)
|
|
105
|
+
|
|
106
|
+
# Si existe local, borrarla
|
|
107
|
+
if File.exist?(local_path)
|
|
108
|
+
puts " #{name} — eliminando copia local (scope: global)"
|
|
109
|
+
FileUtils.rm_rf(local_path)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
dest
|
|
113
|
+
else # local o sin especificar
|
|
114
|
+
# Si existe global, saltear
|
|
115
|
+
existing_global = find_global_path(name)
|
|
116
|
+
if existing_global
|
|
117
|
+
puts " #{name} — disponible globalmente en #{File.join(existing_global, name)}. Saltando."
|
|
118
|
+
return nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
File.join(@local_skills_path, name)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def find_global_path(name)
|
|
126
|
+
GLOBAL_SKILL_PATHS.find do |path|
|
|
127
|
+
File.exist?(File.join(path, name, 'SKILL.md'))
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def default_global_path
|
|
132
|
+
existing = GLOBAL_SKILL_PATHS.find { |p| File.directory?(p) }
|
|
133
|
+
return existing if existing
|
|
134
|
+
|
|
135
|
+
path = GLOBAL_SKILL_PATHS.first
|
|
136
|
+
FileUtils.mkdir_p(path)
|
|
137
|
+
path
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# --- Sync methods ---
|
|
141
|
+
|
|
142
|
+
def sync_gems(gems)
|
|
143
|
+
gems.each do |gem_config|
|
|
144
|
+
name = gem_config['name']
|
|
145
|
+
scope = gem_config['scope']
|
|
146
|
+
dest = resolve_dest(name, scope)
|
|
147
|
+
next unless dest
|
|
148
|
+
|
|
149
|
+
begin
|
|
150
|
+
spec = Gem::Specification.find_by_name(name)
|
|
151
|
+
rescue Gem::MissingSpecError
|
|
152
|
+
puts " WARNING: gema '#{name}' no instalada. Saltando."
|
|
153
|
+
next
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
skill_dir = File.join(spec.gem_dir, 'skill')
|
|
157
|
+
skill_file = File.join(skill_dir, 'SKILL.md')
|
|
158
|
+
|
|
159
|
+
unless File.exist?(skill_file)
|
|
160
|
+
puts " WARNING: #{name} v#{spec.version} no incluye skill en skill/. Saltando."
|
|
161
|
+
next
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
scope_label = scope == 'global' ? 'global' : 'local'
|
|
165
|
+
puts " #{name} v#{spec.version} (#{scope_label})"
|
|
166
|
+
replace_dir(dest)
|
|
167
|
+
FileUtils.cp_r(Dir[File.join(skill_dir, '*')], dest)
|
|
168
|
+
record_lock(name, scope, dest)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def sync_services(services)
|
|
173
|
+
services.each do |service|
|
|
174
|
+
name = service['name']
|
|
175
|
+
scope = service['scope']
|
|
176
|
+
dest = resolve_dest(name, scope)
|
|
177
|
+
next unless dest
|
|
178
|
+
|
|
179
|
+
repo = service['repo']
|
|
180
|
+
scope_label = scope == 'global' ? 'global' : 'local'
|
|
181
|
+
puts " #{name} (GitHub: #{repo}, skill/) [#{scope_label}]"
|
|
182
|
+
replace_dir(dest)
|
|
183
|
+
download_github_dir(repo, 'main', 'skill', dest)
|
|
184
|
+
record_lock(name, scope, dest)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def sync_skills(skills)
|
|
189
|
+
skills.each do |skill|
|
|
190
|
+
name = skill['name']
|
|
191
|
+
scope = skill['scope']
|
|
192
|
+
dest = resolve_dest(name, scope)
|
|
193
|
+
next unless dest
|
|
194
|
+
|
|
195
|
+
repo = skill['repo']
|
|
196
|
+
remote_path = skill['path'] || "#{SKILLS_DIR}/#{name}"
|
|
197
|
+
scope_label = scope == 'global' ? 'global' : 'local'
|
|
198
|
+
puts " #{name} (GitHub: #{repo}, #{remote_path}/) [#{scope_label}]"
|
|
199
|
+
replace_dir(dest)
|
|
200
|
+
download_github_dir(repo, 'main', remote_path, dest)
|
|
201
|
+
record_lock(name, scope, dest)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# --- Helpers ---
|
|
206
|
+
|
|
207
|
+
def replace_dir(dest)
|
|
208
|
+
FileUtils.rm_rf(dest)
|
|
209
|
+
FileUtils.mkdir_p(dest)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def download_github_dir(repo, ref, remote_path, dest)
|
|
213
|
+
entries = list_github_dir(repo, ref, remote_path)
|
|
214
|
+
|
|
215
|
+
if entries.empty?
|
|
216
|
+
puts " WARNING: #{remote_path}/ no encontrado en #{repo}@#{ref}. Saltando."
|
|
217
|
+
return
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
entries.each do |entry|
|
|
221
|
+
next unless entry['type'] == 'file'
|
|
222
|
+
|
|
223
|
+
relative_path = entry['path'].sub(%r{^#{Regexp.escape(remote_path)}/}, '')
|
|
224
|
+
file_dest = File.join(dest, relative_path)
|
|
225
|
+
FileUtils.mkdir_p(File.dirname(file_dest))
|
|
226
|
+
|
|
227
|
+
content = if @use_gh
|
|
228
|
+
gh_fetch_file(repo, ref, entry['path'])
|
|
229
|
+
else
|
|
230
|
+
fetch_url(entry['download_url'])
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
if content
|
|
234
|
+
File.write(file_dest, content)
|
|
235
|
+
else
|
|
236
|
+
puts " WARNING: no se pudo descargar #{entry['path']} de #{repo}."
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def list_github_dir(repo, ref, path)
|
|
242
|
+
response = if @use_gh
|
|
243
|
+
gh_api("repos/#{repo}/contents/#{path}?ref=#{ref}")
|
|
244
|
+
else
|
|
245
|
+
fetch_url("https://api.github.com/repos/#{repo}/contents/#{path}?ref=#{ref}")
|
|
246
|
+
end
|
|
247
|
+
return [] unless response
|
|
248
|
+
|
|
249
|
+
items = JSON.parse(response)
|
|
250
|
+
return [] unless items.is_a?(Array)
|
|
251
|
+
|
|
252
|
+
all_entries = []
|
|
253
|
+
items.each do |item|
|
|
254
|
+
if item['type'] == 'dir'
|
|
255
|
+
all_entries.concat(list_github_dir(repo, ref, item['path']))
|
|
256
|
+
else
|
|
257
|
+
all_entries << item
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
all_entries
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# --- GitHub CLI ---
|
|
264
|
+
|
|
265
|
+
def gh_available?
|
|
266
|
+
`which gh 2>/dev/null`.strip
|
|
267
|
+
$?.success? ? true : nil
|
|
268
|
+
rescue StandardError
|
|
269
|
+
nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def gh_api(endpoint)
|
|
273
|
+
output = `gh api "#{endpoint}" 2>/dev/null`
|
|
274
|
+
$?.success? ? output.force_encoding('UTF-8') : nil
|
|
275
|
+
rescue StandardError
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def gh_fetch_file(repo, ref, path)
|
|
280
|
+
output = `gh api "repos/#{repo}/contents/#{path}?ref=#{ref}" --jq '.content' 2>/dev/null`
|
|
281
|
+
return nil unless $?.success?
|
|
282
|
+
|
|
283
|
+
require 'base64'
|
|
284
|
+
Base64.decode64(output).force_encoding('UTF-8')
|
|
285
|
+
rescue StandardError
|
|
286
|
+
nil
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# --- HTTP directo ---
|
|
290
|
+
|
|
291
|
+
def fetch_url(url)
|
|
292
|
+
uri = URI.parse(url)
|
|
293
|
+
request = Net::HTTP::Get.new(uri)
|
|
294
|
+
request['Authorization'] = "Bearer #{@token}" if @token
|
|
295
|
+
request['User-Agent'] = 'skill-manager/1.0'
|
|
296
|
+
request['Accept'] = 'application/vnd.github.v3+json' if url.include?('api.github.com')
|
|
297
|
+
|
|
298
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
299
|
+
http.request(request)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
response.code == '200' ? response.body.force_encoding('UTF-8') : nil
|
|
303
|
+
rescue StandardError => e
|
|
304
|
+
puts " ERROR fetching #{url}: #{e.message}"
|
|
305
|
+
nil
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
SkillsSync.run if __FILE__ == $PROGRAM_NAME
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yard
|
|
3
|
+
description: Experto en documentación YARD para Ruby. Consultame para escribir documentación correcta con tags, tipos, directivas, duck types y patrones avanzados. Úsala SIEMPRE que necesites documentar código Ruby con YARD o auditar documentación existente.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# YARD Expert
|
|
7
|
+
|
|
8
|
+
Skill de conocimiento completo sobre YARD (Yet Another Ruby Document). Consultame para escribir documentación correcta, auditar cobertura o resolver dudas sobre tags, tipos y directivas.
|
|
9
|
+
|
|
10
|
+
## Glosario
|
|
11
|
+
|
|
12
|
+
**Tag** — Metadato prefijado con `@` que describe un aspecto del código (ej: `@param`, `@return`).
|
|
13
|
+
|
|
14
|
+
**Directiva** — Instrucción prefijada con `@!` que modifica el contexto de parsing (ej: `@!method`, `@!attribute`).
|
|
15
|
+
|
|
16
|
+
**Type specifier list** — Lista de tipos entre corchetes `[Type]` usada en tags como `@param` y `@return`.
|
|
17
|
+
|
|
18
|
+
**Duck type** — Tipo definido por interfaz, no por clase. Se escribe como `#method_name`.
|
|
19
|
+
|
|
20
|
+
**Reference tag** — Sintaxis `(see OBJECT)` que copia tags de otro objeto.
|
|
21
|
+
|
|
22
|
+
## Anatomía de una documentación YARD
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# Descripción breve del método (primera línea).
|
|
26
|
+
#
|
|
27
|
+
# Descripción extendida opcional. Puede tener múltiples párrafos,
|
|
28
|
+
# listas y ejemplos en markdown.
|
|
29
|
+
#
|
|
30
|
+
# @param name [Type] descripción del parámetro
|
|
31
|
+
# @return [Type] descripción del retorno
|
|
32
|
+
# @raise [ExceptionClass] cuándo se lanza
|
|
33
|
+
# @example Título del ejemplo
|
|
34
|
+
# resultado = mi_metodo("valor")
|
|
35
|
+
# # => "esperado"
|
|
36
|
+
def mi_metodo(name)
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Reglas clave:**
|
|
41
|
+
- Primera línea: descripción breve, sin punto final si es corta.
|
|
42
|
+
- Línea vacía entre descripción y tags.
|
|
43
|
+
- Tags multilínea: indentar 2 espacios las líneas siguientes.
|
|
44
|
+
- Orden recomendado de tags: `@param` → `@option` → `@yield` → `@yieldparam` → `@yieldreturn` → `@return` → `@raise` → `@example`.
|
|
45
|
+
|
|
46
|
+
## Sistema de Tipos
|
|
47
|
+
|
|
48
|
+
Ver catálogo completo en [references/tipos.md](references/tipos.md).
|
|
49
|
+
|
|
50
|
+
### Tipos básicos
|
|
51
|
+
```ruby
|
|
52
|
+
# @param name [String] un string
|
|
53
|
+
# @param count [Integer] un entero
|
|
54
|
+
# @param flag [Boolean] true o false (convención YARD, no existe en Ruby)
|
|
55
|
+
# @return [void] sin valor de retorno significativo
|
|
56
|
+
# @return [nil] retorna nil explícitamente
|
|
57
|
+
# @return [self] retorna self (métodos encadenables)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Union types
|
|
61
|
+
```ruby
|
|
62
|
+
# @param input [String, Symbol] acepta string o symbol
|
|
63
|
+
# @return [String, nil] puede retornar nil
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Generics (parametrized types)
|
|
67
|
+
```ruby
|
|
68
|
+
# @param items [Array<String>] array de strings
|
|
69
|
+
# @param map [Hash<Symbol, Integer>] hash con keys symbol y values integer
|
|
70
|
+
# @return [Set<User>] set de usuarios
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Hashes con estructura
|
|
74
|
+
```ruby
|
|
75
|
+
# @param opts [Hash{Symbol => String}] opciones con keys symbol
|
|
76
|
+
# @param data [Hash{String => Array<Integer>}] hash complejo
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Duck types
|
|
80
|
+
```ruby
|
|
81
|
+
# @param io [#read] cualquier objeto que responda a #read
|
|
82
|
+
# @param callable [#call] cualquier objeto callable
|
|
83
|
+
# @param io [#read, #close] debe responder a ambos
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Order-dependent lists
|
|
87
|
+
```ruby
|
|
88
|
+
# @return [Array(String, Integer, Hash)] exactamente 3 elementos en ese orden
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Literals
|
|
92
|
+
```ruby
|
|
93
|
+
# @return [true] siempre retorna true
|
|
94
|
+
# @return [false, nil] retorna false o nil
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Tags principales
|
|
98
|
+
|
|
99
|
+
### @param
|
|
100
|
+
```ruby
|
|
101
|
+
# @param name [String] el nombre del usuario
|
|
102
|
+
# @param age [Integer] la edad (debe ser > 0)
|
|
103
|
+
def create(name, age); end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### @option (para hashes de opciones)
|
|
107
|
+
```ruby
|
|
108
|
+
# @param opts [Hash] opciones de configuración
|
|
109
|
+
# @option opts [String] :host ("localhost") el hostname
|
|
110
|
+
# @option opts [Integer] :port (3000) el puerto
|
|
111
|
+
# @option opts [Boolean] :ssl (false) usar SSL
|
|
112
|
+
def connect(opts = {}); end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### @return
|
|
116
|
+
```ruby
|
|
117
|
+
# @return [String] la representación en texto
|
|
118
|
+
# @return [void] no usar el valor de retorno
|
|
119
|
+
def to_s; end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### @yield y @yieldparam
|
|
123
|
+
```ruby
|
|
124
|
+
# @yield [user, index] itera sobre cada usuario
|
|
125
|
+
# @yieldparam user [User] el usuario actual
|
|
126
|
+
# @yieldparam index [Integer] la posición en la lista
|
|
127
|
+
# @yieldreturn [Boolean] true para continuar, false para detener
|
|
128
|
+
def each_user(&block); end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### @raise
|
|
132
|
+
```ruby
|
|
133
|
+
# @raise [ArgumentError] si el nombre está vacío
|
|
134
|
+
# @raise [ActiveRecord::RecordNotFound] si no existe el registro
|
|
135
|
+
def find!(name); end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### @example
|
|
139
|
+
```ruby
|
|
140
|
+
# @example Uso básico
|
|
141
|
+
# user = User.find("john")
|
|
142
|
+
# user.name #=> "john"
|
|
143
|
+
#
|
|
144
|
+
# @example Con opciones
|
|
145
|
+
# user = User.find("john", include: :posts)
|
|
146
|
+
def find(name, **opts); end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### @see
|
|
150
|
+
```ruby
|
|
151
|
+
# @see User#destroy método relacionado
|
|
152
|
+
# @see https://api.example.com/docs documentación externa
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### @deprecated
|
|
156
|
+
```ruby
|
|
157
|
+
# @deprecated Usar {#new_method} en su lugar desde v2.0.
|
|
158
|
+
def old_method; end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### @abstract
|
|
162
|
+
```ruby
|
|
163
|
+
# @abstract Subclases deben implementar {#execute}.
|
|
164
|
+
class BaseCommand; end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### @note y @todo
|
|
168
|
+
```ruby
|
|
169
|
+
# @note Este método no es thread-safe.
|
|
170
|
+
# @todo Agregar soporte para paginación.
|
|
171
|
+
def fetch_all; end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### @since y @api
|
|
175
|
+
```ruby
|
|
176
|
+
# @since 1.5.0
|
|
177
|
+
# @api private
|
|
178
|
+
def internal_method; end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Directivas
|
|
182
|
+
|
|
183
|
+
### @!method (documentar métodos dinámicos)
|
|
184
|
+
```ruby
|
|
185
|
+
class User
|
|
186
|
+
# @!method name
|
|
187
|
+
# @return [String] el nombre del usuario
|
|
188
|
+
# @!method name=(value)
|
|
189
|
+
# @param value [String] el nuevo nombre
|
|
190
|
+
attr_accessor :name
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### @!attribute
|
|
195
|
+
```ruby
|
|
196
|
+
# @!attribute [r] count
|
|
197
|
+
# @return [Integer] el conteo actual
|
|
198
|
+
# @!attribute [rw] name
|
|
199
|
+
# @return [String] el nombre
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### @!macro (evitar repetición)
|
|
203
|
+
```ruby
|
|
204
|
+
# @!macro [attach] property
|
|
205
|
+
# @!method $1
|
|
206
|
+
# @return [$2] el valor de $1
|
|
207
|
+
property :name, String
|
|
208
|
+
property :age, Integer
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### @!group / @!endgroup
|
|
212
|
+
```ruby
|
|
213
|
+
# @!group Validaciones
|
|
214
|
+
|
|
215
|
+
def validate_name; end
|
|
216
|
+
def validate_age; end
|
|
217
|
+
|
|
218
|
+
# @!endgroup
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### @!scope y @!visibility
|
|
222
|
+
```ruby
|
|
223
|
+
# @!scope class
|
|
224
|
+
# @!visibility private
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Reference tags
|
|
228
|
+
```ruby
|
|
229
|
+
# @param user [String] el usuario
|
|
230
|
+
# @param host [String] el host
|
|
231
|
+
def clean(user, host); end
|
|
232
|
+
|
|
233
|
+
# @param (see #clean)
|
|
234
|
+
def activate(user, host); end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Antipatrones
|
|
238
|
+
|
|
239
|
+
**Documentar lo obvio** — No repitas el nombre del método en la descripción.
|
|
240
|
+
```ruby
|
|
241
|
+
# MAL:
|
|
242
|
+
# Gets the name.
|
|
243
|
+
# @return [String] the name
|
|
244
|
+
def name; end
|
|
245
|
+
|
|
246
|
+
# BIEN:
|
|
247
|
+
# @return [String] nombre completo del usuario (nombre + apellido)
|
|
248
|
+
def name; end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Omitir tipos** — Siempre especificá tipos en `@param` y `@return`.
|
|
252
|
+
```ruby
|
|
253
|
+
# MAL:
|
|
254
|
+
# @param name el nombre
|
|
255
|
+
|
|
256
|
+
# BIEN:
|
|
257
|
+
# @param name [String] el nombre
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Usar Boolean sin aclarar** — Ruby no tiene clase Boolean. Es una convención YARD.
|
|
261
|
+
```ruby
|
|
262
|
+
# MAL:
|
|
263
|
+
# @return [TrueClass, FalseClass]
|
|
264
|
+
|
|
265
|
+
# BIEN:
|
|
266
|
+
# @return [Boolean] true si el usuario es admin
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**@return void sin explicar** — Usá void cuando el retorno no importa, no cuando no sabés.
|
|
270
|
+
```ruby
|
|
271
|
+
# BIEN: método que muta estado, el retorno no importa
|
|
272
|
+
# @return [void]
|
|
273
|
+
def save!; end
|
|
274
|
+
|
|
275
|
+
# MAL: el retorno SÍ importa, no uses void
|
|
276
|
+
# @return [void]
|
|
277
|
+
def valid?; end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## FAQ
|
|
281
|
+
|
|
282
|
+
### ¿Cómo documento un método que acepta **kwargs?
|
|
283
|
+
```ruby
|
|
284
|
+
# @param name [String] el nombre
|
|
285
|
+
# @param opts [Hash] opciones adicionales
|
|
286
|
+
# @option opts [Integer] :timeout (30) segundos de espera
|
|
287
|
+
# @option opts [Boolean] :retry (true) reintentar en fallo
|
|
288
|
+
def fetch(name, **opts); end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### ¿Cómo documento un método con splat?
|
|
292
|
+
```ruby
|
|
293
|
+
# @param args [Array<String>] lista variable de nombres
|
|
294
|
+
def process(*args); end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### ¿Cómo verifico la cobertura?
|
|
298
|
+
```bash
|
|
299
|
+
bundle exec yard stats --list-undoc
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### ¿Cómo genero la documentación?
|
|
303
|
+
```bash
|
|
304
|
+
bundle exec yard doc
|
|
305
|
+
# Servidor local:
|
|
306
|
+
bundle exec yard server --reload
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Referencias
|
|
310
|
+
|
|
311
|
+
- [Tipos completos](references/tipos.md) — Catálogo exhaustivo de type specifications
|