# <!-- rdoc-file=struct.c -->
# Class Data provides a convenient way to define simple classes for value-alike
# objects.
#
# The simplest example of usage:
#
#     Measure = Data.define(:amount, :unit)
#
#     # Positional arguments constructor is provided
#     distance = Measure.new(100, 'km')
#     #=> #<data Measure amount=100, unit="km">
#
#     # Keyword arguments constructor is provided
#     weight = Measure.new(amount: 50, unit: 'kg')
#     #=> #<data Measure amount=50, unit="kg">
#
#     # Alternative form to construct an object:
#     speed = Measure[10, 'mPh']
#     #=> #<data Measure amount=10, unit="mPh">
#
#     # Works with keyword arguments, too:
#     area = Measure[amount: 1.5, unit: 'm^2']
#     #=> #<data Measure amount=1.5, unit="m^2">
#
#     # Argument accessors are provided:
#     distance.amount #=> 100
#     distance.unit #=> "km"
#
# Constructed object also has a reasonable definitions of #== operator, #to_h
# hash conversion, and #deconstruct / #deconstruct_keys to be used in pattern
# matching.
#
# ::define method accepts an optional block and evaluates it in the context of
# the newly defined class. That allows to define additional methods:
#
#     Measure = Data.define(:amount, :unit) do
#       def <=>(other)
#         return unless other.is_a?(self.class) && other.unit == unit
#         amount <=> other.amount
#       end
#
#       include Comparable
#     end
#
#     Measure[3, 'm'] < Measure[5, 'm'] #=> true
#     Measure[3, 'm'] < Measure[5, 'kg']
#     # comparison of Measure with Measure failed (ArgumentError)
#
# Data provides no member writers, or enumerators: it is meant to be a storage
# for immutable atomic values. But note that if some of data members is of a
# mutable class, Data does no additional immutability enforcement:
#
#     Event = Data.define(:time, :weekdays)
#     event = Event.new('18:00', %w[Tue Wed Fri])
#     #=> #<data Event time="18:00", weekdays=["Tue", "Wed", "Fri"]>
#
#     # There is no #time= or #weekdays= accessors, but changes are
#     # still possible:
#     event.weekdays << 'Sat'
#     event
#     #=> #<data Event time="18:00", weekdays=["Tue", "Wed", "Fri", "Sat"]>
#
# See also Struct, which is a similar concept, but has more container-alike API,
# allowing to change contents of the object and enumerate it.
#
class Data
  # <!--
  #   rdoc-file=struct.c
  #   - define(*symbols) -> class
  # -->
  # Defines a new Data class.
  #
  #     measure = Data.define(:amount, :unit)
  #     #=> #<Class:0x00007f70c6868498>
  #     measure.new(1, 'km')
  #     #=> #<data amount=1, unit="km">
  #
  #     # It you store the new class in the constant, it will
  #     # affect #inspect and will be more natural to use:
  #     Measure = Data.define(:amount, :unit)
  #     #=> Measure
  #     Measure.new(1, 'km')
  #     #=> #<data Measure amount=1, unit="km">
  #
  # Note that member-less Data is acceptable and might be a useful technique for
  # defining several homogeneous data classes, like
  #
  #     class HTTPFetcher
  #       Response = Data.define(:body)
  #       NotFound = Data.define
  #       # ... implementation
  #     end
  #
  # Now, different kinds of responses from `HTTPFetcher` would have consistent
  # representation:
  #
  #     #<data HTTPFetcher::Response body="<html...">
  #     #<data HTTPFetcher::NotFound>
  #
  # And are convenient to use in pattern matching:
  #
  #     case fetcher.get(url)
  #     in HTTPFetcher::Response(body)
  #       # process body variable
  #     in HTTPFetcher::NotFound
  #       # handle not found case
  #     end
  #
  def self.define: [KLASS < _DataClass] (*Symbol) ?{ (KLASS) [self: KLASS] -> void } -> KLASS

  interface _DataClass
    # <!--
    #   rdoc-file=struct.c
    #   - new(*args) -> instance
    #   - new(**kwargs) -> instance
    #   - ::[](*args) -> instance
    #   - ::[](**kwargs) -> instance
    # -->
    # Constructors for classes defined with ::define accept both positional and
    # keyword arguments.
    #
    #     Measure = Data.define(:amount, :unit)
    #
    #     Measure.new(1, 'km')
    #     #=> #<data Measure amount=1, unit="km">
    #     Measure.new(amount: 1, unit: 'km')
    #     #=> #<data Measure amount=1, unit="km">
    #
    #     # Alternative shorter intialization with []
    #     Measure[1, 'km']
    #     #=> #<data Measure amount=1, unit="km">
    #     Measure[amount: 1, unit: 'km']
    #     #=> #<data Measure amount=1, unit="km">
    #
    # All arguments are mandatory (unlike Struct), and converted to keyword
    # arguments:
    #
    #     Measure.new(amount: 1)
    #     # in `initialize': missing keyword: :unit (ArgumentError)
    #
    #     Measure.new(1)
    #     # in `initialize': missing keyword: :unit (ArgumentError)
    #
    # Note that `Measure#initialize` always receives keyword arguments, and that
    # mandatory arguments are checked in `initialize`, not in `new`. This can be
    # important for redefining initialize in order to convert arguments or provide
    # defaults:
    #
    #     Measure = Data.define(:amount, :unit) do
    #       NONE = Data.define
    #
    #       def initialize(amount:, unit: NONE.new)
    #         super(amount: Float(amount), unit:)
    #       end
    #     end
    #
    #     Measure.new('10', 'km') # => #<data Measure amount=10.0, unit="km">
    #     Measure.new(10_000)     # => #<data Measure amount=10000.0, unit=#<data NONE>>
    #
    %a{annotate:rdoc:copy:Data.new}
    def new: (*untyped) -> Data
           | (**untyped) -> Data

    %a{annotate:rdoc:copy:Data.[]}
    def []: (*untyped) -> Data
          | (**untyped) -> Data

    # <!--
    #   rdoc-file=struct.c
    #   - DataClass::members -> array_of_symbols
    # -->
    # Returns an array of member names of the data class:
    #
    #     Measure = Data.define(:amount, :unit)
    #     Measure.members # => [:amount, :unit]
    #
    %a{annotate:rdoc:copy:Data.members}
    def members: () -> Array[Symbol]
  end

  %a{annotate:rdoc:skip}
  def self.new: () -> bot

  def self.allocate: () -> bot

  # <!--
  #   rdoc-file=struct.c
  #   - self == other -> true or false
  # -->
  # Returns  `true` if `other` is the same class as `self`, and all members are
  # equal.
  #
  # Examples:
  #
  #     Measure = Data.define(:amount, :unit)
  #
  #     Measure[1, 'km'] == Measure[1, 'km'] #=> true
  #     Measure[1, 'km'] == Measure[2, 'km'] #=> false
  #     Measure[1, 'km'] == Measure[1, 'm']  #=> false
  #
  #     Measurement = Data.define(:amount, :unit)
  #     # Even though Measurement and Measure have the same "shape"
  #     # their instances are never equal
  #     Measure[1, 'km'] == Measurement[1, 'km'] #=> false
  #
  def ==: (untyped) -> bool

  # <!--
  #   rdoc-file=struct.c
  #   - deconstruct     -> array
  # -->
  # Returns the values in `self` as an array, to use in pattern matching:
  #
  #     Measure = Data.define(:amount, :unit)
  #
  #     distance = Measure[10, 'km']
  #     distance.deconstruct #=> [10, "km"]
  #
  #     # usage
  #     case distance
  #     in n, 'km' # calls #deconstruct underneath
  #       puts "It is #{n} kilometers away"
  #     else
  #       puts "Don't know how to handle it"
  #     end
  #     # prints "It is 10 kilometers away"
  #
  # Or, with checking the class, too:
  #
  #     case distance
  #     in Measure(n, 'km')
  #       puts "It is #{n} kilometers away"
  #     # ...
  #     end
  #
  def deconstruct: () -> Array[untyped]

  # <!--
  #   rdoc-file=struct.c
  #   - deconstruct_keys(array_of_names_or_nil) -> hash
  # -->
  # Returns a hash of the name/value pairs, to use in pattern matching.
  #
  #     Measure = Data.define(:amount, :unit)
  #
  #     distance = Measure[10, 'km']
  #     distance.deconstruct_keys(nil) #=> {:amount=>10, :unit=>"km"}
  #     distance.deconstruct_keys([:amount]) #=> {:amount=>10}
  #
  #     # usage
  #     case distance
  #     in amount:, unit: 'km' # calls #deconstruct_keys underneath
  #       puts "It is #{amount} kilometers away"
  #     else
  #       puts "Don't know how to handle it"
  #     end
  #     # prints "It is 10 kilometers away"
  #
  # Or, with checking the class, too:
  #
  #     case distance
  #     in Measure(amount:, unit: 'km')
  #       puts "It is #{amount} kilometers away"
  #     # ...
  #     end
  #
  def deconstruct_keys: (?Array[Symbol]?) -> Hash[Symbol, untyped]

  # <!--
  #   rdoc-file=struct.c
  #   - self.eql?(other) -> true or false
  # -->
  # Equality check that is used when two items of data are keys of a Hash.
  #
  # The subtle difference with #== is that members are also compared with their
  # #eql? method, which might be important in some cases:
  #
  #     Measure = Data.define(:amount, :unit)
  #
  #     Measure[1, 'km'] == Measure[1.0, 'km'] #=> true, they are equal as values
  #     # ...but...
  #     Measure[1, 'km'].eql? Measure[1.0, 'km'] #=> false, they represent different hash keys
  #
  # See also Object#eql? for further explanations of the method usage.
  #
  def eql?: (untyped) -> bool

  # <!--
  #   rdoc-file=struct.c
  #   - hash -> integer
  # -->
  # Redefines Object#hash (used to distinguish objects as Hash keys) so that data
  # objects of the same class with same content would have the same `hash` value,
  # and represented the same Hash key.
  #
  #     Measure = Data.define(:amount, :unit)
  #
  #     Measure[1, 'km'].hash == Measure[1, 'km'].hash #=> true
  #     Measure[1, 'km'].hash == Measure[10, 'km'].hash #=> false
  #     Measure[1, 'km'].hash == Measure[1, 'm'].hash #=> false
  #     Measure[1, 'km'].hash == Measure[1.0, 'km'].hash #=> false
  #
  #     # Structurally similar data class, but shouldn't be considered
  #     # the same hash key
  #     Measurement = Data.define(:amount, :unit)
  #
  #     Measure[1, 'km'].hash == Measurement[1, 'km'].hash #=> false
  #
  def hash: () -> Integer

  # <!--
  #   rdoc-file=struct.c
  #   - inspect -> string
  #   - to_s -> string
  # -->
  # Returns a string representation of `self`:
  #
  #     Measure = Data.define(:amount, :unit)
  #
  #     distance = Measure[10, 'km']
  #
  #     p distance  # uses #inspect underneath
  #     #<data Measure amount=10, unit="km">
  #
  #     puts distance  # uses #to_s underneath, same representation
  #     #<data Measure amount=10, unit="km">
  #
  def inspect: () -> String

  # <!--
  #   rdoc-file=struct.c
  #   - members -> array_of_symbols
  # -->
  # Returns the member names from `self` as an array:
  #
  #     Measure = Data.define(:amount, :unit)
  #     distance = Measure[10, 'km']
  #
  #     distance.members #=> [:amount, :unit]
  #
  def members: () -> Array[Symbol]

  # <!--
  #   rdoc-file=struct.c
  #   - to_h -> hash
  #   - to_h {|name, value| ... } -> hash
  # -->
  # Returns Hash representation of the data object.
  #
  #     Measure = Data.define(:amount, :unit)
  #     distance = Measure[10, 'km']
  #
  #     distance.to_h
  #     #=> {:amount=>10, :unit=>"km"}
  #
  # Like Enumerable#to_h, if the block is provided, it is expected to produce
  # key-value pairs to construct a hash:
  #
  #     distance.to_h { |name, val| [name.to_s, val.to_s] }
  #     #=> {"amount"=>"10", "unit"=>"km"}
  #
  # Note that there is a useful symmetry between #to_h and #initialize:
  #
  #     distance2 = Measure.new(**distance.to_h)
  #     #=> #<data Measure amount=10, unit="km">
  #     distance2 == distance
  #     #=> true
  #
  def to_h: () -> Hash[Symbol, untyped]

  # <!-- rdoc-file=struct.c -->
  # Returns a string representation of `self`:
  #
  #     Measure = Data.define(:amount, :unit)
  #
  #     distance = Measure[10, 'km']
  #
  #     p distance  # uses #inspect underneath
  #     #<data Measure amount=10, unit="km">
  #
  #     puts distance  # uses #to_s underneath, same representation
  #     #<data Measure amount=10, unit="km">
  #
  alias to_s inspect

  # <!--
  #   rdoc-file=struct.c
  #   - with(**kwargs) -> instance
  # -->
  # Returns a shallow copy of `self` --- the instance variables of `self` are
  # copied, but not the objects they reference.
  #
  # If the method is supplied any keyword arguments, the copy will be created with
  # the respective field values updated to use the supplied keyword argument
  # values. Note that it is an error to supply a keyword that the Data class does
  # not have as a member.
  #
  #     Point = Data.define(:x, :y)
  #
  #     origin = Point.new(x: 0, y: 0)
  #
  #     up = origin.with(x: 1)
  #     right = origin.with(y: 1)
  #     up_and_right = up.with(y: 1)
  #
  #     p origin       # #<data Point x=0, y=0>
  #     p up           # #<data Point x=1, y=0>
  #     p right        # #<data Point x=0, y=1>
  #     p up_and_right # #<data Point x=1, y=1>
  #
  #     out = origin.with(z: 1) # ArgumentError: unknown keyword: :z
  #     some_point = origin.with(1, 2) # ArgumentError: expected keyword arguments, got positional arguments
  #
  def with: (**untyped) -> instance
end
