generic_reader 0.5.0
generic_reader: ^0.5.0 copied to clipboard
Methods for the systematic reading of compile-time constant expressions.
Generic Reader #
Introduction #
The premise of source code generation is that we can specify (hopefully few) details and create libraries, classes, variables, and methods during the build process.
Source code generation relies heavily on constants known at compile time,
represented by a DartObject
.
For built-in types, DartObject
has methods that
allow reading the underlying constant object to obtain a runtime object.
The package generic_reader
includes an extention on
DartObject
that simplifies reading constants of
type bool
, double
,int
,num
,String
,Symbol
,Type
,
List
, Set
, Map
,Iterabel
, Enum
and provides a systematic way of reading arbitrary constants of known
data-type.
Usage #
To use the package generic_reader
the following steps are required:
-
Include
generic_reader
as a dependencies in your pubspec.yaml file. -
Register a Decoder object for each user defined data-type
T
that is going to be read. Note: The built-in typesbool
,double
,int
,num
,String
,Type
,Symbol
do not require a decoder. -
Use Dart's static
analyzer
to read a library, get the relevantVariableElement
, and calculate the constant expression represented by aDartObject
using the methodcomputeConstantValue()
. -
Read the compile-time constant values using the extension methods
read<T>
,readList<T>
,readIterator<T>
readSet<T>
,readMap<T>
. -
Use the constant values to generate the source-code and complete the building process.
Decoder #
The extension Reader
provides a systematic method of
retrieving constants of
arbitrary data-types by allowing users to register Decoder
objects.
Decoder<T>
is an abstract
parameterized class with a method T read<T>(DartObject obj)
that attempt to read a variable of type T
from obj
and return the result.
The example below demonstrates how to create a custom decode for the
sample class Annotation
and register an instance of the decoder with
the extension Reader
.
import 'package:generic_reader/generic_reader.dart';
class Annotation {
const A({required this.id, required this.names,);
final int id;
final Set<String> names;
@override
String toString() => 'A(id: $id, names: $names)';
}
class AnnotationDecoder extends Decoder<Annotation> {
const AnnotationDecoder();
@override
Annotation read(DartObject obj) {
final id = obj.read<int>(fieldName: 'id');
final names = obj.readSet<String>(fieldName: 'names');
return A(id: id, names: names);
}
}
Read.addDecoder(const AnnotationDecoder());
The example below show how to register a decoder for a Dart Enum
and read
an instance of the enumeration. In this case, instead of creating a custom
decoder class we just register an instance of the already defined generic class
EnumDecoder
:
Click to show source-code.
import 'package:ansi_modifier/ansi_modifier.dart';
import 'package:build_test/build_test.dart' show resolveSource;
import 'package:generic_reader/generic_reader.dart';
/// Demonstrates how to use [Reader] to read an enum.
enum Order { asc, desc }
Future<void> main() async {
print('\nReading library: example\n');
// Read the dart library
final lib = await resolveSource(
r'''
library example;
enum Order { asc, desc }
class A {
const A();
final Order order = Order.asc;
}
''',
(resolver) => resolver.findLibraryByName('example'),
readAllSourcesFromFilesystem: false,
);
if (lib == null) return;
/// Add a decoder for the enum:
Reader.addDecoder(const EnumDecoder<Order>(Order.values));
/// Compute the compile-time constant value
final enumObj = lib.classes[0].fields[0].computeConstantValue();
/// Read the compile-time constant value to obtain a runtime instance of the
/// enumeration.
final enum0 = enumObj?.read<Order>();
print(
'\nReading an enum of type ${'Order'.style(Ansi.green)}: '
'$enum0\n',
);
}
The program listed below show how to read a constant of type
List<List<String>>
:
Click to show source-code.
import 'package:ansi_modifier/ansi_modifier.dart';
import 'package:build_test/build_test.dart' show resolveSource;
import 'package:generic_reader/generic_reader.dart';
/// Demonstrates how to use [Reader] to read a nested list.
Future<void> main() async {
print('\nReading library: example\n');
final lib = await resolveSource(
r'''
library example;
class A {
const A();
final nestedList = List<List<String>> [['a'], ['b']];
}
''',
(resolver) => resolver.findLibraryByName('example'),
readAllSourcesFromFilesystem: false,
);
if (lib == null) return;
final listOfString = 'List<String>'.style(Ansi.green);
final listOfListOfString = 'List<List<String>>'.style(Ansi.green);
print('\nAdding decoder for $listOfString and $listOfListOfString\n');
Reader.addDecoder(const ListDecoder<String>());
Reader.addDecoder(const ListDecoder<List<String>>());
print(Reader.info);
final listObj = lib.classes[0].fields[0].computeConstantValue();
final list1 = listObj?.read<List<List<String>>>();
final list2 = listObj?.read();
final list3 = listObj?.readList<List<String>>();
print('\nlistObj.read<$listOfListOfString>: $list1');
print('\nlistObj.read(): $list2');
print('\nlistObj.readList<$listOfString>(): $list3\n');
}
The program above produces the following terminal output:
Click to show terminal output.
$ dart example/bin/list_example.dart
Reading library: example
0s _ResolveSourceBuilder<LibraryElement?> on 5 inputs; $package$
1s _ResolveSourceBuilder<LibraryElement?> on 5 inputs: 5 no-op
Built with build_runner in 1s; wrote 0 outputs.
Adding decoder for List<String> and List<List<String>>
Reader:
Registered types: (bool, double, int, num,
String, Symbol, Type, List<String>, List<List<String>>)
Mapped types : {}
listObj.read<List<List<String>>>: [[a], [b]]
listObj.read(): [[a], [b]]
listObj.readList<List<String>>(): [[a], [b]]
Limitations #
-
Constants retrievable with
Reader
must have a built-in Dart type, a type made available by depending on a package, or a type defined in the file being read. -
Defining decoder functions for each data-type has its obvious limitiations when it comes to generic types. In practice, however, generic classes are often designed in such a manner that only few type parameters are valid or likely to be useful. Constants that need to be retrieved during the source-generation process are most likely annotations and simple data-types that convey information to source code generators.
Examples #
For further information on how to use Reader to retrieve constants of arbitrary type see example.
Features and bugs #
Please file feature requests and bugs at the issue tracker.