Module: Runar::SDK::Codegen

Defined in:
lib/runar/sdk/codegen.rb

Class Method Summary collapse

Class Method Details

.build_codegen_context(artifact) ⇒ Object


Context builder




206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/runar/sdk/codegen.rb', line 206

def build_codegen_context(artifact) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
  is_stateful    = stateful_artifact?(artifact)
  public_methods = get_public_methods(artifact)

  has_stateful_methods = is_stateful &&
                         public_methods.any? { |m| !terminal_method?(m, is_stateful) }
  has_terminal_methods = public_methods.any? { |m| terminal_method?(m, is_stateful) }

  # Constructor params
  ctor_params = artifact.abi.constructor_params
  constructor_params = ctor_params.each_with_index.map do |p, i|
    {
      'name'    => to_snake_case(p.name),
      'type'    => map_type(p.type),
      'abiType' => p.type,
      'isLast'  => i == ctor_params.length - 1,
    }
  end

  constructor_args_expr = constructor_params.map { |p| p['name'] }.join(', ')

  # Methods
  methods = public_methods.map do |method|
    user_params_raw = get_user_params(method, is_stateful)
    sdk_args_raw    = get_sdk_arg_params(method, is_stateful)
    terminal        = terminal_method?(method, is_stateful)
    method_name     = safe_method_name(method.name)

    user_params = user_params_raw.each_with_index.map do |p, i|
      {
        'name'    => to_snake_case(p['name']),
        'type'    => p['rb_type'],
        'abiType' => p['abi_type'],
        'isLast'  => i == user_params_raw.length - 1,
      }
    end

    # SDK args expression
    sdk_args_parts = sdk_args_raw.map do |p|
      p['hidden'] ? 'nil' : to_snake_case(p['name'])
    end
    sdk_args_expr = sdk_args_parts.join(', ')

    # Sig params (for prepare/finalize)
    sig_params_raw = sdk_args_raw.select { |p| p['abi_type'] == 'Sig' }
    sig_params = sig_params_raw.each_with_index.map do |sp, i|
      idx = sdk_args_raw.index { |p| p['name'] == sp['name'] }
      {
        'name'     => to_snake_case(sp['name']),
        'argIndex' => idx,
        'isLast'   => i == sig_params_raw.length - 1,
      }
    end

    sig_entries_expr = sig_params.map { |sp| "#{sp['argIndex']} => #{sp['name']}" }.join(', ')

    # Prepare params (user params minus Sig)
    prepare_user_params = user_params.reject { |p| p['abiType'] == 'Sig' }
    prepare_user_params = prepare_user_params.each_with_index.map do |p, i|
      p.merge('isLast' => i == prepare_user_params.length - 1)
    end

    {
      'originalName'       => method.name,
      'name'               => method_name,
      'capitalizedName'    => to_pascal_case(method.name),
      'isTerminal'         => terminal,
      'isStatefulMethod'   => !terminal && is_stateful,
      'hasSigParams'       => !sig_params.empty?,
      'hasUserParams'      => !user_params.empty?,
      'userParams'         => user_params,
      'sdkArgsExpr'        => sdk_args_expr,
      'sigParams'          => sig_params,
      'sigEntriesExpr'     => sig_entries_expr,
      'hasPrepareUserParams' => !prepare_user_params.empty?,
      'prepareUserParams'  => prepare_user_params,
    }
  end

  {
    'contractName'         => artifact.contract_name,
    'contractNameSnake'    => to_snake_case(artifact.contract_name),
    'isStateful'           => is_stateful,
    'hasStatefulMethods'   => has_stateful_methods,
    'hasTerminalMethods'   => has_terminal_methods,
    'hasConstructorParams' => !constructor_params.empty?,
    'constructorParams'    => constructor_params,
    'constructorArgsExpr'  => constructor_args_expr,
    'methods'              => methods,
  }
end

.classify_params(method, is_stateful) ⇒ Object


Param classification




144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/runar/sdk/codegen.rb', line 144

def classify_params(method, is_stateful)
  method.params.map do |p|
    hidden = p.type == 'Sig' ||
             (is_stateful && (
               p.type == 'SigHashPreimage' ||
               p.name == '_changePKH' ||
               p.name == '_changeAmount' ||
               p.name == '_newAmount'
             ))
    {
      'name'     => p.name,
      'abi_type' => p.type,
      'rb_type'  => map_type(p.type),
      'hidden'   => hidden,
    }
  end
end

.generate_ruby(artifact) ⇒ String

Generate a typed Ruby wrapper class from a compiled Runar artifact.

The generated class wraps RunarContract and exposes typed methods for each public contract method, with appropriate options types for terminal vs state-mutating methods.

Parameters:

Returns:

  • (String)

    generated Ruby source code



433
434
435
436
# File 'lib/runar/sdk/codegen.rb', line 433

def generate_ruby(artifact)
  context = build_codegen_context(artifact)
  render_mustache(RUBY_TEMPLATE, context)
end

.get_public_methods(artifact) ⇒ Object



198
199
200
# File 'lib/runar/sdk/codegen.rb', line 198

def get_public_methods(artifact)
  artifact.abi.methods.select(&:is_public)
end

.get_sdk_arg_params(method, is_stateful) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
# File 'lib/runar/sdk/codegen.rb', line 166

def get_sdk_arg_params(method, is_stateful)
  classified = classify_params(method, is_stateful)
  return classified unless is_stateful

  classified.reject do |p|
    p['abi_type'] == 'SigHashPreimage' ||
      p['name'] == '_changePKH' ||
      p['name'] == '_changeAmount' ||
      p['name'] == '_newAmount'
  end
end

.get_user_params(method, is_stateful) ⇒ Object



162
163
164
# File 'lib/runar/sdk/codegen.rb', line 162

def get_user_params(method, is_stateful)
  classify_params(method, is_stateful).reject { |p| p['hidden'] }
end

.map_type(abi_type) ⇒ Object



111
112
113
# File 'lib/runar/sdk/codegen.rb', line 111

def map_type(abi_type)
  RUBY_TYPE_MAP.fetch(abi_type, 'Object')
end

.render_mustache(template, context) ⇒ Object



88
89
90
# File 'lib/runar/sdk/codegen.rb', line 88

def render_mustache(template, context)
  render_section(template, context)
end

.render_section(template, context) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/runar/sdk/codegen.rb', line 42

def render_section(template, context)
  result = template
  changed = true

  while changed
    changed = false
    result = result.gsub(SECTION_RE) do
      changed = true
      section_type = ::Regexp.last_match(1)
      key          = ::Regexp.last_match(2)
      body         = ::Regexp.last_match(3)
      value        = resolve(context, key)

      if section_type == '^'
        # Inverted section: render if falsy / empty list
        if !value || (value.is_a?(Array) && value.empty?)
          render_section(body, context)
        else
          ''
        end
      elsif value.is_a?(Array)
        value.map do |item|
          if item.is_a?(Hash)
            render_section(body, context.merge(item))
          else
            render_section(body, context.merge('.' => item))
          end
        end.join
      elsif value.is_a?(Hash)
        render_section(body, context.merge(value))
      elsif value
        render_section(body, context)
      else
        ''
      end
    end
  end

  # Variable interpolation
  result.gsub(VAR_RE) do
    key = ::Regexp.last_match(1)
    value = resolve(context, key)
    value.nil? ? '' : value.to_s
  end
end

.resolve(context, key) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/runar/sdk/codegen.rb', line 28

def resolve(context, key)
  return context['.'] if key == '.'

  parts = key.split('.')
  current = context
  parts.each do |part|
    return nil if current.nil?
    return nil unless current.is_a?(Hash)

    current = current[part]
  end
  current
end

.safe_method_name(name) ⇒ Object



135
136
137
138
# File 'lib/runar/sdk/codegen.rb', line 135

def safe_method_name(name)
  snake = to_snake_case(name)
  SNAKE_RESERVED.include?(snake) ? "call_#{snake}" : snake
end

.stateful_artifact?(artifact) ⇒ Boolean


Artifact analysis


Returns:



194
195
196
# File 'lib/runar/sdk/codegen.rb', line 194

def stateful_artifact?(artifact)
  !artifact.state_fields.empty?
end

.terminal_method?(method, is_stateful) ⇒ Boolean


Terminal detection


Returns:



182
183
184
185
186
187
188
# File 'lib/runar/sdk/codegen.rb', line 182

def terminal_method?(method, is_stateful)
  return true unless is_stateful
  return method.is_terminal unless method.is_terminal.nil?

  # Fallback: terminal if no _changePKH param
  method.params.none? { |p| p.name == '_changePKH' }
end

.to_pascal_case(name) ⇒ Object



126
127
128
129
130
# File 'lib/runar/sdk/codegen.rb', line 126

def to_pascal_case(name)
  return name if name.empty?

  name[0].upcase + name[1..]
end

.to_snake_case(name) ⇒ Object


Name conversion utilities




119
120
121
122
123
124
# File 'lib/runar/sdk/codegen.rb', line 119

def to_snake_case(name)
  name
    .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
    .gsub(/([a-z0-9])([A-Z])/, '\1_\2')
    .downcase
end