annotation Chem::RegisterFormat

Overview

Registers a file format.

The annotated type provides the implementation of a file format that encodes an encoded type. The file format is determined from the annotated type's name, where the last component of the fully qualified name is used (e.g., Baz for Foo::Bar::Baz). An entry of the same name will be added to the Format enum, where the declared extensions and file patterns will be associated with the corresponding file format. This annotation accepts the following named arguments:

The ability to read or write is determined by the declaration of reader and writer classes, respectively, which must include the FormatReader, FormatWriter and other related mixins when appropiate. The encoded type is dictated by the type variable of the included mixins. The FormatReader::Headed and FormatReader::Attached provides interfaces to read additional information into custom objects.

Convenience read (.from_* and .read) and write (#to_* and #write) methods will be generated on the encoded types during compilation time using the type information of the included mixins in the reader and writer classes. Types declared by the FormatReader::Headed and FormatReader::Attached are also considered encoded types. Additionally, convenience read and write methods will be generated on Array for file formats that can hold multiple entries, which are declared via the FormatReader::MultiEntry and FormatWriter::MultiEntry mixins.

Example

The following code registers the Foo format associated with the Chem::Foo module, which provides read and write capabilities of A via the Reader and Writer classes, respectively. Both declare that the Foo format can hold multiple entries via the corresponding MultiEntry mixins. The reader class also provides support for reading the header information into a B instance and reading secondary information into a C instance.

record A
record B
record C

@[Chem::RegisterFormat(ext: %w(.foo), names: %w(foo_*))]
module Foo
  class Reader
    include FormatReader(A)
    include FormatReader::MultiEntry(A)
    include FormatReader::Headed(B)
    include FormatReader::Attached(C)

    protected def decode_attached : C
      C.new
    end

    protected def decode_entry : A
      A.new
    end

    protected def decode_headed : B
      B.new
    end
  end

  class Writer
    include FormatWriter(A)
    include FormatWriter::MultiEntry(A)

    def encode_entry(frame : A) : Nil; end
  end
end

A member named Foo is added to the Format enum, which can be used to query the format during runtime using the .from_* methods.

Chem::Format::Foo                      # => Foo
Chem::Format::Foo.extnames             # => [".foo"]
Chem::Format::Foo.file_patterns        # => ["foo_*"]
Chem::Format.from_filename("file.foo") # => Foo
Chem::Format.from_filename("foo_1")    # => Foo

The convenience A.from_foo and A.read methods are generated during compilation time to create an A instance from an IO or file using the Foo file format. The latter can be specified via the corresponding member of the Format enum or as a string. Additionally, the file format can be guessed from the filename.

# static read methods (can forward arguments to Foo::Reader)
A.from_foo(IO::Memory.new) # => A()
A.from_foo("a.foo")        # => A()

# dynamic read methods (format is detected on runtime; no arguments)
A.read(IO::Memory.new, Chem::Format::Foo) # => A()
A.read(IO::Memory.new, "foo")             # => A()
A.read("a.foo", Chem::Format::Foo)        # => A()
A.read("a.foo", "foo")                    # => A()
A.read("a.foo")                           # => A()

The above methods are also created on the types representing the header (B) and attached (C) types. This is convenient since one does not to worry about if X is either the encoded type, header or attached type to be read from a Foo file.

Similar to the read methods, A.to_foo and A.write are generated to write an A instance to an IO or file using the Foo file format.

# static read methods (can forward arguments to Foo::Writer)
A.new.to_foo                 # returns a string representation
A.new.to_foo(IO::Memory.new) # writes to an IO
A.new.to_foo("a.foo")        # writes to a file

# dynamic read methods (format is detected on runtime; no arguments)
A.new.write(IO::Memory.new, Chem::Format::A)
A.new.write(IO::Memory.new, "foo")
A.new.write("a.foo", Chem::Format::A)
A.new.write("a.foo", "foo")
A.new.write("a.foo")

These methods are not generated for header (B) and attached (C) types however, because these cannot produce a valid Foo file by themselves. If a header/attached object is required to write a valid file, it should be declared as a required argument in the writer (see Cube::Writer or VASP::Chgcar::Writer).

Since Foo::Reader and Foo::Writer reads and writes multiple entries (indicated by the corresponding MultiEntry mixins), the .from_foo, .read, #to_foo, and #write methods are also generated in Array during compilation time.

Array(A).from_foo(IO::Memory.new)   # => [Foo(), ...]
Array(A).from_foo("a.foo")          # => [Foo(), ...]
Array(A).read(IO::Memory.new, :foo) # => [Foo(), ...]
# and other overloads

Array(A).new.to_foo                     # returns a string representation
Array(A).new.to_foo(IO::Memory.new)     # writes to an IO
Array(A).new.to_foo("a.foo")            # writes to a file
Array(A).new.write IO::Memory.new, :foo # writes to an IO
# and other overloads

Calling any of these methods on an array of unsupported types will produce a missing method error during compilation.

Refer to the implementations of the supported file formats (e.g., PDB and XYZ) for real examples.

Defined in:

chem/register_format.cr