Class: Synthra::Export::Typescript

Inherits:
Base
  • Object
show all
Defined in:
lib/synthra/export/typescript.rb

Overview

Exports schema as TypeScript interfaces

Examples:

Export a schema

exporter = Typescript.new(schema, registry: registry)
typescript = exporter.export

Constant Summary collapse

TYPE_MAPPINGS =

Type mappings from DSL types to TypeScript types

{
  # Strings
  "uuid" => "string",
  "ulid" => "string",
  "email" => "string",
  "url" => "string",
  "ip" => "string",
  "ipv6" => "string",
  "date" => "string",
  "past_date" => "string",
  "future_date" => "string",
  "timestamp" => "string",
  "text" => "string",
  "name" => "string",
  "full_name" => "string",
  "first_name" => "string",
  "last_name" => "string",
  "phone" => "string",
  "city" => "string",
  "country" => "string",
  "country_code" => "string",
  "postal_code" => "string",
  "state" => "string",
  "street" => "string",
  "address" => "string",
  "username" => "string",
  "social_handle" => "string",
  "domain" => "string",
  "user_agent" => "string",
  "mac_address" => "string",
  "iban" => "string",
  "credit_card" => "string",
  "currency" => "string",
  "currency_code" => "string",
  "paragraph" => "string",
  "sentence" => "string",
  "word" => "string",
  "message_text" => "string",
  "hashtag_text" => "string",
  "snowflake_id" => "string",
  "numeric_string_id" => "string",

  # Numbers (integers + floats both map to TS number)
  "number" => "number",
  "integer" => "number",
  "age" => "number",
  "airport_elevation" => "number",
  "discount" => "number",
  "formula" => "number",
  "row_number" => "number",
  "sequence" => "number",
  "id_sequence" => "number",
  "float" => "number",
  "money" => "number",
  "latitude" => "number",
  "longitude" => "number",
  "airport_latitude" => "number",
  "airport_longitude" => "number",
  "product_price" => "number",
  "tax_rate" => "number",
  "transaction_amount" => "number",

  # Boolean
  "boolean" => "boolean",

  # Date (string in JSON)
  "date_between" => "string",
  "mobile_device_release_date" => "string",
  "now" => "string",
  "datetime" => "string",

  # Objects
  "object" => "Record<string, unknown>",
  "map_by_field" => "Record<string, unknown>",

  # Arrays
  "array" => "unknown[]",
  "empty_array" => "never[]",
  "json_array" => "unknown[]",
  "indices_pair" => "[number, number]"
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

This class inherits a constructor from Synthra::Export::Base

Class Method Details

.export_all(registry, **options) ⇒ String

Export all schemas in a registry

Parameters:

  • registry (Registry)

    the registry

  • options (Hash)

    export options

Returns:

  • (String)

    TypeScript interfaces



125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/synthra/export/typescript.rb', line 125

def self.export_all(registry, **options)
  lines = []
  lines << "// Generated from Synthra schemas"
  lines << "// Schemas: #{registry.names.join(', ')}"
  lines << ""

  registry.schemas.each do |name, schema|
    exporter = new(schema, registry: registry, **options)
    lines << exporter.build_interface(schema)
    lines << ""
  end

  lines.join("\n")
end

Instance Method Details

#build_array_type(args) ⇒ Object (private)



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/synthra/export/typescript.rb', line 246

def build_array_type(args)
  element = args[:element]
  if element
    if schema_reference?(element.to_s)
      "#{element}[]"
    elsif TYPE_MAPPINGS[element.to_s]
      "#{TYPE_MAPPINGS[element.to_s]}[]"
    else
      # Unknown element types are string-producing types (e.g. array(string)).
      "string[]"
    end
  else
    "unknown[]"
  end
end

#build_const_type(args) ⇒ Object (private)



232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/synthra/export/typescript.rb', line 232

def build_const_type(args)
  value = args[:value]
  case value
  when String
    "\"#{value}\""
  when TrueClass, FalseClass
    value.to_s
  when Numeric
    value.to_s
  else
    "\"#{value}\""
  end
end

#build_enum_type(args) ⇒ Object (private)



227
228
229
230
# File 'lib/synthra/export/typescript.rb', line 227

def build_enum_type(args)
  values = args[:values]&.map { |v| v.respond_to?(:value) ? v.value : v } || []
  values.map { |v| "\"#{v}\"" }.join(" | ")
end

#build_field_comment(field) ⇒ Object (private)



286
287
288
289
290
291
292
293
294
295
296
# File 'lib/synthra/export/typescript.rb', line 286

def build_field_comment(field)
  return nil unless options[:comments]

  comments = []
  comments << "@type #{field.type_name}" if field.type_name
  comments << "@optional" if field.optional?
  comments << "@nullable" if field.nullable?

  return nil if comments.empty?
  "/** #{comments.join(' ')} */"
end

#build_interface(target_schema) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/synthra/export/typescript.rb', line 140

def build_interface(target_schema)
  lines = []
  
  if target_schema.deprecated?
    lines << "/** @deprecated #{target_schema.deprecation_message || 'This interface is deprecated'} */"
  end

  lines << "interface #{target_schema.name} {"

  target_schema.fields.each do |field|
    ts_type = field_to_typescript(field)
    optional = field.optional? ? "?" : ""
    nullable = field.nullable? ? " | null" : ""
    
    # Add JSDoc for complex fields
    comment = build_field_comment(field)
    lines << "  #{comment}" if comment
    
    lines << "  #{field.name}#{optional}: #{ts_type}#{nullable};"
  end

  lines << "}"
  lines.join("\n")
end

#build_map_by_field_type(args) ⇒ Object (private)



271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/synthra/export/typescript.rb', line 271

def build_map_by_field_type(args)
  entries = args[:entries] || {}
  if entries.empty?
    "Record<string, unknown>"
  else
    # Create a more specific type for known entries
    entry_types = entries.map do |name, info|
      schema_name = info.is_a?(Hash) ? (info[:schema] || info["schema"]) : info.to_s
      type = schema_reference?(schema_name) ? schema_name : "unknown"
      "#{name}: #{type}"
    end
    "{ [key: string]: { #{entry_types.join('; ')} }[typeof key] }"
  end
end

#build_one_of_type(args) ⇒ Object (private)



262
263
264
265
266
267
268
269
# File 'lib/synthra/export/typescript.rb', line 262

def build_one_of_type(args)
  schemas = args[:schemas] || []
  types = schemas.map do |s|
    schema_name = s.is_a?(Hash) ? (s[:schema] || s["schema"]) : s.to_s
    schema_reference?(schema_name) ? schema_name : "unknown"
  end
  types.join(" | ")
end

#collect_field_schemas(field, schemas) ⇒ Object (private)



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/synthra/export/typescript.rb', line 175

def collect_field_schemas(field, schemas)
  type_name = field.type_name

  if schema_reference?(type_name)
    ref_schema = get_schema(type_name)
    if ref_schema && !schemas.any? { |s| s.name == ref_schema.name }
      schemas << ref_schema
      # Recursively collect nested schemas
      ref_schema.fields.each { |f| collect_field_schemas(f, schemas) }
    end
  end

  if type_name == "array"
    element = field.type_args[:element]
    if element && schema_reference?(element.to_s)
      ref_schema = get_schema(element.to_s)
      if ref_schema && !schemas.any? { |s| s.name == ref_schema.name }
        schemas << ref_schema
        ref_schema.fields.each { |f| collect_field_schemas(f, schemas) }
      end
    end
  end
end

#collect_referenced_schemasObject (private)



167
168
169
170
171
172
173
# File 'lib/synthra/export/typescript.rb', line 167

def collect_referenced_schemas
  schemas = []
  schema.fields.each do |field|
    collect_field_schemas(field, schemas)
  end
  schemas.uniq(&:name)
end

#exportObject



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/synthra/export/typescript.rb', line 96

def export
  lines = []

  # Add header comment
  lines << "// Generated from Synthra schema: #{schema.name}"
  lines << "// Version: #{schema.version}" if schema.version
  lines << "// DEPRECATED: #{schema.deprecation_message}" if schema.deprecated?
  lines << ""

  # Export referenced schemas first
  exported = Set.new
  collect_referenced_schemas.each do |ref_schema|
    lines << build_interface(ref_schema)
    lines << ""
    exported << ref_schema.name
  end

  # Export main schema
  lines << build_interface(schema) unless exported.include?(schema.name)

  lines.join("\n")
end

#field_to_typescript(field) ⇒ Object (private)



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/synthra/export/typescript.rb', line 199

def field_to_typescript(field)
  type_name = field.type_name
  type_args = field.type_args

  case type_name
  when "enum"
    build_enum_type(type_args)
  when "const"
    build_const_type(type_args)
  when "array"
    build_array_type(type_args)
  when "one_of"
    build_one_of_type(type_args)
  when "map_by_field"
    build_map_by_field_type(type_args)
  else
    if TYPE_MAPPINGS[type_name]
      TYPE_MAPPINGS[type_name]
    elsif schema_reference?(type_name)
      type_name
    else
      # Most Synthra types generate strings, so an unmapped, non-structural
      # type is a string rather than unknown.
      "string"
    end
  end
end