class Campa::Reader

Reads strings or files into Campa expressions. rubocop: disable Metrics/ClassLength

Constants

BOOLS
BOOLS_START
CAST_FLOAT
CAST_INT
SEPARATORS

Public Class Methods

new(input) click to toggle source

Given a String, a file pointer or any #getc, #eof? it allows fetch every valid Campa form from it.

If the String is a valid file path it will be converted into a file pointer. Anything else will be converted into a StringIO

@param input [String, (getc, eof?)]

# File lib/campa/reader.rb, line 17
def initialize(input)
  @input = to_io_like(input)
  next_char
end

Public Instance Methods

next() click to toggle source

Return the next Campa form available in the underlying io like object.

@return [Object] next available Campa form

# File lib/campa/reader.rb, line 26
def next
  eat_separators
  return read if !@input.eof?
  return if @current_char.nil?

  # Exhaust the reader if @input.eof? and !@current_char.nil?
  read.tap { @current_char = nil }
end

Private Instance Methods

boolean?() click to toggle source
# File lib/campa/reader.rb, line 204
def boolean?
  return false if !BOOLS_START.include?(@current_char)

  @current_token = @current_char
  @current_token << next_char until @input.eof? || peek == " " || peek == ")"
  BOOLS.include? @current_token
end
break?() click to toggle source
# File lib/campa/reader.rb, line 189
def break?
  @current_char == "\n"
end
delimiter?() click to toggle source
# File lib/campa/reader.rb, line 193
def delimiter?
  @current_char == ")"
end
digit?(char = nil) click to toggle source
# File lib/campa/reader.rb, line 197
def digit?(char = nil)
  char ||= @current_char
  # TODO: should we force the encoding of source files?
  #   (since codepoints will be different depending on encoding).
  !char.nil? && (char.ord >= 48 && char.ord <= 57)
end
eat_separators() click to toggle source
# File lib/campa/reader.rb, line 68
def eat_separators
  if @input.eof?
    @current_char = nil if separator? || break?
    return
  end

  next_char while separator? || break?
end
next_char() click to toggle source
# File lib/campa/reader.rb, line 51
def next_char
  if @next_char.nil?
    @current_char = @input.getc
  else
    @current_char = @next_char
    @next_char = nil
  end

  @current_char
end
peek() click to toggle source
# File lib/campa/reader.rb, line 62
def peek
  return @next_char if !@next_char.nil?

  @next_char = @input.getc
end
read() click to toggle source

rubocop: disable Metrics/MethodLength, Metrics/PerceivedComplexity rubocop: disable Style/EmptyCaseCondition, Metrics/CyclomaticComplexity

# File lib/campa/reader.rb, line 79
def read
  case
  when @current_char == "\""
    read_string
  when digit? || @current_char == "-" && digit?(peek)
    read_number
  when @current_char == "'"
    read_quotation
  when @current_char == "("
    read_list
  when boolean?
    read_boolean
  else
    read_symbol
  end
end
read_boolean() click to toggle source
# File lib/campa/reader.rb, line 163
def read_boolean
  boolean_value = @current_token
  @current_token = nil
  next_char
  boolean_value == "true"
end
read_list() click to toggle source
# File lib/campa/reader.rb, line 145
def read_list
  # eats the opening (
  next_char

  elements = []
  while !@input.eof? && !delimiter?
    token = self.next
    elements << token
    eat_separators if separator?
  end
  raise Error::MissingDelimiter, ")" if !delimiter?

  # eats the closing )
  next_char

  List.new(*elements)
end
read_number() click to toggle source
# File lib/campa/reader.rb, line 117
def read_number
  number = @current_char
  cast = CAST_INT

  until @input.eof?
    next_char
    break if separator? || delimiter?

    cast = CAST_FLOAT if @current_char == "."
    number << @current_char
  end

  safe_cast(number, cast)
end
read_quotation() click to toggle source
# File lib/campa/reader.rb, line 138
def read_quotation
  # eats the ' char
  next_char

  List.new(SYMBOL_QUOTE, self.next)
end
read_string() click to toggle source

rubocop: enable Metrics/MethodLength, Metrics/PerceivedComplexity rubocop: enable Style/EmptyCaseCondition, Metrics/CyclomaticComplexity

# File lib/campa/reader.rb, line 98
def read_string
  return if @input.eof?

  string = ""
  # eats the opening "
  next_char

  while !@input.eof? && @current_char != "\""
    string << @current_char
    next_char
  end
  raise Error::MissingDelimiter, "\"" if @current_char != "\""

  # eats the closing "
  next_char

  string
end
read_symbol() click to toggle source
# File lib/campa/reader.rb, line 170
def read_symbol
  label = @current_token || @current_char
  @current_token = nil

  until @input.eof?
    next_char
    break if separator? || delimiter? || break?

    label << @current_char
  end

  # TODO: validate symbol (raise if invalid chars are present)
  Symbol.new(label)
end
safe_cast(number, cast) click to toggle source
# File lib/campa/reader.rb, line 132
def safe_cast(number, cast)
  cast.call(number)
rescue ArgumentError
  raise Error::InvalidNumber, number
end
separator?() click to toggle source
# File lib/campa/reader.rb, line 185
def separator?
  SEPARATORS.include? @current_char
end
to_io_like(input) click to toggle source
# File lib/campa/reader.rb, line 43
def to_io_like(input)
  return input if input.respond_to?(:getc) && input.respond_to?(:eof?)
  return File.new(input) if File.file?(input)

  # TODO: check if it is "castable" first,
  StringIO.new(input)
end