Module: Terminalwire::V2::Conformance::Sexp

Defined in:
lib/terminalwire/v2/conformance.rb

Overview

A tiny S-expression reader + tape interpreter. The whole grammar:

list = "(" form* ")" ;  atom = token | "string" | :keyword | number | true|false|nil ; ; comment

Data mapping (unambiguous by each list's shape):

(type :k v ...) FRAME -> {"t"=>type, k=>v}   (:k v ...) MAP -> {k=>v}
(a b c) LIST -> [a,b,c]                       (bin "b64") -> bytes

A tape file is (tape NAME (ROLE ...config) ...). The transcript is flat and reads like a recording; #interpret groups it into the step shape the runners consume (emit:[...], reject / out:[...], exit, stdout).

Class Method Summary collapse

Class Method Details

.action(form) ⇒ Object



218
219
220
221
222
223
224
225
226
# File 'lib/terminalwire/v2/conformance.rb', line 218

def action(form)
  head = form[0]
  rest = form[1..]
  if rest.length == 1 && !rest[0].is_a?(Symbol) && !rest[0].is_a?(Array)
    { head => rest[0] }
  else
    { head => to_map(rest) }
  end
end

.atom(text) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/terminalwire/v2/conformance.rb', line 144

def atom(text)
  case text
  when "true" then true
  when "false" then false
  when "nil" then nil
  else
    if text.start_with?(":") then text[1..].to_sym
    elsif text.match?(/\A-?\d+\z/) then text.to_i
    elsif text.match?(/\A-?\d+\.\d+\z/) then text.to_f
    else text
    end
  end
end

.group_client(forms) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/terminalwire/v2/conformance.rb', line 205

def group_client(forms)
  steps = []
  forms.each do |f|
    case f[0]
    when "process" then steps << { "process" => value(f[1]), "out" => [] }
    when "out" then steps.last["out"] << value(f[1])
    when "exit" then steps.last["exit"] = f[1]
    when "stdout" then steps.last["stdout"] = f[1]
    end
  end
  steps
end

.group_server(forms) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/terminalwire/v2/conformance.rb', line 188

def group_server(forms)
  steps = []
  forms.each do |f|
    case f[0]
    when "recv" then steps << { "recv" => value(f[1]), "emit" => [] }
    when "do" then steps << { "do" => action(f[1]), "emit" => [] }
    when "send" then steps.last["emit"] << { "send" => value(f[1]) }
    when "event"
      ev = { "event" => f[1].to_s }
      ev["data"] = to_map(f[2..]) unless f[2..].empty?
      steps.last["emit"] << ev
    when "reject" then steps.last["reject"] = true
    end
  end
  steps
end

.interpret(form) ⇒ Object



180
181
182
183
184
185
186
# File 'lib/terminalwire/v2/conformance.rb', line 180

def interpret(form)
  _tape, name, config_form, *transcript = form
  role = config_form[0]
  config = to_map(config_form[1..])
  steps = role == "client" ? group_client(transcript) : group_server(transcript)
  { "name" => name, "role" => role, "config" => config, "tape" => steps }
end

.load(text) ⇒ Object



82
83
84
# File 'lib/terminalwire/v2/conformance.rb', line 82

def load(text)
  read_all(text).map { |form| interpret(form) }
end

.read_all(text) ⇒ Object



86
87
88
89
90
91
# File 'lib/terminalwire/v2/conformance.rb', line 86

def read_all(text)
  toks = tokenize(text)
  forms = []
  forms << read_form(toks) until toks.empty?
  forms
end

.read_form(toks) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/terminalwire/v2/conformance.rb', line 130

def read_form(toks)
  t = toks.shift
  if t == "("
    list = []
    list << read_form(toks) until toks.first == ")"
    toks.shift
    list
  elsif t.is_a?(Array) && t[0] == :str
    t[1]
  else
    atom(t[1])
  end
end

.to_map(pairs) ⇒ Object



174
175
176
177
178
# File 'lib/terminalwire/v2/conformance.rb', line 174

def to_map(pairs)
  h = {}
  pairs.each_slice(2) { |k, v| h[k.to_s] = value(v) }
  h
end

.tokenize(str) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/terminalwire/v2/conformance.rb', line 93

def tokenize(str)
  toks = []
  i = 0
  n = str.length
  while i < n
    c = str[i]
    if c == ";"
      i += 1 while i < n && str[i] != "\n"
    elsif c =~ /\s/
      i += 1
    elsif c == "(" || c == ")"
      toks << c
      i += 1
    elsif c == '"'
      j = i + 1
      buf = +""
      while j < n && str[j] != '"'
        if str[j] == "\\"
          buf << { "n" => "\n", "t" => "\t", "r" => "\r" }.fetch(str[j + 1], str[j + 1])
          j += 2
        else
          buf << str[j]
          j += 1
        end
      end
      toks << [:str, buf]
      i = j + 1
    else
      j = i
      j += 1 while j < n && !"() \t\r\n;\"".include?(str[j])
      toks << [:tok, str[i...j]]
      i = j
    end
  end
  toks
end

.value(form) ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/terminalwire/v2/conformance.rb', line 158

def value(form)
  return form unless form.is_a?(Array)
  return [] if form.empty?

  head = form[0]
  if head.is_a?(Symbol)
    to_map(form)
  elsif head == "bin"
    Base64.decode64(form[1]).b
  elsif form[1..].any? { |e| e.is_a?(Symbol) }
    { "t" => head }.merge(to_map(form[1..]))
  else
    form.map { |e| value(e) }
  end
end