Class: Synthra::OpenAPIImporter
- Inherits:
-
Object
- Object
- Synthra::OpenAPIImporter
- Defined in:
- lib/synthra/openapi_importer.rb
Overview
OpenAPI/Swagger Schema Importer
Converts OpenAPI specifications to Synthra schemas.
Constant Summary collapse
- TYPE_MAP =
OpenAPI type to Synthra type mapping
{ # Strings "string" => "text", "string:email" => "email", "string:uri" => "url", "string:url" => "url", "string:uuid" => "uuid", "string:date" => "date", "string:date-time" => "timestamp", "string:password" => "password", "string:byte" => "text", "string:binary" => "text", "string:hostname" => "domain", "string:ipv4" => "ip", "string:ipv6" => "ipv6", # Numbers "integer" => "number", "integer:int32" => "number", "integer:int64" => "number", "number" => "float", "number:float" => "float", "number:double" => "float", # Boolean "boolean" => "boolean", # Complex "array" => "array", "object" => "object" }.freeze
Instance Attribute Summary collapse
-
#spec ⇒ Object
readonly
Returns the value of attribute spec.
-
#spec_path ⇒ Object
readonly
Returns the value of attribute spec_path.
Instance Method Summary collapse
- #components_schemas ⇒ Object private
- #escape_regex(pattern) ⇒ Object private
- #extract_ref_name(ref) ⇒ Object private
-
#import ⇒ Hash<String, String>
Import all schemas from the OpenAPI spec.
-
#import_to_directory(output_dir) ⇒ Array<String>
Import and write to directory.
-
#initialize(spec_path) ⇒ OpenAPIImporter
constructor
Create a new importer.
- #load_spec ⇒ Object private
- #property_to_type(prop_def) ⇒ Object private
- #schema_to_dsl(name, definition) ⇒ Object private
- #to_snake_case(str) ⇒ Object private
Constructor Details
#initialize(spec_path) ⇒ OpenAPIImporter
Create a new importer
59 60 61 62 |
# File 'lib/synthra/openapi_importer.rb', line 59 def initialize(spec_path) @spec_path = spec_path @spec = load_spec end |
Instance Attribute Details
#spec ⇒ Object (readonly)
Returns the value of attribute spec.
53 54 55 |
# File 'lib/synthra/openapi_importer.rb', line 53 def spec @spec end |
#spec_path ⇒ Object (readonly)
Returns the value of attribute spec_path.
53 54 55 |
# File 'lib/synthra/openapi_importer.rb', line 53 def spec_path @spec_path end |
Instance Method Details
#components_schemas ⇒ Object (private)
125 126 127 128 129 |
# File 'lib/synthra/openapi_importer.rb', line 125 def components_schemas @spec.dig("components", "schemas") || @spec.dig("definitions") || # Swagger 2.0 {} end |
#escape_regex(pattern) ⇒ Object (private)
218 219 220 |
# File 'lib/synthra/openapi_importer.rb', line 218 def escape_regex(pattern) pattern.gsub('"', '\\"') end |
#extract_ref_name(ref) ⇒ Object (private)
213 214 215 216 |
# File 'lib/synthra/openapi_importer.rb', line 213 def extract_ref_name(ref) # Handle #/components/schemas/User or #/definitions/User ref.split("/").last end |
#import ⇒ Hash<String, String>
Import all schemas from the OpenAPI spec
68 69 70 71 72 73 74 75 76 77 |
# File 'lib/synthra/openapi_importer.rb', line 68 def import schemas = {} components_schemas.each do |name, definition| dsl = schema_to_dsl(name, definition) schemas[name] = dsl if dsl end schemas end |
#import_to_directory(output_dir) ⇒ Array<String>
Import and write to directory
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/synthra/openapi_importer.rb', line 84 def import_to_directory(output_dir) FileUtils.mkdir_p(output_dir) created = [] base = File.(output_dir) import.each do |name, dsl| # SECURITY (path traversal): a schema name from the spec is attacker-controlled. basename # strips any "../" path components and the allowlist drops anything left, so the file can only # land inside output_dir; the expand_path assertion is a belt-and-suspenders backstop. safe = File.basename(to_snake_case(name.to_s)).gsub(/[^a-z0-9_]+/i, "_") # :nocov: defensive backstop — File.basename + the gsub allowlist always leave a non-empty # token for any non-empty schema name, so this empty-fallback is unreachable in practice. safe = "schema" if safe.empty? # :nocov: path = File.join(output_dir, "#{safe}.dsl") # :nocov: defensive backstop — basename already neutralizes "../" traversal, so this # expand_path containment check can never fail for a name that produced a safe basename. unless File.(path).start_with?(base + File::SEPARATOR) raise Synthra::Errors::SecurityError, "Refusing to write outside #{output_dir}: #{name.inspect}" end # :nocov: File.write(path, dsl) created << path end created end |
#load_spec ⇒ Object (private)
115 116 117 118 119 120 121 122 123 |
# File 'lib/synthra/openapi_importer.rb', line 115 def load_spec content = File.read(@spec_path) if @spec_path.end_with?(".json") JSON.parse(content) else YAML.safe_load(content, permitted_classes: [Date, Time]) end end |
#property_to_type(prop_def) ⇒ Object (private)
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/synthra/openapi_importer.rb', line 149 def property_to_type(prop_def) type = prop_def["type"] format = prop_def["format"] # Handle references if prop_def["$ref"] ref = prop_def["$ref"] return extract_ref_name(ref) end # Handle enums if prop_def["enum"] values = prop_def["enum"].join(", ") return "enum(#{values})" end # Handle arrays if type == "array" items_type = property_to_type(prop_def["items"] || {}) min_items = prop_def["minItems"] || 0 max_items = prop_def["maxItems"] || 10 return "array(#{items_type}, #{min_items}..#{max_items})" end # Handle nested objects if type == "object" if prop_def["properties"] # Inline object - use object type return "object" end return "object" end # Check for string constraints if type == "string" # Check for specific patterns if (pattern = prop_def["pattern"]) return "regex(\"#{escape_regex(pattern)}\")" end # Check for length constraints min_len = prop_def["minLength"] max_len = prop_def["maxLength"] if min_len || max_len range = "#{min_len || 1}..#{max_len || 100}" return "text(#{range})" end end # Check for number constraints if %w[integer number].include?(type) min_val = prop_def["minimum"] max_val = prop_def["maximum"] if min_val || max_val range = "#{min_val || 0}..#{max_val || 1000}" return "number(#{range})" end end # Look up in type map key = format ? "#{type}:#{format}" : type TYPE_MAP[key] || TYPE_MAP[type] || "text" end |
#schema_to_dsl(name, definition) ⇒ Object (private)
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/synthra/openapi_importer.rb', line 131 def schema_to_dsl(name, definition) return nil unless definition["type"] == "object" || definition["properties"] lines = ["#{name}:"] properties = definition["properties"] || {} required = definition["required"] || [] properties.each do |prop_name, prop_def| type = property_to_type(prop_def) optional = required.include?(prop_name) ? "" : "?" nullable = prop_def["nullable"] ? "?" : "" lines << " #{prop_name}#{optional}: #{type}#{nullable}" end lines.join("\n") end |
#to_snake_case(str) ⇒ Object (private)
222 223 224 225 226 |
# File 'lib/synthra/openapi_importer.rb', line 222 def to_snake_case(str) str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') .gsub(/([a-z\d])([A-Z])/, '\1_\2') .downcase end |