#### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU General Public License as published the Free
#### Software Foundation, either version 3 of the License, or (at your option)
#### any later version.
####
#### This program is distributed in the hope that it will be useful, but WITHOUT
#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#### FITNESS FOR A PARTICULAR PURPOSE.See the GNU General Public License for
#### more details.
####
#### You should have received a copy of the GNU General Public License along
#### with this program.If not, see<http:####www.gnu.org/licenses/.>
require "../../libremiliacr"
module RemiLib::RSConf
# Base class for all RSConf errors.
class RSConfError < Exception
end
# Represents a stream encoding error.
class RSConfEncodingError < RSConfError
end
# Raised when a Byte-Order-Mark is detected.
class RSConfBOMError < RSConfEncodingError
end
# Raised when an error occurs during parsing and is parsing-related.
class RSConfParseError < RSConfEncodingError
getter line : UInt64 = 1
getter column : UInt64 = 1
def initialize(@line : UInt64, @column : UInt64, msg : String)
super(msg)
end
end
# Raised when an error occurs specifically because of an issue related to
# serialization or deserialization.
class RSConfSerializationError < RSConfError
end
# :nodoc:
ILLEGAL_WHITESPACE = [
8203, # Zero width space
160, # No-break space
8199, # Figure space
5760, # Ogham space mark
8192, # En quad
8193, # Em quad
8194, # En space
8195, # Em space
8196, # Three-per-em space
8197, # Four-per-em space
8198, # Six-per-em space
8200, # Punctuation space
8201, # Thin space
8202, # Hair space
8239, # Narrow no-break space
8287, # Medium mathematical space
12288 # Ideographic space
] of UInt16
# How integers are stored in memory. This effectively puts a maximum
# supported value on integers in RSConf data for this library.
alias RSInteger = Int128
# `RSScalar` wraps a scalar value in RSConf. Scalar values are integers,
# floats, booleans, strings, or the null value.
struct RSScalar
# Returns the value of this scalar.
getter val : RSInteger|Float64|Bool|String|Nil
# Creates a new `RSScalar` instance. The value must fit into an
# `RSInteger`.
def initialize(value : Int)
@val = value.to_i128
end
# :ditto:
def initialize(value : Float32|Float64)
@val = value.to_f64
end
# Creates a new `RSScalar` instance.
def initialize(value : Bool)
@val = value
end
# Creates a new `RSScalar` instance.
def initialize(value : String)
@val = value
end
# Creates a new `RSScalar` instance.
def initialize(value : Nil)
@val = value
end
{% begin %}
{% for pair in [[:RSInteger, :int, :int?, :isInt?],
[:Float64, :float, :float?, :isFloat?],
[:Bool, :bool, :bool?, :isBool?],
[:String, :string, :string?, :isString?]] %}
# Returns the scalar value that has the type `{{pair[0].id}}`. If
# this instance does not hold the correct type, an exception is raised.
def {{pair[1].id}} : {{pair[0].id}}
@val.as({{pair[0].id}})
end
# Returns the scalar value that has the type `{{pair[0].id}}|Nil`. If
# this instance does not hold the correct type, an exception is raised.
def {{pair[2].id}} : {{pair[0].id}}|Nil
@val.as?({{pair[0].id}})
end
# Returns `true` if this instance contains a scalar of the type
# `{{pair[0].id}}`, or `false` otherwise.
def {{pair[3].id}} : Bool
@val.is_a?({{pair[0].id}})
end
{% end %}
{% end %}
# Returns the scalar value that has the type `RSInteger`. If this instance
# does not hold either an integer or float, an exception is raised. If this
# holds a float, it is converted to `RSInteger` using `to_128!`.
def int! : RSInteger
@val.as(RSInteger) || @val.as(Float64).to_i128!
end
# Attempts to return the scalar value that has the type `RSInteger`. If
# this instance does not hold either an integer, this returns `nil`.
#
# This is not the same as a scalar that holds an integer or a null value.
# For that, use `#int?`.
def tryInt : RSInteger?
@val.as?(RSInteger)
end
# Returns `true` if this scalar holds an integer or float value, or `false`
# otherwise.
def isIntable? : Bool
@val.is_a?(RSInteger) || @val.is_a?(Float64)
end
# Returns the scalar value that has the type `Float64`. If this instance
# does not hold either an integer or float, an exception is raised. If this
# holds an integer, it is converted to `Float64` using `to_f64!`.
def float! : Float64
@val.as(Float64) || @val.as(RSInteger).to_f64!
end
# Attempts to return the scalar value that has the type `Float64`. If this
# instance does not hold a float, this returns `nil`.
#
# This is not the same as a scalar that holds a float or a null value. For
# that, use `#float?`.
def tryFloat : Float64?
@val.as?(Float64)
end
# Returns `true` if this scalar holds an integer or float value, or `false`
# otherwise.
def isFloatable? : Bool
@val.is_a?(Float64) || @val.is_a?(RSInteger)
end
# Returns `true` if this scalar holds the null value, or `false` otherwise.
def isNil? : Bool
@val.nil?
end
# Returns this scalar value as a string. Non-string values will be
# converted to string using `#to_s`.
def string! : String
@val.to_s
end
# Converts this instance to RSConf data by writing data using *builder*.
def toRsconf(builder : ::RemiLib::RSConf::Builder)
if @val.nil?
builder.writeNil
else
@val.toRsconf(builder)
end
end
end
# The `RSObject` class holds an object value in RSConf. Object values are
# like hash tables, where they map a string to an RSConf value.
class RSObject
@val : Hash(String, RSScalar|RSObject|RSArray)
delegate :each, to: @val
delegate :has_key?, to: @val
delegate :size, to: @val
# Creates a new `RSObject` instance.
def initialize(@val : Hash(String, RSScalar|RSObject|RSArray))
end
# Creates a new `RSObject` instance by yielding each key and its index to
# the block, then storing the result of the block as the value for that key.
def initialize(keys : Array(String), &)
@val = {} of String => RSScalar|RSObject|RSArray
keys.each_with_index do |k, idx|
newVal = (yield k, idx)
@val[k] = RSScalar.new(newVal)
end
end
# Creates a new `RSObject` instance with no mappings.
def initialize
@val = {} of String => RSScalar|RSObject|RSArray
end
# Same as `Hash#[]`.
def [](key : String) : RSScalar|RSObject|RSArray
@val[key]
end
# Same as `Hash#[]=`.
def []=(key : String, val : RSScalar|RSObject|RSArray) : self
@val[key] = val
self
end
# :ditto:
def []=(key : String, val : Int) : self
@val[key] = RSScalar.new(val)
self
end
# :ditto:
def []=(key : String, val : Float32|Float64) : self
@val[key] = RSScalar.new(val)
self
end
# :ditto:
def []=(key : String, val : Bool) : self
@val[key] = RSScalar.new(val)
self
end
# :ditto:
def []=(key : String, val : String) : self
@val[key] = RSScalar.new(val)
self
end
# :ditto:
def []=(key : String, val : Nil) : self
@val[key] = RSScalar.new(val)
self
end
# :ditto:
def []?(key : String) : Tuple(RSScalar|RSObject|RSArray, Bool)
if @val.has_key?(key)
{@val[key], true}
else
{@val[key], false}
end
end
# Converts this instance to RSConf data by writing data using *builder*.
def toRsconf(builder : ::RemiLib::RSConf::Builder)
builder.writeObject do
@val.each do |k, v|
builder.writePair(k, v)
end
end
end
end
# The `RSArray` class holds an array of RSConf values.
class RSArray
@val : Array(RSScalar|RSObject|RSArray)
delegate :[], to: @val
delegate :[]=, to: @val
delegate :<<, to: @val
delegate :each, to: @val
delegate :size, to: @val
# Creates a new `RSArray` instance.
def initialize(@val : Array(RSScalar|RSObject|RSArray))
end
# Creates a new `RSArray` instance with no elements.
def initialize
@val = Array(RSScalar|RSObject|RSArray).new
end
# Converts this instance to RSConf data by writing data using *builder*.
def toRsconf(builder : ::RemiLib::RSConf::Builder)
builder.writeArray do
@val.each(&.toRsconf(builder))
end
end
end
# Any valid RSConf value.
alias RSValue = RSScalar|RSObject|RSArray
# The type of RSConf values that can appear at the toplevel.
#
# Note that in RSConf, if the root value is an object, it does not require the
# `{` and `}` characters.
alias RSTopLevel = RSObject|RSArray
end