Class: Synthra::Export::Javascript

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

Overview

Exports schema as JavaScript with JSDoc type annotations

Examples:

Export a schema

exporter = Javascript.new(schema, registry: registry)
javascript = exporter.export

Constant Summary collapse

TYPE_MAPPINGS =

Type mappings from DSL types to JSDoc 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 JS 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" => "Object",
  "map_by_field" => "Object",

  # Arrays
  "array" => "Array",
  "empty_array" => "Array",
  "json_array" => "Array",
  "indices_pair" => "Array.<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)

    JavaScript with JSDoc types



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/synthra/export/javascript.rb', line 131

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

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

  lines.join("\n")
end

Instance Method Details

#build_array_type(args) ⇒ Object (private)



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/synthra/export/javascript.rb', line 250

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

#build_const_type(args) ⇒ Object (private)



236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/synthra/export/javascript.rb', line 236

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)



231
232
233
234
# File 'lib/synthra/export/javascript.rb', line 231

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

#build_factory_function(target_schema) ⇒ Object (private)



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/synthra/export/javascript.rb', line 275

def build_factory_function(target_schema)
  lines = []
  lines << "/**"
  lines << " * Create a new #{target_schema.name} object"
  lines << " * @param {Partial<#{target_schema.name}>} [overrides] - Optional field overrides"
  lines << " * @returns {#{target_schema.name}}"
  lines << " */"
  lines << "function create#{target_schema.name}(overrides = {}) {"
  lines << "  return {"

  target_schema.fields.each_with_index do |field, i|
    default = field_default_value(field)
    comma = i < target_schema.fields.length - 1 ? "," : ""
    lines << "    #{field.name}: overrides.#{field.name} ?? #{default}#{comma}"
  end

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

#build_one_of_type(args) ⇒ Object (private)



266
267
268
269
270
271
272
273
# File 'lib/synthra/export/javascript.rb', line 266

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 : "Object"
  end
  "(#{types.join('|')})"
end

#build_typedef(target_schema) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/synthra/export/javascript.rb', line 148

def build_typedef(target_schema)
  lines = []

  # JSDoc typedef
  lines << "/**"
  lines << " * @typedef {Object} #{target_schema.name}"
  if target_schema.deprecated?
    lines << " * @deprecated #{target_schema.deprecation_message || 'This type is deprecated'}"
  end

  target_schema.fields.each do |field|
    js_type = field_to_javascript(field)
    optional = field.optional? ? " (optional)" : ""
    nullable = field.nullable? ? "|null" : ""

    lines << " * @property {#{js_type}#{nullable}} #{field.optional? ? "[#{field.name}]" : field.name}#{optional}"
  end

  lines << " */"
  lines.join("\n")
end

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



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/synthra/export/javascript.rb', line 180

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
      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)



172
173
174
175
176
177
178
# File 'lib/synthra/export/javascript.rb', line 172

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
118
119
120
121
122
123
# File 'lib/synthra/export/javascript.rb', line 96

def export
  lines = []

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

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

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

  # Add factory function
  lines << ""
  lines << build_factory_function(schema)

  lines.join("\n")
end

#field_default_value(field) ⇒ Object (private)



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/synthra/export/javascript.rb', line 296

def field_default_value(field)
  type_name = field.type_name

  return "null" if field.optional? || field.nullable?

  case type_name
  when "uuid", "ulid", "text", "name", "email", "url", "phone", "city", "country", "address"
    "''"
  when "number", "integer", "float", "money", "id_sequence"
    "0"
  when "boolean"
    "false"
  when "array"
    "[]"
  when "object", "map_by_field"
    "{}"
  when "enum"
    values = field.type_args[:values] || []
    values.first ? "'#{values.first.respond_to?(:value) ? values.first.value : values.first}'" : "''"
  when "const"
    value = field.type_args[:value]
    value.is_a?(String) ? "'#{value}'" : value.to_s
  when "timestamp", "now", "date", "past_date", "future_date"
    "new Date().toISOString()"
  else
    schema_reference?(type_name) ? "{}" : "null"
  end
end

#field_to_javascript(field) ⇒ Object (private)



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/synthra/export/javascript.rb', line 203

def field_to_javascript(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"
    "Object.<string, Object>"
  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 the any-type wildcard.
      "string"
    end
  end
end