class RBSJsonSchema::Generator

Constants

Alias

Attributes

generated_schemas[R]
output[R]
path_decls[R]
stderr[R]
stdout[R]
stringify_keys[R]

Public Class Methods

new(stringify_keys:, output:, stdout:, stderr:) click to toggle source
# File lib/rbs_json_schema/generator.rb, line 12
def initialize(stringify_keys:, output:, stdout:, stderr:)
  @stringify_keys = stringify_keys ? true : false
  @output = output
  @stdout = stdout
  @stderr = stderr
  @path_decls = {}
  @generated_schemas = {}
end

Public Instance Methods

generate(uri) click to toggle source

IMPORTANT: Function invoked to generate RBS from JSON schema Generates RBS from JSON schema after validating options & writes to file/STDOUT

# File lib/rbs_json_schema/generator.rb, line 23
def generate(uri)
  # Validate options received from CLI
  validate_options()

  @path_decls[uri.path] ||= RBS::AST::Declarations::Module.new(
    name: generate_type_name_for_uri(uri, module_name: true),
    type_params: RBS::AST::Declarations::ModuleTypeParams.empty,
    members: [],
    self_types: [],
    annotations: [],
    location: nil,
    comment: nil
  )
  generate_rbs(uri, read_from_uri(uri))
end
generate_rbs(uri, document) click to toggle source

IMPORTANT: Function used to generate AST alias declarations from a URI & a schema document

# File lib/rbs_json_schema/generator.rb, line 40
def generate_rbs(uri, document)
  # If schema is already generated for a URI, do not re-generate declarations/types
  if fragment = uri.fragment
    # return if fragment.empty? # If fragment is empty, implies top level schema which is always generated since it is the starting point of the algorithm

    if @generated_schemas.dig(uri.path, fragment.empty? ? "#" : fragment) # Check if types have been generated for a particular path & fragment
      return
    end
  else
    if @generated_schemas.dig(uri.path, "#") # Check if types have been generated for a particular path
      return
    end
  end

  unless document.is_a?(Hash)
    raise ValidationError.new(message: "Invalid JSON Schema: #{document}")
  end

  @generated_schemas[uri.path] ||= {}
  if fragment = uri.fragment
    @generated_schemas[uri.path][fragment.empty? ? "#" : fragment] = true
  else
    @generated_schemas[uri.path]["#"] = true
  end
  # Parse & generate declarations from remaining schema content
  decl = Alias.new(
    name: generate_type_name_for_uri(uri), # Normal type name with no prefix
    type: translate_type(uri, document), # Obtain type of alias by parsing the schema document
    annotations: [],
    location: nil,
    comment: nil
  )
  # Append the declaration if & only if the declaration has a valid RBS::Type assigned
  if @path_decls[uri.path]
    @path_decls[uri.path].members << decl if !decl.type.nil?
  else
    @path_decls[uri.path] = RBS::AST::Declarations::Module.new(
      name: generate_type_name_for_uri(uri, module_name: true),
      type_params: RBS::AST::Declarations::ModuleTypeParams.empty,
      members: [],
      self_types: [],
      annotations: [],
      location: nil,
      comment: nil
    )
    @path_decls[uri.path].members << decl if !decl.type.nil?
  end
end
literal_type(literal) click to toggle source
# File lib/rbs_json_schema/generator.rb, line 89
def literal_type(literal)
  # Assign literal type
  case literal
  when String, Integer, TrueClass, FalseClass
    RBS::Types::Literal.new(literal: literal, location: nil)
  when nil
    RBS::Types::Bases::Nil.new(location: nil)
  else
    raise ValidationError.new(message: "Unresolved literal found: #{literal}")
  end
end
read_from_uri(uri) click to toggle source

Read contents from a URI

# File lib/rbs_json_schema/generator.rb, line 202
def read_from_uri(uri)
  schema_source =
    case uri.scheme
    when "file", nil
      File.read(uri.path)
    when "http", "https"
      Net::HTTP.get(uri)
    else
      raise ValidationError.new(message: "Could not read content from URI: #{uri}")
    end

  schema = JSON.parse(schema_source)

  if (fragment = uri.fragment) && !fragment.empty?
    case
    when fragment.start_with?("/")
      path = fragment.split("/")
      path.shift()

      schema.dig(*path) or
        raise ValidationError.new(message: "JSON pointer doesn't point a schema: #{uri.fragment}")
    else
      raise ValidationError.new(message: "JSON pointer is expected: #{uri.fragment}")
    end
  else
    schema
  end
end
translate_type(uri, schema) click to toggle source

Parse JSON schema & return the `RBS::Types` to be assigned

# File lib/rbs_json_schema/generator.rb, line 106
def translate_type(uri, schema)
  case
  when values = schema["enum"]
    unless values.is_a?(Array)
      raise ValidationError.new(message: "Invalid JSON Schema: enum: #{values}")
    end

    types = values.map { |literal| literal_type(literal) }
    RBS::Types::Union.new(types: types, location: nil)
  when const = schema["const"]
    literal_type(const)
  when schema["type"] == "array" || schema.key?("items")
    case
    when schema["items"].is_a?(Array)
      # tuple
      types = schema["items"].map { |definition| translate_type(uri, definition) }
      RBS::Types::Tuple.new(types: types, location: nil)
    when schema["items"].is_a?(Hash)
      # array
      elem_type = translate_type(uri, schema["items"])
      RBS::BuiltinNames::Array.instance_type(elem_type)
    else
      RBS::BuiltinNames::Array.instance_type(untyped_type)
    end
  when schema["type"] == "object" || schema.key?("properties") || schema.key?("additionalProperties")
    case
    when properties = schema["properties"]
      # @type var properties: json_schema
      fields = properties.each.with_object({}) do |pair, hash|
        # @type var hash: Hash[Symbol, RBS::Types::t]
        key, value = pair

        hash[key.to_sym] = translate_type(uri, value)
      end

      RBS::Types::Record.new(fields: fields, location: nil)
    when prop = schema["additionalProperties"]
      RBS::BuiltinNames::Hash.instance_type(
        RBS::BuiltinNames::String.instance_type,
        translate_type(uri, prop)
      )
    else
      RBS::BuiltinNames::Hash.instance_type(
        RBS::BuiltinNames::String.instance_type,
        untyped_type
      )
    end
  when one_of = schema["oneOf"]
    RBS::Types::Union.new(
      types: one_of.map { |defn| translate_type(uri, defn) },
      location: nil
    )
  when all_of = schema["allOf"]
    RBS::Types::Intersection.new(
      types: all_of.map { |defn| translate_type(uri, defn) },
      location: nil
    )
  when ty = schema["type"]
    case ty
    when "integer"
      RBS::BuiltinNames::Integer.instance_type
    when "number"
      RBS::BuiltinNames::Numeric.instance_type
    when "string"
      RBS::BuiltinNames::String.instance_type
    when "boolean"
      RBS::Types::Bases::Bool.new(location: nil)
    when "null"
      RBS::Types::Bases::Nil.new(location: nil)
    else
      raise ValidationError.new(message: "Invalid JSON Schema: type: #{ty}")
    end
  when ref = schema["$ref"]
    ref_uri =
      begin
        # Parse URI of `$ref`
        URI.parse(schema["$ref"])
      rescue URI::InvalidURIError => _
        raise ValidationError.new(message: "Invalid URI encountered in: $ref = #{ref}")
      end

    resolved_uri = resolve_uri(uri, ref_uri) # Resolve `$ref` URI with respect to current URI
    # Generate AST::Declarations::Alias
    generate_rbs(resolved_uri, read_from_uri(resolved_uri))

    # Assign alias type with appropriate namespace
    RBS::Types::Alias.new(
      name: generate_type_name_for_uri(resolved_uri, namespace: resolved_uri.path != uri.path),
      location: nil
    )
  else
    raise ValidationError.new(message: "Invalid JSON Schema: #{schema.keys.join(", ")}")
  end
end
untyped_type() click to toggle source
# File lib/rbs_json_schema/generator.rb, line 101
def untyped_type
  RBS::Types::Bases::Any.new(location: nil)
end
write_output() click to toggle source

Write output using `RBS::Writer` to a particular `IO`

# File lib/rbs_json_schema/generator.rb, line 232
def write_output
  # If an output directory is given, open a file & write to it
  if output = self.output
    @path_decls.each do |path, decls|
      name = decls.name.name.to_s.underscore
      file_path = File.join(output, "#{name}.rbs")
      File.open(file_path, 'w') do |io|
        stdout.puts "Writing output to file: #{file_path}"
        RBS::Writer.new(out: io).write([decls])
      end
    end
  # If no output directory is given write to STDOUT
  else
    RBS::Writer.new(out: stdout).write(@path_decls.values)
  end
end

Private Instance Methods

generate_type_name_for_uri(uri, module_name: false, namespace: false) click to toggle source

Utility function to assign type name from URI

# File lib/rbs_json_schema/generator.rb, line 250
        def generate_type_name_for_uri(uri, module_name: false, namespace: false)
  dup_uri = uri.dup # Duplicate URI object for processing
  path = dup_uri.path.split("/").last or raise # Extract path
  path.gsub!(/(.json$)?/, '') # Remove JSON file extension if found
  prefix = path.camelize # prefix is used to write module name, hence converted to camel case

  # Return module_name
  if module_name
    return RBS::TypeName.new(
      name: prefix.to_sym,
      namespace: RBS::Namespace.empty
    )
  end

  name = :t

  if fragment = dup_uri.fragment
    unless fragment.empty?
      fragment[0] = "" if fragment.start_with?("/") # Remove initial slash if present in fragment
      name = fragment.downcase.tr("/", "_")
    end
  end

  # Return type name for type alias
  RBS::TypeName.new(
    name: name.to_sym,
    namespace: namespace ? RBS::Namespace.new(path: [prefix.to_sym], absolute: false) : RBS::Namespace.empty
  )
end
resolve_uri(uri, ref_uri) click to toggle source

Utility function to resolve two URIs

# File lib/rbs_json_schema/generator.rb, line 289
        def resolve_uri(uri, ref_uri)
  begin
    # Attempt to merge the two URIs
    uri + ref_uri
  rescue URI::BadURIError, ArgumentError => _
    # Raise error in case of invalid URI
    raise ValidationError.new(message: "Could not resolve URI: #{uri} + #{ref_uri}")
  end
end
type_name(name, absolute: nil) click to toggle source

Returns type name with prefixes & appropriate namespace

# File lib/rbs_json_schema/generator.rb, line 281
        def type_name(name, absolute: nil)
  RBS::TypeName.new(
    name: name.to_sym,
    namespace: absolute ? RBS::Namespace.root : RBS::Namespace.empty
  )
end
validate_options() click to toggle source

Validate options given to the CLI

# File lib/rbs_json_schema/generator.rb, line 300
        def validate_options
  if output = self.output
    path = Pathname(output)
    # Check if a valid directory exists?
    raise ValidationError.new(message: "#{output}: Directory not found!") if !path.directory?
  end
end