lux-hammer 0.2.2 → 0.2.3

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 (5) hide show
  1. checksums.yaml +4 -4
  2. data/.version +1 -1
  3. data/README.md +97 -28
  4. data/lib/lux-hammer.rb +73 -42
  5. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b973a2becedf10ed4247fce46d195f07619c6f6b9bf56f41464b57764733716
4
- data.tar.gz: a1ffdc8fa9b19f47599a3e930181fa1972095872db4f92740308cf393090bfcd
3
+ metadata.gz: ae2c5c9dbcd3b17941440e327e04e84dbffaec7209e4ce15b205cf83525b3724
4
+ data.tar.gz: 516eb4a4cb0f8f02f489eb00d2e4500cfe70f600eaebe192fb1749c81fe37074
5
5
  SHA512:
6
- metadata.gz: 55e093ed6cc2ed08349fdfd9296360153b2d0cb4763b57546e4e9453657e0da7e74608179b70946d6600499624b0cac1c57017208a91143d3eba6917a1cef976
7
- data.tar.gz: 3401f9c5defba4419e40c571cfcab4869ceb408dbcf8a88d9bab98aea2760ad810ceee02feaa31f78b661849e88b1ecb7c5e63ad3e930ed3baa371637d15f20b
6
+ metadata.gz: d922e58738cb3afc95c54864fc79d9a6ec556e3634a94f258711652abf220ba7f436e1d0f0cf24f48ba9c6372af22339d8377b491c9de93f708e19a7ca13692d
7
+ data.tar.gz: 6ad385997b87bf70ba58ec3ae6d701b3717d5e9584ef00aae29ebeb5c9b3666a39f6032277e275987f6de94161809767eeb838e288718fda09287327009756b6
data/.version CHANGED
@@ -1 +1 @@
1
- 0.2.2
1
+ 0.2.3
data/README.md CHANGED
@@ -1,9 +1,52 @@
1
1
  # hammer
2
2
 
3
- A modern CLI builder for Ruby. The good parts of
4
- [Rake](https://github.com/ruby/rake) and
5
- [Thor](https://github.com/rails/thor) without the cruft. Drop a
6
- `Hammerfile`, run `hammer`, ship.
3
+ The bastard Frankenstein child of Rake, Thor, and Joshua. Sewn
4
+ together from three good ideas, with the rest of each parent left on
5
+ the cutting room floor.
6
+
7
+ Drop a `Hammerfile`, run `hammer`, ship.
8
+
9
+ ```ruby
10
+ namespace :db do # Rake-style colon paths
11
+ define :migrate do # Joshua-style define block
12
+ desc 'Run pending migrations'
13
+ opt :pretend, type: :boolean, alias: :p # Thor-style typed opts
14
+ proc do |o|
15
+ say.green "migrating pretend=#{o[:pretend].inspect}"
16
+ end
17
+ end
18
+ end
19
+ ```
20
+
21
+ ```sh
22
+ $ hammer db:migrate --pretend
23
+ migrating pretend=true
24
+ ```
25
+
26
+ ## The bloodline
27
+
28
+ * **From [Rake](https://github.com/ruby/rake)** we took the *namespace
29
+ tree* and *task prereqs* - `db:users:list` colon paths, `needs :env`
30
+ dependencies, the one-file `Hammerfile` at the project root. Left on
31
+ the table: the build-system DNA (file/mtime tasks) and that
32
+ `task[a,b,c]` argument syntax nobody enjoys typing into a shell.
33
+
34
+ * **From [Thor](https://github.com/rails/thor)** we took *real CLI
35
+ ergonomics* - typed options, defaults, aliases, required flags, and
36
+ generated `-h` help. Left on the table: the four-constant top-level
37
+ (`Thor`, `Thor::Group`, `Thor::Shell`, `Thor::Actions`), the
38
+ parallel method-params-vs-`method_option` arg systems, the
39
+ `HashWithIndifferentAccess`, and the file generator half of the gem
40
+ that most CLIs never reach for.
41
+
42
+ * **From [Joshua](https://github.com/dux/joshua)** we took the
43
+ *`define :name do ... end` block DSL* - declarative metadata up
44
+ top, one `proc do |opts| ... end` at the bottom doing the work. No
45
+ `def`-and-`desc` split, no class required, no boilerplate between
46
+ "what this command is" and "what it does".
47
+
48
+ The result: ~400 lines of code, zero runtime dependencies, and a file
49
+ that reads top-to-bottom like a recipe.
7
50
 
8
51
  ## Install
9
52
 
@@ -93,7 +136,7 @@ Rake::Task['db:migrate'].invoke('prod')
93
136
  ```
94
137
  ```ruby
95
138
  # hammer - reads like a normal Ruby method call
96
- hammer_db_migrate(env: 'prod')
139
+ hammer 'db:migrate', env: 'prod'
97
140
  ```
98
141
 
99
142
  ### Thor: `desc` welded to a method, no aliases, two arg systems
@@ -426,7 +469,7 @@ Hooks fire outer -> inner, then the command's handler:
426
469
  before { Dotenv.load } # runs before every command
427
470
 
428
471
  namespace :db do
429
- before { hammer_env } # runs before every db:* command
472
+ before { hammer :env } # runs before every db:* command
430
473
  define :migrate do
431
474
  proc { |opts| ... } # no boilerplate require inside
432
475
  end
@@ -442,7 +485,7 @@ Pairs naturally with hidden commands (next section): keep `:env` /
442
485
  ## Hidden commands (no `desc`)
443
486
 
444
487
  A command declared without a `desc` is **hidden from help listings**
445
- but stays fully dispatchable and `hammer_*`-callable:
488
+ but stays fully dispatchable and `hammer`-callable:
446
489
 
447
490
  ```ruby
448
491
  define :env do
@@ -450,7 +493,7 @@ define :env do
450
493
  end
451
494
 
452
495
  namespace :db do
453
- before { hammer_env } # call it from a hook
496
+ before { hammer :env } # call it from a hook
454
497
  define :migrate do
455
498
  desc 'Run migrations'
456
499
  proc { |_| ... }
@@ -459,7 +502,7 @@ end
459
502
  ```
460
503
 
461
504
  `hammer` and `hammer db` won't list `env`, but `hammer env`,
462
- `hammer_env` from another proc, and `before { hammer_env }` all work.
505
+ `hammer :env` from another proc, and `before { hammer :env }` all work.
463
506
 
464
507
  ## Prereqs (`needs`)
465
508
 
@@ -483,7 +526,7 @@ end
483
526
  ```
484
527
 
485
528
  Prereq names are colon paths resolved against the root class - same
486
- lookup as `hammer_*`. Use `needs 'db:env'` to depend on a namespaced
529
+ lookup as `hammer`. Use `needs 'db:env'` to depend on a namespaced
487
530
  command.
488
531
 
489
532
  Each prereq fires **at most once per top-level invocation**, so if
@@ -491,7 +534,7 @@ Each prereq fires **at most once per top-level invocation**, so if
491
534
  only once. Prereqs run with default options (no argv passed through).
492
535
 
493
536
  `needs` vs `before`:
494
- * `before { hammer_env }` - fires for *every* command in a scope.
537
+ * `before { hammer :env }` - fires for *every* command in a scope.
495
538
  * `needs :env` - declared per command, deduped across the call chain.
496
539
 
497
540
  ## Command aliases (`alt`)
@@ -509,7 +552,7 @@ Then `hammer server`, `hammer s`, and `hammer srv` all dispatch to the
509
552
  same command. Alts work inside namespaces too: `alt :m` on `db:migrate`
510
553
  makes `db:m` resolve.
511
554
 
512
- ## Cross-invocation (`hammer_*`)
555
+ ## Cross-invocation (`hammer`)
513
556
 
514
557
  From inside any command's proc - or from outside via the class - you can
515
558
  invoke other commands without re-shelling out:
@@ -517,28 +560,54 @@ invoke other commands without re-shelling out:
517
560
  ```ruby
518
561
  define :deploy do
519
562
  proc do |opts|
520
- hammer_build(env: 'prod', verbose: true)
521
- hammer_db_migrate
563
+ hammer :build, env: 'prod', verbose: true
564
+ hammer 'db:migrate'
522
565
  say.green 'deployed'
523
566
  end
524
567
  end
525
568
  ```
526
569
 
527
- The mapping mirrors the CLI literally:
570
+ Signature: `hammer(name, *args, **opts)`. `name` is a symbol for a
571
+ single command or a colon-path string for namespaced commands. Trailing
572
+ positionals become positional ARGV; kwargs become CLI flags:
528
573
 
529
- * `hammer_X_Y_Z` → command path `X:Y:Z` (underscores in the method
530
- name become colons)
531
- * positional args → positional ARGV
574
+ * `hammer :foo` → `foo`
575
+ * `hammer 'db:users:list'` → `db:users:list`
576
+ * `hammer :evaluate, 'puts 42'` `evaluate "puts 42"` (positional ARGV)
532
577
  * `verbose: true` → `--verbose`
533
- * `no_cache: true` → `--no-cache` (just the same rule - underscores in
534
- the kwarg key become dashes)
578
+ * `no_cache: true` → `--no-cache` (underscores in the key become dashes)
535
579
  * `dry_run: true` → `--dry-run`
536
580
  * `env: 'prod'` → `--env=prod`
537
581
  * `anything: false` → skipped (no-op; use `no_x: true` to negate)
538
582
 
539
- `MyCli.hammer_db_users_list("a", verbose: true)` also works at the
583
+ `MyCli.hammer 'db:users:list', verbose: true` also works at the
540
584
  class level, useful for tests and scripting.
541
585
 
586
+ ## Chained dispatch (`+`)
587
+
588
+ Run several commands in one shot, Rake-style, using a bare `+` token
589
+ as the separator:
590
+
591
+ ```sh
592
+ hammer build + deploy + notify
593
+ hammer build prod -v + db:migrate --pretend + deploy --url=https://x.com
594
+ ```
595
+
596
+ Each segment is parsed and dispatched independently - its own opts,
597
+ its own positional args. `needs` prereqs dedupe across the whole
598
+ chain, so a shared `:env` step runs once even if every chained command
599
+ declares `needs :env`.
600
+
601
+ A `+` only acts as a separator when it arrives as its own argv token.
602
+ Quoting protects it: `--foo="a + b"` reaches the parser as a single
603
+ token and is left alone. To pass a literal `+` as a positional, double
604
+ it - `++` unescapes to a single `+`:
605
+
606
+ ```sh
607
+ hammer echo ++ x # echo gets positional args ["+", "x"]
608
+ hammer echo + bar # runs echo, then runs bar
609
+ ```
610
+
542
611
  ## Shell helpers
543
612
 
544
613
  These are mixed into every handler (and also live on `Hammer::Shell` for
@@ -673,7 +742,7 @@ end
673
742
  define :deploy do
674
743
  desc 'Deploy to prod'
675
744
  proc do |_|
676
- hammer_db_migrate # cross-file invocation just works
745
+ hammer 'db:migrate' # cross-file invocation just works
677
746
  say.cyan 'deployed'
678
747
  end
679
748
  end
@@ -798,7 +867,7 @@ define :deploy do
798
867
  opt :force, type: :boolean
799
868
 
800
869
  proc do |opts|
801
- hammer_build(env: 'prod')
870
+ hammer :build, env: 'prod'
802
871
  exit 0 unless yes? "deploy to #{opts[:url]}?" unless opts[:force]
803
872
  say.yellow "deploying to #{opts[:url]}"
804
873
  end
@@ -909,7 +978,7 @@ class MyCli < Hammer
909
978
  end
910
979
 
911
980
  MyCli.start(ARGV) # or:
912
- MyCli.hammer_greet('dino', loud: true)
981
+ MyCli.hammer :greet, loud: true
913
982
  ```
914
983
 
915
984
  ## Development
@@ -941,7 +1010,7 @@ few small things that have been bugging me about both for years.
941
1010
  | Opts container | `Thor::CoreExt::HashWithIndifferentAccess` | plain `Hash` with symbol keys |
942
1011
  | Positional args | method positional params + `method_option`, two parallel systems | declared-order opts fill from positional, single system |
943
1012
  | Sub-namespaces | `register SubClass, 'name', '...'` (inheritance ceremony) | `namespace :name do ... end` (no classes needed) |
944
- | Cross-invoke | `invoke 'name', [args], opts` | `hammer_name(*args, **kwargs)` (looks like a method call) |
1013
+ | Cross-invoke | `invoke 'name', [args], opts` | `hammer :name, **opts` (looks like a method call) |
945
1014
  | Inline CLI | class only | class DSL **or** `Hammer.run do ... end` block DSL **or** a `Hammerfile` |
946
1015
 
947
1016
  **What hammer does better and why:**
@@ -955,7 +1024,7 @@ few small things that have been bugging me about both for years.
955
1024
  you into method params (which then clash with options) or makes you read
956
1025
  `ARGV` yourself. Hammer just says: opts you declared come first, leftover
957
1026
  goes to `opts[:args]`.
958
- * **Cross-invocation reads as Ruby.** `hammer_db_migrate(env: 'prod')`
1027
+ * **Cross-invocation reads as Ruby.** `hammer 'db:migrate', env: 'prod'`
959
1028
  looks like a method call. Thor's `invoke('db:migrate', [], env: 'prod')`
960
1029
  always feels like reflection.
961
1030
  * **No generator complexity.** Thor's other half is file scaffolding and
@@ -971,8 +1040,8 @@ few small things that have been bugging me about both for years.
971
1040
  | Namespacing | colon paths (`db:migrate`) | colon paths (`db:migrate`) - parity |
972
1041
  | Per-task options | `task[a,b,c]` positional only | typed `opt`s with flags, aliases, defaults, required |
973
1042
  | Help | `rake -T` (plain list) | bare `hammer` lists everything grouped by namespace; `hammer X -h` for per-command help with examples and defaults |
974
- | Cross-invoke | `Rake::Task['db:migrate'].invoke` | `hammer_db_migrate` |
975
- | Prerequisites | `task :build => [:clean, :compile]` (declarative DAG) | explicit - call `hammer_clean; hammer_compile` in the proc |
1043
+ | Cross-invoke | `Rake::Task['db:migrate'].invoke` | `hammer 'db:migrate'` |
1044
+ | Prerequisites | `task :build => [:clean, :compile]` (declarative DAG) | explicit - call `hammer :clean; hammer :compile` in the proc |
976
1045
  | File tasks | yes (mtime-based) | no |
977
1046
  | Aliases | none (workarounds via re-defined tasks) | `alt :short_name` |
978
1047
  | Split across files | `import 'other.rake'` | `load auto: true` (or explicit paths/globs) |
data/lib/lux-hammer.rb CHANGED
@@ -17,7 +17,7 @@ require_relative 'hammer/command_builder'
17
17
  # opt :verbose, type: :boolean, alias: :v
18
18
  # opt :env, type: :string, default: 'dev'
19
19
  # proc do |opts|
20
- # say "building #{opts[:env]} args=#{opts[:args].inspect}", :green
20
+ # say.green "building #{opts[:env]} args=#{opts[:args].inspect}"
21
21
  # end
22
22
  # end
23
23
  # end
@@ -168,7 +168,7 @@ class Hammer
168
168
  def namespace(name, &block)
169
169
  sub = Class.new(Hammer)
170
170
  # Track the top-level CLI class so cross-invocation
171
- # (`hammer_<colon_path>`) from inside a namespaced command dispatches
171
+ # (`hammer 'ns:cmd'`) from inside a namespaced command dispatches
172
172
  # against the full tree, not just the current namespace.
173
173
  sub.instance_variable_set(:@root, root)
174
174
  # Parent link, so `before` hooks defined further up the namespace
@@ -188,7 +188,7 @@ class Hammer
188
188
  #
189
189
  # before { |opts| Dotenv.load }
190
190
  # namespace :db do
191
- # before { hammer_env }
191
+ # before { hammer :env }
192
192
  # define :migrate do ... end
193
193
  # end
194
194
  def before(&block)
@@ -257,15 +257,41 @@ class Hammer
257
257
  # Entry point. Parses ARGV, finds the right command, runs it.
258
258
  # Command names are Rake-style colon paths: "build", "db:migrate",
259
259
  # "db:users:list".
260
+ #
261
+ # Rake-style chained dispatch: `hammer build + deploy + notify`.
262
+ # A bare `+` argv token separates commands; `++` escapes to a literal
263
+ # `+` positional. Quoted shell args (`--foo="a + b"`) arrive as a
264
+ # single token and are not split.
260
265
  def start(argv = ARGV)
261
266
  # Track prereqs fired during this top-level invocation so a `needs`
262
267
  # chain runs each prereq at most once. Nested `start` calls (e.g.
263
- # `needs` -> `hammer_*` -> `start`) share the set; the outermost
264
- # call owns its lifetime.
268
+ # `needs` -> `hammer` -> `start`, or a `+` chain) share the set;
269
+ # the outermost call owns its lifetime.
265
270
  outer = Thread.current[:hammer_needs_ran].nil?
266
271
  Thread.current[:hammer_needs_ran] ||= {}
267
272
  Thread.current[:hammer_before_ran] ||= {}
268
273
 
274
+ split_chain(argv).each { |seg| dispatch(seg) }
275
+ ensure
276
+ Thread.current[:hammer_needs_ran] = nil if outer
277
+ Thread.current[:hammer_before_ran] = nil if outer
278
+ end
279
+
280
+ private
281
+
282
+ # Split argv on bare `+` tokens; unescape `++` -> `+` within each
283
+ # segment. Returns [argv] when no chain operator is present.
284
+ def split_chain(argv)
285
+ return [argv] unless argv.include?('+') || argv.include?('++')
286
+ segs = argv.slice_when { |a, b| a == '+' || b == '+' }
287
+ .map { |seg| seg.reject { |t| t == '+' } }
288
+ .map { |seg| seg.map { |t| t == '++' ? '+' : t } }
289
+ .reject(&:empty?)
290
+ segs.empty? ? [argv] : segs
291
+ end
292
+
293
+ # Run a single (already-split) command segment.
294
+ def dispatch(argv)
269
295
  argv = argv.dup
270
296
  name = argv.shift
271
297
 
@@ -283,11 +309,10 @@ class Hammer
283
309
  Shell.print_error("unknown command: #{name}")
284
310
  print_help
285
311
  exit 1
286
- ensure
287
- Thread.current[:hammer_needs_ran] = nil if outer
288
- Thread.current[:hammer_before_ran] = nil if outer
289
312
  end
290
313
 
314
+ public
315
+
291
316
  # Find a command by canonical name or alt within this class.
292
317
  def find_command(name)
293
318
  commands[name.to_s] || commands.values.find { |c| c.matches?(name) }
@@ -313,23 +338,22 @@ class Hammer
313
338
  klass
314
339
  end
315
340
 
316
- # MyCli.hammer_db_users_list("a", verbose: true) ->
317
- # MyCli.start(["db:users:list", "a", "--verbose"])
341
+ # Programmatic dispatch by name. Useful for scripting and tests.
342
+ #
343
+ # MyCli.hammer :build -> start(["build"])
344
+ # MyCli.hammer 'db:users:list' -> start(["db:users:list"])
345
+ # MyCli.hammer :eval, 'puts 42' -> start(["eval", "puts 42"])
346
+ # MyCli.hammer :build, env: 'prod' -> start(["build", "--env=prod"])
347
+ # MyCli.hammer :build, verbose: true -> start(["build", "--verbose"])
348
+ # MyCli.hammer :build, no_cache: true -> start(["build", "--no-cache"])
349
+ # MyCli.hammer :build, cache: false -> skipped (no-op)
318
350
  #
319
- # Useful for scripting and tests. Underscores in the method name map
320
- # to colons; underscores in kwarg keys map to dashes in the flag.
321
- def method_missing(name, *args, **kwargs, &block)
322
- str = name.to_s
323
- return super unless str.start_with?('hammer_')
324
-
325
- path = str.sub(/^hammer_/, '').tr('_', ':')
326
- argv = [path, *args.map(&:to_s)]
327
- # kwarg key mirrors the CLI flag literally:
328
- # verbose: true -> --verbose
329
- # no_cache: true -> --no-cache (just the general rule)
330
- # env: 'prod' -> --env=prod
331
- # anything: false -> skipped (no-op; use `no_x: true` to negate)
332
- kwargs.each do |k, v|
351
+ # Symbols are single-segment names; pass a string with colons for
352
+ # namespaced paths. Trailing positionals become positional ARGV.
353
+ # Underscores in option keys become dashes in flags.
354
+ def hammer(name, *args, **opts)
355
+ argv = [name.to_s, *args.map(&:to_s)]
356
+ opts.each do |k, v|
333
357
  next if v == false
334
358
  flag = "--#{k.to_s.tr('_', '-')}"
335
359
  argv << (v == true ? flag : "#{flag}=#{v}")
@@ -337,10 +361,6 @@ class Hammer
337
361
  start(argv)
338
362
  end
339
363
 
340
- def respond_to_missing?(name, include_private = false)
341
- name.to_s.start_with?('hammer_') || super
342
- end
343
-
344
364
  # Yield [full_colon_path, Command] for every command in this class
345
365
  # and all nested namespaces.
346
366
  def each_command(prefix = nil, &block)
@@ -442,7 +462,7 @@ class Hammer
442
462
  def print_command_list(klass, prefix = nil)
443
463
  rows = []
444
464
  # Commands without a `desc` are hidden from listings but still
445
- # dispatchable + `hammer_*`-callable - useful for private helpers
465
+ # dispatchable + `hammer`-callable - useful for private helpers
446
466
  # invoked from `before` hooks or other commands (e.g. `:env`, `:app`).
447
467
  klass.each_command(prefix) { |full, c| rows << [full, c] unless c.desc.empty? }
448
468
  return if rows.empty?
@@ -530,20 +550,15 @@ class Hammer
530
550
  #
531
551
  # define :deploy do
532
552
  # proc do |opts|
533
- # hammer_build
534
- # hammer_db_migrate(pretend: true)
553
+ # hammer :build
554
+ # hammer 'db:migrate', pretend: true
535
555
  # end
536
556
  # end
537
- def method_missing(name, *args, **kwargs, &block)
538
- return super unless name.to_s.start_with?('hammer_')
539
- # Dispatch from the root class so `hammer_a_b` resolves against the
540
- # full colon path "a:b" even when called inside a namespaced command
541
- # (where self.class would be the namespace subclass).
542
- self.class.root.send(name, *args, **kwargs, &block)
543
- end
544
-
545
- def respond_to_missing?(name, include_private = false)
546
- name.to_s.start_with?('hammer_') || super
557
+ #
558
+ # Dispatches from the root class so colon paths resolve against the
559
+ # full tree even when called from inside a namespaced command.
560
+ def hammer(name, *args, **opts)
561
+ self.class.root.hammer(name, *args, **opts)
547
562
  end
548
563
 
549
564
  # ----- block DSL -----------------------------------------------------
@@ -575,11 +590,27 @@ class Hammer
575
590
  path = find_hammerfile(Dir.pwd)
576
591
  unless path
577
592
  Shell.print_error "no Hammerfile found in #{Dir.pwd} or any parent directory"
593
+
594
+ # Heuristic: *.rb files referencing `Hammer.` are likely inline CLIs
595
+ # the user could promote into a Hammerfile.
596
+ excludes = %w[.git node_modules tmp vendor coverage dist build]
597
+ .map { |d| "--exclude-dir=#{d}" }.join(' ')
598
+ candidates = `grep -rl --include='*.rb' #{excludes} 'Hammer\\.' . 2>/dev/null`
599
+ .lines.map(&:strip).reject(&:empty?)
600
+ unless candidates.empty?
601
+ Shell.say "possible CLI implementation(s) - files referencing `Hammer.`:", :yellow
602
+ candidates.first(10).each { |f| Shell.say " #{f.sub(%r{\A\./}, '')}" }
603
+ Shell.say ''
604
+ end
605
+
578
606
  Shell.say "create one - example:"
607
+ puts
579
608
  Shell.say <<~RUBY
580
609
  define :hello do
581
610
  desc 'say hello'
582
- proc { |opts| say "hello \#{opts[:args].first || 'world'}", :green }
611
+ proc do |opts|
612
+ say.green "hello \#{opts[:args].first || 'world'}"
613
+ end
583
614
  end
584
615
  RUBY
585
616
  exit 1
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lux-hammer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dino Reic