Module: Hammer::Input

Defined in:
lib/hammer/input.rb

Overview

Stdin + JSON helpers for opts. Hammer always attaches piped stdin as opts[:stdin]; recipes opt into JSON body handling via Hammer::Input.prepare_json! (typically from a before hook) and/or opt :json, type: :json / global_opt :json, type: :json.

Class Method Summary collapse

Class Method Details

.attach_stdin!(opts) ⇒ Object

Idempotent: sets opts if not already present.



29
30
31
32
33
# File 'lib/hammer/input.rb', line 29

def attach_stdin!(opts)
  return opts[:stdin] if opts.key?(:stdin)

  opts[:stdin] = read_stdin
end

.parse_json(raw, source: 'json') ⇒ Object

Parse a JSON source string into a Hash/Array with symbol keys. source is only used in error messages ('stdin', '--json', path, ...).



37
38
39
40
41
42
# File 'lib/hammer/input.rb', line 37

def parse_json(raw, source: 'json')
  data = JSON.parse(raw)
  symbolize(data)
rescue JSON::ParserError => e
  raise Hammer::Parser::Error, "invalid JSON (#{source}): #{e.message}"
end

.prepare_json!(opts, key: :json) ⇒ Object

Prepare opts as a Hash/Array when possible.

1. If opts[key] is already Hash/Array -> symbolize, done
2. If opts[key] is a String ("...", @file, -) -> parse
3. If opts[key] is nil/absent and opts[:stdin] looks like JSON
 object/array -> fill from stdin
4. Boolean true/false left alone (legacy `opt :json, type: :boolean`)

Returns the resolved value (or nil). Mutates opts.



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/hammer/input.rb', line 80

def prepare_json!(opts, key: :json)
  attach_stdin!(opts)
  key = key.to_sym
  val = opts[key]

  if val.is_a?(Hash) || val.is_a?(Array)
    opts[key] = symbolize(val)
    return opts[key]
  end

  if val.is_a?(String) && !val.empty?
    opts[key] = resolve_json_value(val, opts, source: "--#{key.to_s.tr('_', '-')}")
    return opts[key]
  end

  # Skip booleans (output-mode flags) and other non-nil junk.
  return val unless val.nil?

  raw = opts[:stdin]
  return nil if raw.nil? || raw.empty?

  stripped = raw.lstrip
  return nil unless stripped.start_with?('{', '[')

  opts[key] = parse_json(raw, source: 'stdin')
  opts[key]
end

.read_stdinObject

Read piped stdin once. Returns nil when stdin is a TTY or empty. Does not consume an interactive TTY.



15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/hammer/input.rb', line 15

def read_stdin
  return nil if $stdin.closed?
  return nil if $stdin.tty?

  data = $stdin.read
  return nil if data.nil?

  data = data.dup.force_encoding(Encoding::UTF_8) if data.respond_to?(:force_encoding)
  data.empty? ? nil : data
rescue StandardError
  nil
end

.resolve_json_value(value, opts = {}, source: 'json') ⇒ Object

Resolve a value that may be:

- Hash / Array  -> returned as-is (symbolized if Hash has string keys)
- "-"           -> use opts[:stdin]
- "@path"       -> File.read(path)
- JSON string   -> parse


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/hammer/input.rb', line 49

def resolve_json_value(value, opts = {}, source: 'json')
  return symbolize(value) if value.is_a?(Hash) || value.is_a?(Array)

  s = value.to_s
  if s == '-'
    attach_stdin!(opts)
    raw = opts[:stdin]
    raise Hammer::Parser::Error, "no JSON on stdin for #{source}" if raw.nil? || raw.empty?

    return parse_json(raw, source: 'stdin')
  end

  if s.start_with?('@')
    path = s[1..]
    raise Hammer::Parser::Error, "JSON file not found: #{path}" unless File.file?(path)

    return parse_json(File.read(path), source: path)
  end

  parse_json(s, source: source)
end

.symbolize(obj) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/hammer/input.rb', line 108

def symbolize(obj)
  case obj
  when Hash
    obj.each_with_object({}) do |(k, v), h|
      h[k.respond_to?(:to_sym) ? k.to_sym : k] = symbolize(v)
    end
  when Array
    obj.map { |v| symbolize(v) }
  else
    obj
  end
end