module SoberSwag
  module Serializer
    ##
    # Base class for everything that provides serialization functionality in SoberSwag.
    # SoberSwag serializers transform Ruby types into JSON types, with some associated *schema*.
    # This schema is then used in the generated OpenAPI V3 documentation.
    class Base
      ##
      # Return a new serializer that is an *array* of elements of this serializer.
      # This serializer will take in an array, and use `self` to serialize every element.
      #
      # @return [SoberSwag::Serializer::Array]
      def array
        SoberSwag::Serializer::Array.new(self)
      end

      ##
      # Returns a serializer that will pass `nil` values on unscathed.
      # That means that if you try to serialize `nil` with it, it will result in a JSON `null`.
      # @return [SoberSwag::Serializer::Optional]
      def optional
        SoberSwag::Serializer::Optional.new(self)
      end

      alias nilable optional

      ##
      # Add metadata onto the *type* of a serializer.
      # Note that this *returns a new serializer with metadata added* and does not perform mutation.
      # @param hash [Hash] the metadata to set.
      # @return [SoberSwag::Serializer::Meta] a serializer with metadata added
      def meta(hash)
        SoberSwag::Serializer::Meta.new(self, hash)
      end

      ##
      # Get a new serializer that will first run the given block before serializing an object.
      # For example, if you have a serializer for strings called `StringSerializer`,
      # and you want to serialize `Date` objects via encoding them to a standardized string format,
      # you can use:
      #
      # ```
      #   DateSerializer = StringSerializer.via_map do |date|
      #     date.strftime('%Y-%m-%d')
      #   end
      # ```
      #
      # @yieldparam [Object] the object before serialization
      # @yieldreturn [Object] a transformed object, that will
      #   be passed to {#serialize}
      # @return [SoberSwag::Serializer::Mapped] the new serializer
      def via_map(&block)
        SoberSwag::Serializer::Mapped.new(self, block)
      end

      ##
      # Is this type lazily defined?
      #
      # If we have two serializers that are *mutually recursive*, we need to do some "fun" magic to make that work.
      # This comes up in a case like:
      #
      # ```ruby
      #   SchoolClass = SoberSwag::OutputObject.define do
      #     field :name, primitive(:String)
      #     view :detail do
      #       field :students, -> { Student.view(:base) }
      #     end
      #   end
      #
      #   Student = SoberSwag::OutputObject.define do
      #     field :name, primitive(:String)
      #     view :detail do
      #       field :classes, -> { SchoolClass.view(:base) }
      #     end
      #   end
      # ```
      #
      # This would result in an infinite loop if we tried to define the type struct the easy way.
      # So, we instead use mutation to achieve "laziness."
      def lazy_type?
        false
      end

      ##
      # The lazy version of this type, for mutual recursion.
      # @see #lazy_type? for why this is needed
      #
      # Once you call {#finalize_lazy_type!}, the type will be "fleshed out," and can be actually used.
      def lazy_type
        type
      end

      ##
      # Finalize a lazy type.
      #
      # Should be idempotent: call it once, and it does nothing on subsequent calls (but returns the type).
      def finalize_lazy_type!
        type
      end

      ##
      # Serialize an object.
      # @abstract
      def serialize(_object, _options = {})
        raise ArgumentError, 'not implemented!'
      end

      ##
      # Get the type that we serialize to.
      # @abstract
      def type
        raise ArgumentError, 'not implemented!'
      end

      ##
      # Returns self.
      #
      # This exists due to a hack.
      def serializer
        self
      end

      ##
      # @overload identifier()
      #   Returns the external identifier, used to uniquely identify this object within
      #   the schemas section of an OpenAPI v3 document.
      #   @return [String] the identifier.
      # @overload identifier(arg)
      #   Sets the external identifier to use to uniquely identify
      #   this object within the schemas section of an OpenAPI v3 document.
      #   @param arg [String] the identifier to use
      #   @return [String] the identifer set
      def identifier(arg = nil)
        @identifier = arg if arg
        @identifier
      end
    end
  end
end