# frozen_string_literal: true module LIT module Builder # @api private # @since 0.1.0 class Object AST = Parser::AST def initialize(ast, target_module) @ast = ast @target_module = target_module @type_checker = TypeChecker.new(target_module) end def build @ast.statements.map { |stmt| build_statement(stmt) } end private def build_statement(stmt) if stmt.is_a?(AST::EndpointStatement) build_endpoint(stmt) elsif stmt.is_a?(AST::TypeDeclarationStatement::Struct) build_struct(stmt) elsif stmt.is_a?(AST::TypeDeclarationStatement::Enum) build_enum(stmt) elsif stmt.is_a?(AST::TypeDeclarationStatement::TypeAlias) build_type_alias(stmt) else raise InvalidASTError, "invalid statement: #{stmt}" end end def build_endpoint(endpoint) request = make_type(endpoint.request) response = make_type(endpoint.response) working_module = @target_module endpoint_namespace_stack = endpoint.name.split(".").map { |x| Utils.camelize(x) } endpoint_namespace_stack.each do |namespace| m = Module.new Utils.const_reset(working_module, namespace, m) working_module = m end Utils.const_reset(working_module, "Request", request) Utils.const_reset(working_module, "Response", response) Utils.const_reset(working_module, "DefinedIn", @target_module) end def build_struct(struct) klass = make_struct(struct.fields) Utils.const_reset(@target_module, struct.name, klass) end def build_enum(enum) mod = make_enum(enum.variants) Utils.const_reset(@target_module, enum.name, mod) end def build_type_alias(type_alias) object = make_type(type_alias.type) Utils.const_reset(@target_module, type_alias.name, object) end def make_struct(fields) type_checker = @type_checker Class.new(::LIT::Object::Struct) do define_method(:initialize) do |args| fields.each do |(field_name, field_type)| value = args.fetch(field_name) type_checker.check_type!(field_type, value) instance_variable_set(:"@#{field_name}", value) end end fields.keys.each do |field_name| define_method(field_name) { instance_variable_get(:"@#{field_name}") } end define_singleton_method(:__fields__) do fields.reduce({}) do |acc, (field_name, field_type)| acc.merge(field_name => field_type) end end end end # rubocop:disable Metrics/MethodLength def make_enum(variants) mod = Module.new do include ::LIT::Object::Enum end variants.each do |(variant_name, variant)| variant_name = Utils.camelize(variant_name) if variant == AST::EnumVariant::Unit Utils.const_reset(mod, variant_name, Module.new do include ::LIT::Object::EnumVariant define_singleton_method(:__parent__) { mod } define_singleton_method(:__kind__) { :unit } define_singleton_method(:__name__) { variant_name } end) elsif variant.is_a?(AST::EnumVariant::Struct) klass = make_struct(variant.fields) klass.include(::LIT::Object::EnumVariant) klass.define_method(:__parent__) { mod } klass.define_method(:__kind__) { :struct } klass.define_singleton_method(:__kind__) { :struct } klass.define_method(:__name__) { variant_name } Utils.const_reset(mod, variant_name, klass) else raise InvalidASTError, "invalid enum variant type: #{variant}" end end mod end def make_type(type) object = if type.is_a?(AST::Type::Primitive::Map) Map.new( @target_module, type.key_type, type.value_type, ).build elsif type.is_a?(AST::Type::Primitive::Option) Option.new(@target_module, type.type).build elsif type.is_a?(AST::Type::Primitive::Array) Array.new(@target_module, type.type).build elsif type.is_a?(AST::Type::AnonymousStruct) make_struct(type.fields) elsif type.is_a?(AST::Type::AnonymousEnum) make_enum(type.variants) elsif type == AST::Type::Primitive::String || type == AST::Type::Primitive::Integer || type == AST::Type::Primitive::Float || type == AST::Type::Primitive::Boolean Module.new elsif type.is_a?(AST::Type::Alias) target_module = @target_module Class.new do define_singleton_method(:const_missing) do |name| target_module.const_get(type.name).const_get(name) end define_method(:initialize) do |*args| @inner = target_module.const_get(type.name).new(*args) end define_method(:method_missing) do |*args| @inner.send(*args) end end else raise InvalidASTError, "invalid type: #{type}" end Utils.const_reset(object, "TYPE", type) object end # rubocop:enable Metrics/MethodLength end end end