From 7925b9ddbdecd49472d576352b42461b32d6fe8c Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 20 Nov 2019 14:38:59 +0100 Subject: [PATCH 01/10] checkpoint before merging all generated code into a single file --- doc/code-generation.md | 4 +- doc/modelinfo.md | 2 +- generator/build.yaml | 13 ++++-- generator/lib/objectbox_generator.dart | 5 +- generator/lib/src/code_chunks.dart | 9 +++- .../{generator.dart => entity_binding.dart} | 46 ++++++++++--------- generator/test/helpers.dart | 4 +- objectbox-model.json | 15 +++++- objectbox_model.dart | 8 ++++ test/entity2.dart | 10 ++++ test/test_env.dart | 1 + 11 files changed, 81 insertions(+), 36 deletions(-) rename generator/lib/src/{generator.dart => entity_binding.dart} (79%) create mode 100644 objectbox_model.dart create mode 100644 test/entity2.dart diff --git a/doc/code-generation.md b/doc/code-generation.md index f957ecd47..25c4f935f 100644 --- a/doc/code-generation.md +++ b/doc/code-generation.md @@ -6,7 +6,7 @@ Unfortunately, only few documentation exists on how to generate code using Dart' ## Basics -In order to set up code generation, a new package needs to be created exclusively for this task. Here, it it called `objectbox_generator`. This package needs to contain a file called [`build.yaml`](/generator/build.yaml) as well as an entry point for the builder, [`objectbox_generator.dart`](/generator/lib/objectbox_generator.dart), and a generator specifically for one annotation class, [`generator.dart`](/generator/lib/src/generator.dart). The latter needs to contain a class which extends `GeneratorForAnnotation` and overrides `Future generateForAnnotatedElement(Element elementBare, ConstantReader annotation, BuildStep buildStep)`, which returns a string containing the generated code for a single annotation instance. +In order to set up code generation, a new package needs to be created exclusively for this task. Here, it it called `objectbox_generator`. This package needs to contain a file called [`build.yaml`](/generator/build.yaml) as well as an entry point for the builder, [`objectbox_generator.dart`](/generator/lib/objectbox_generator.dart), and a generator specifically for one annotation class, [`generator.dart`](/generator/lib/src/entity_binding.dart). The latter needs to contain a class which extends `GeneratorForAnnotation` and overrides `Future generateForAnnotatedElement(Element elementBare, ConstantReader annotation, BuildStep buildStep)`, which returns a string containing the generated code for a single annotation instance. It is then possible to traverse through the annotated class in `generateForAnnotatedElement` and e.g. determine all class member fields and their types. Additionally, such member fields can be annotated themselves, but because here, only the `@Entity` annotation is explicitly handled using a separate generator class, member annotations can be read and processed in line. @@ -28,7 +28,7 @@ The entry function for generator testing is the main function of [`generator_tes The `build` package internally uses designated classes for reading from and writing to files or, to be more general, any kind of _assets_. In this case, we do not want to involve any kind of files as output and only very specific files as input, so it is necessary to create our own versions of the so-called `AssetReader` and `AssetWriter`. -In [`helpers.dart`](/generator/test/helpers.dart), `_InMemoryAssetWriter` is supposed to receive a single output string and store it in memory. Eventually, the string it stores will be the output of [`EntityGenerator`](/generator/lib/src/generator.dart#L15). +In [`helpers.dart`](/generator/test/helpers.dart), `_InMemoryAssetWriter` is supposed to receive a single output string and store it in memory. Eventually, the string it stores will be the output of [`EntityGenerator`](/generator/lib/src/entity_binding.dart#L15). On the other hand, `_SingleFileAssetReader` shall read a single input Dart source file from the [`test/cases`](/generator/test/cases) directory. Note that currently, test cases have the rather ugly file extension `.dart_testcase`, such as [`single_entity.dart_testcase`](/generator/test/cases/single_entity/single_entity.dart_testcase). This is a workaround, because otherwise, running `pub run build_runner build` in the repository's root directory would generate `.g.dart` files from _all_ `.dart` files in the repository. An option to exclude certain directories from `build_runner` is yet to be found. diff --git a/doc/modelinfo.md b/doc/modelinfo.md index 5e38f02db..d2a530748 100644 --- a/doc/modelinfo.md +++ b/doc/modelinfo.md @@ -14,4 +14,4 @@ Nonetheless, the concrete implementation in this repository is documented in the - [`ModelEntity`](/lib/src/modelinfo/modelentity.dart) describes an entity of a model and consists of instances of `ModelProperty` as well as an id, name and last property id - [`ModelInfo`](/lib/src/modelinfo/modelinfo.dart) logically contains an entire ObjectBox model file like [this one](/objectbox-model.json) and thus consists of an array of `ModelEntity` as well as various meta information for ObjectBox and model version information -Such model meta information is only actually needed when generating `objectbox-model.json`, i.e. when `objectbox_generator` is invoked. This is the case in [`generator.dart`](/generator/lib/src/generator.dart#L24). In [generated code](/generator/lib/src/code_chunks.dart#L12), the JSON file is loaded in the same way, but only the `ModelEntity` instances are kept. +Such model meta information is only actually needed when generating `objectbox-model.json`, i.e. when `objectbox_generator` is invoked. This is the case in [`generator.dart`](/generator/lib/src/entity_binding.dart#L24). In [generated code](/generator/lib/src/code_chunks.dart#L12), the JSON file is loaded in the same way, but only the `ModelEntity` instances are kept. diff --git a/generator/build.yaml b/generator/build.yaml index 346aba44e..46fa9d74a 100644 --- a/generator/build.yaml +++ b/generator/build.yaml @@ -1,15 +1,18 @@ +# See docs: +# https://pub.dev/packages/build_config +# https://github.com/dart-lang/build/blob/master/docs/build_yaml_format.md + targets: $default: builders: - objectbox_generator|objectbox: + objectbox_generator|entities: enabled: true builders: - objectbox: - target: ":objectbox_generator" + entities: import: "package:objectbox_generator/objectbox_generator.dart" - builder_factories: ["objectboxModelFactory"] - build_extensions: {".dart": [".objectbox_model.g.part"]} + builder_factories: ["entityBindingBuilder"] + build_extensions: {".dart": [".objectbox_entity.g.part"]} auto_apply: dependents build_to: cache applies_builders: ["source_gen|combining_builder"] diff --git a/generator/lib/objectbox_generator.dart b/generator/lib/objectbox_generator.dart index 1427324ba..cacd60615 100644 --- a/generator/lib/objectbox_generator.dart +++ b/generator/lib/objectbox_generator.dart @@ -1,5 +1,6 @@ import "package:build/build.dart"; import "package:source_gen/source_gen.dart"; -import "package:objectbox_generator/src/generator.dart"; +import "src/entity_binding.dart"; + +Builder entityBindingBuilder(BuilderOptions options) => SharedPartBuilder([EntityGenerator()], "objectbox_entity"); -Builder objectboxModelFactory(BuilderOptions options) => SharedPartBuilder([EntityGenerator()], "objectbox_model"); diff --git a/generator/lib/src/code_chunks.dart b/generator/lib/src/code_chunks.dart index 1607cc603..a89d9d197 100644 --- a/generator/lib/src/code_chunks.dart +++ b/generator/lib/src/code_chunks.dart @@ -5,7 +5,14 @@ import "package:source_gen/source_gen.dart" show InvalidGenerationSourceError; class CodeChunks { // TODO ModelInfo, once per DB - static String modelInfoLoader() => """ + static String modelInfoDefinition(ModelInfo modelInfo) => """ + import 'package:objectbox/objectbox.dart'; + + ModelInfo getObjectBoxModel() => ModelInfo( + lastEntityId: IdUid(${modelInfo.lastEntityId.id}, ${modelInfo.lastEntityId.uid}), + lastRelationId: IdUid(${modelInfo.lastRelationId.id}, ${modelInfo.lastRelationId.uid}), + lastIndexId: IdUid(${modelInfo.lastIndexId.id}, ${modelInfo.lastIndexId.uid}), + ); """; static String instanceBuildersReaders(ModelEntity readEntity) { diff --git a/generator/lib/src/generator.dart b/generator/lib/src/entity_binding.dart similarity index 79% rename from generator/lib/src/generator.dart rename to generator/lib/src/entity_binding.dart index 8a3d0a87c..fd3a58c15 100644 --- a/generator/lib/src/generator.dart +++ b/generator/lib/src/entity_binding.dart @@ -12,16 +12,22 @@ import "code_chunks.dart"; import "merge.dart"; class EntityGenerator extends GeneratorForAnnotation { - static const ALL_MODELS_JSON = "objectbox-model.json"; - - // each .g.dart file needs to get a header with functions to load the ALL_MODELS_JSON file exactly once. Store the input .dart file ids this has already been done for here - List entityHeaderDone = []; + static const modelJSON = "objectbox-model.json"; + static const modelDart = "objectbox_model.dart"; Future _loadModelInfo() async { - if ((await FileSystemEntity.type(ALL_MODELS_JSON)) == FileSystemEntityType.notFound) { + if ((await FileSystemEntity.type(modelJSON)) == FileSystemEntityType.notFound) { return ModelInfo.createDefault(); } - return ModelInfo.fromMap(json.decode(await (File(ALL_MODELS_JSON).readAsString()))); + return ModelInfo.fromMap(json.decode(await (File(modelJSON).readAsString()))); + } + + void _writeModelInfo(ModelInfo modelInfo) async { + final json = JsonEncoder.withIndent(" ").convert(modelInfo.toMap()); + await File(modelJSON).writeAsString(json); + + final code = CodeChunks.modelInfoDefinition(modelInfo); + await File(modelDart).writeAsString(code); } final _propertyChecker = const TypeChecker.fromRuntime(obx.Property); @@ -36,19 +42,16 @@ class EntityGenerator extends GeneratorForAnnotation { } var element = elementBare as ClassElement; + log.warning(buildStep.inputId.toString()); + // load existing model from JSON file if possible String inputFileId = buildStep.inputId.toString(); - ModelInfo allModels = await _loadModelInfo(); + ModelInfo modelInfo = await _loadModelInfo(); - // optionally add header for loading the .g.json file - var ret = ""; - if (!entityHeaderDone.contains(inputFileId)) { - ret += CodeChunks.modelInfoLoader(); - entityHeaderDone.add(inputFileId); - } + var code = ""; // process basic entity (note that allModels.createEntity is not used, as the entity will be merged) - ModelEntity readEntity = ModelEntity(IdUid.empty(), null, element.name, [], allModels); + ModelEntity readEntity = ModelEntity(IdUid.empty(), null, element.name, [], modelInfo); var entityUid = annotation.read("uid"); if (entityUid != null && !entityUid.isNull) readEntity.id.uid = entityUid.intValue; @@ -124,20 +127,19 @@ class EntityGenerator extends GeneratorForAnnotation { } // merge existing model and annotated model that was just read, then write new final model to file - mergeEntity(allModels, readEntity); - final modelJson = JsonEncoder.withIndent(" ").convert(allModels.toMap()); - await File(ALL_MODELS_JSON).writeAsString(modelJson); + mergeEntity(modelInfo, readEntity); + _writeModelInfo(modelInfo); - readEntity = allModels.findEntityByName(element.name); - if (readEntity == null) return ret; + readEntity = modelInfo.findEntityByName(element.name); + if (readEntity == null) return code; // main code for instance builders and readers - ret += CodeChunks.instanceBuildersReaders(readEntity); + code += CodeChunks.instanceBuildersReaders(readEntity); // for building queries - ret += CodeChunks.queryConditionClasses(readEntity); + code += CodeChunks.queryConditionClasses(readEntity); - return ret; + return code; } catch (e, s) { log.warning(s); rethrow; diff --git a/generator/test/helpers.dart b/generator/test/helpers.dart index 5d0ccceee..1a787f507 100644 --- a/generator/test/helpers.dart +++ b/generator/test/helpers.dart @@ -62,7 +62,7 @@ Future> _buildGeneratorOutput(String caseName) async { var reader = _SingleFileAssetReader(); Resolvers resolvers = AnalyzerResolvers(); - await runBuilder(objectboxModelFactory(BuilderOptions.empty), entities, reader, writer, resolvers); + await runBuilder(entityBindingBuilder(BuilderOptions.empty), entities, reader, writer, resolvers); return writer.output; } @@ -85,7 +85,7 @@ void testGeneratorOutput(String caseName, bool updateExpected) { Map built = await _buildGeneratorOutput(caseName); built.forEach((assetId, generatedCode) async { - final expectedPath = assetId.path.replaceAll(".objectbox_model.g.part", ".g.dart_expected"); + final expectedPath = assetId.path.replaceAll(".objectbox_entity.g.part", ".g.dart_expected"); checkExpectedContents(expectedPath, generatedCode, updateExpected); }); diff --git a/objectbox-model.json b/objectbox-model.json index d59840e4f..4b0eab96e 100644 --- a/objectbox-model.json +++ b/objectbox-model.json @@ -118,9 +118,22 @@ "type": 2 } ] + }, + { + "id": "3:3569200127393812728", + "lastPropertyId": "1:2429256362396080523", + "name": "TestEntity2", + "properties": [ + { + "id": "1:2429256362396080523", + "name": "id", + "type": 6, + "flags": 1 + } + ] } ], - "lastEntityId": "2:2679953000475642792", + "lastEntityId": "3:3569200127393812728", "lastIndexId": "0:0", "lastRelationId": "0:0", "lastSequenceId": "0:0", diff --git a/objectbox_model.dart b/objectbox_model.dart new file mode 100644 index 000000000..be1fa68be --- /dev/null +++ b/objectbox_model.dart @@ -0,0 +1,8 @@ + import 'package:objectbox/objectbox.dart'; + + ModelInfo getObjectBoxModel() => ModelInfo( + lastEntityId: IdUid(3, 3569200127393812728), + lastRelationId: IdUid(0, 0), + lastIndexId: IdUid(0, 0), + ); + \ No newline at end of file diff --git a/test/entity2.dart b/test/entity2.dart new file mode 100644 index 000000000..c87945d3f --- /dev/null +++ b/test/entity2.dart @@ -0,0 +1,10 @@ +import "package:objectbox/objectbox.dart"; + +part 'entity2.g.dart'; + +// Testing a model for entities in multiple files is generated properly +@Entity() +class TestEntity2 { + @Id() + int id; +} diff --git a/test/test_env.dart b/test/test_env.dart index b9e7d8a98..7f3968651 100644 --- a/test/test_env.dart +++ b/test/test_env.dart @@ -1,6 +1,7 @@ import "dart:io"; import "package:objectbox/objectbox.dart"; import "entity.dart"; +import '../objectbox_model.dart'; class TestEnv { final Directory dir; From 0465d9ca58c1d06a03d8cbaaed1dd173a4ea0757 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 20 Nov 2019 16:54:54 +0100 Subject: [PATCH 02/10] Split out EntityGenerator to EntityResolver and CodeBuilder (WIP) --- generator/build.yaml | 29 ++++-- generator/lib/objectbox_generator.dart | 12 ++- generator/lib/src/code_builder.dart | 73 +++++++++++++++ ...tity_binding.dart => entity_resolver.dart} | 89 +++++++------------ 4 files changed, 137 insertions(+), 66 deletions(-) create mode 100644 generator/lib/src/code_builder.dart rename generator/lib/src/{entity_binding.dart => entity_resolver.dart} (58%) diff --git a/generator/build.yaml b/generator/build.yaml index 46fa9d74a..3ef9bc02f 100644 --- a/generator/build.yaml +++ b/generator/build.yaml @@ -1,21 +1,34 @@ # See docs: # https://pub.dev/packages/build_config # https://github.com/dart-lang/build/blob/master/docs/build_yaml_format.md +# https://github.com/dart-lang/build/blob/master/docs/transforming_code.md targets: $default: builders: - objectbox_generator|entities: + objectbox_generator|resolver: + enabled: true + objectbox_generator|generator: enabled: true builders: - entities: + # Finds all classes annotated with @Entity annotation and creates intermediate files for the generator. + # It's executed multiple times, once per file + resolver: import: "package:objectbox_generator/objectbox_generator.dart" - builder_factories: ["entityBindingBuilder"] - build_extensions: {".dart": [".objectbox_entity.g.part"]} + builder_factories: ["entityResolverFactory"] + # build_extensions: Required. A map from input extension to the list of output extensions that may be created + # for that input. This must match the merged buildExtensions maps from each Builder in builder_factories. + build_extensions: {'.dart': ['.objectbox.info']} auto_apply: dependents build_to: cache - applies_builders: ["source_gen|combining_builder"] - defaults: - generate_for: - exclude: ["example/**"] + + # Writes objectbox_model.dart and objectbox-model.json from the prepared .objectbox.info files found in the repo. + generator: + import: "package:objectbox_generator/objectbox_generator.dart" + builder_factories: ["codeGeneratorFactory"] + # build_extensions: Required. A map from input extension to the list of output extensions that may be created + # for that input. This must match the merged buildExtensions maps from each Builder in builder_factories. + build_extensions: {"$lib$": ["/objectbox_model.dart"]} + auto_apply: dependents + build_to: source diff --git a/generator/lib/objectbox_generator.dart b/generator/lib/objectbox_generator.dart index cacd60615..0498610b8 100644 --- a/generator/lib/objectbox_generator.dart +++ b/generator/lib/objectbox_generator.dart @@ -1,6 +1,12 @@ import "package:build/build.dart"; -import "package:source_gen/source_gen.dart"; -import "src/entity_binding.dart"; +import "src/entity_resolver.dart"; +import "src/code_builder.dart"; -Builder entityBindingBuilder(BuilderOptions options) => SharedPartBuilder([EntityGenerator()], "objectbox_entity"); +// See docs +// https://github.com/dart-lang/build/blob/master/docs/writing_a_builder.md +// https://github.com/dart-lang/build/blob/master/docs/writing_an_aggregate_builder.md +// https://pub.dev/packages/build_config#configuring-builders-applied-to-your-package +Builder entityResolverFactory(BuilderOptions options) => EntityResolver(); + +Builder codeGeneratorFactory(BuilderOptions options) => CodeBuilder(); diff --git a/generator/lib/src/code_builder.dart b/generator/lib/src/code_builder.dart new file mode 100644 index 000000000..cd5b7593f --- /dev/null +++ b/generator/lib/src/code_builder.dart @@ -0,0 +1,73 @@ +import "dart:async"; +import 'package:build/build.dart'; +import 'package:glob/glob.dart'; +import 'package:path/path.dart' as path; + +/// CodeBuilder collects all ".objectbox.info" files created by EntityResolver and generates objectbox-model.json and +/// objectbox_model.dart +class CodeBuilder extends Builder { + @override + final buildExtensions = { + r'$lib$': ['objectbox_model.dart'] + }; + + AssetId assetPath(BuildStep buildStep, String filename) { + return AssetId( + buildStep.inputId.package, + path.join('lib', filename), + ); + } + + @override + FutureOr build(BuildStep buildStep) async { + // will be only called once, for the whole directory + + final files = []; + await for (final input in buildStep.findAssets(Glob('lib/**'))) { + files.add(input.path); + } + + return buildStep.writeAsString(assetPath(buildStep, 'objectbox_model.dart'), "// TODO generated model code"); + } + + +//class EntityResolver extends GeneratorForAnnotation { +// static const modelJSON = "objectbox-model.json"; +// static const modelDart = "objectbox_model.dart"; + +// Future _loadModelInfo() async { +// if ((await FileSystemEntity.type(modelJSON)) == FileSystemEntityType.notFound) { +// return ModelInfo.createDefault(); +// } +// return ModelInfo.fromMap(json.decode(await (File(modelJSON).readAsString()))); +// } +// +// void _writeModelInfo(ModelInfo modelInfo) async { +// final json = JsonEncoder.withIndent(" ").convert(modelInfo.toMap()); +// await File(modelJSON).writeAsString(json); +// +// final code = CodeChunks.modelInfoDefinition(modelInfo); +// await File(modelDart).writeAsString(code); +// } + +// +// // load existing model from JSON file if possible +// ModelInfo modelInfo = await _loadModelInfo(); +// +// var code = ""; + +// // merge existing model and annotated model that was just read, then write new final model to file +// mergeEntity(modelInfo, readEntity); +// _writeModelInfo(modelInfo); +// +// readEntity = modelInfo.findEntityByName(element.name); +// if (readEntity == null) return code; +// +// // main code for instance builders and readers +// code += CodeChunks.instanceBuildersReaders(readEntity); +// +// // for building queries +// code += CodeChunks.queryConditionClasses(readEntity); +// +// return code; +} diff --git a/generator/lib/src/entity_binding.dart b/generator/lib/src/entity_resolver.dart similarity index 58% rename from generator/lib/src/entity_binding.dart rename to generator/lib/src/entity_resolver.dart index fd3a58c15..53940e1ff 100644 --- a/generator/lib/src/entity_binding.dart +++ b/generator/lib/src/entity_resolver.dart @@ -1,60 +1,56 @@ import "dart:async"; import "dart:convert"; -import "dart:io"; import "package:analyzer/dart/element/element.dart"; import 'package:build/build.dart'; -import "package:build/src/builder/build_step.dart"; import "package:source_gen/source_gen.dart"; import "package:objectbox/objectbox.dart" as obx; import "package:objectbox/src/bindings/constants.dart"; import "package:objectbox/src/modelinfo/index.dart"; -import "code_chunks.dart"; -import "merge.dart"; -class EntityGenerator extends GeneratorForAnnotation { - static const modelJSON = "objectbox-model.json"; - static const modelDart = "objectbox_model.dart"; +/// EntityResolver finds all classes with an @Entity annotation and generates ".objectbox.info" files in build cache. +/// It's using some tools from source_gen but defining its custom builder because source_gen expects only dart code. +class EntityResolver extends Builder { + @override + final buildExtensions = { + '.dart': ['.objectbox.info'] + }; - Future _loadModelInfo() async { - if ((await FileSystemEntity.type(modelJSON)) == FileSystemEntityType.notFound) { - return ModelInfo.createDefault(); + final _annotationChecker = const TypeChecker.fromRuntime(obx.Entity); + final _propertyChecker = const TypeChecker.fromRuntime(obx.Property); + final _idChecker = const TypeChecker.fromRuntime(obx.Id); + + @override + FutureOr build(BuildStep buildStep) async { + final resolver = buildStep.resolver; + if (!await resolver.isLibrary(buildStep.inputId)) return; + final libReader = LibraryReader(await buildStep.inputLibrary); + + // generate for all entities + final entities = List>(); + for (var annotatedEl in libReader.annotatedWith(_annotationChecker)) { + entities.add(generateForAnnotatedElement(annotatedEl.element, annotatedEl.annotation).toMap()); } - return ModelInfo.fromMap(json.decode(await (File(modelJSON).readAsString()))); - } - void _writeModelInfo(ModelInfo modelInfo) async { - final json = JsonEncoder.withIndent(" ").convert(modelInfo.toMap()); - await File(modelJSON).writeAsString(json); + if (entities.isEmpty) return; - final code = CodeChunks.modelInfoDefinition(modelInfo); - await File(modelDart).writeAsString(code); + final json = JsonEncoder().convert(entities); + await buildStep.writeAsString(buildStep.inputId.changeExtension(".objectbox.info"), json); } - final _propertyChecker = const TypeChecker.fromRuntime(obx.Property); - final _idChecker = const TypeChecker.fromRuntime(obx.Id); - - @override - Future generateForAnnotatedElement( - Element elementBare, ConstantReader annotation, BuildStep buildStep) async { + ModelEntity generateForAnnotatedElement(Element elementBare, ConstantReader annotation) { try { if (elementBare is! ClassElement) { throw InvalidGenerationSourceError("in target ${elementBare.name}: annotated element isn't a class"); } var element = elementBare as ClassElement; - log.warning(buildStep.inputId.toString()); - - // load existing model from JSON file if possible - String inputFileId = buildStep.inputId.toString(); - ModelInfo modelInfo = await _loadModelInfo(); - - var code = ""; - // process basic entity (note that allModels.createEntity is not used, as the entity will be merged) - ModelEntity readEntity = ModelEntity(IdUid.empty(), null, element.name, [], modelInfo); + ModelEntity readEntity = ModelEntity(IdUid.empty(), null, element.name, [], null); var entityUid = annotation.read("uid"); if (entityUid != null && !entityUid.isNull) readEntity.id.uid = entityUid.intValue; + log.info("entity ${readEntity.name}(${readEntity.id})"); + // read all suitable annotated properties bool hasIdProperty = false; for (var f in element.fields) { @@ -64,11 +60,11 @@ class EntityGenerator extends GeneratorForAnnotation { if (_idChecker.hasAnnotationOfExact(f)) { if (hasIdProperty) { throw InvalidGenerationSourceError( - "in target ${elementBare.name}: has more than one properties annotated with @Id"); + "in target ${elementBare.name}: has more than one properties annotated with @Id"); } if (f.type.toString() != "int") { throw InvalidGenerationSourceError( - "in target ${elementBare.name}: field with @Id property has type '${f.type.toString()}', but it must be 'int'"); + "in target ${elementBare.name}: field with @Id property has type '${f.type.toString()}', but it must be 'int'"); } hasIdProperty = true; @@ -83,12 +79,6 @@ class EntityGenerator extends GeneratorForAnnotation { propUid = _propertyAnnotation.getField('uid').toIntValue(); fieldType = _propertyAnnotation.getField('type').toIntValue(); flags = _propertyAnnotation.getField('flag').toIntValue() ?? 0; - - log.info( - "annotated property found on ${f.name} with parameters: propUid(${propUid}) fieldType(${fieldType}) flags(${flags})"); - } else { - log.info( - "property found on ${f.name} with parameters: propUid(${propUid}) fieldType(${fieldType}) flags(${flags})"); } if (fieldType == null) { @@ -110,7 +100,7 @@ class EntityGenerator extends GeneratorForAnnotation { fieldType = OBXPropertyType.Double; } else { log.warning( - "skipping field '${f.name}' in entity '${element.name}', as it has the unsupported type '$fieldTypeStr'"); + " skipping property '${f.name}' in entity '${element.name}', as it has the unsupported type '$fieldTypeStr'"); continue; } } @@ -119,6 +109,8 @@ class EntityGenerator extends GeneratorForAnnotation { ModelProperty prop = ModelProperty(IdUid.empty(), f.name, fieldType, flags, readEntity); if (propUid != null) prop.id.uid = propUid; readEntity.properties.add(prop); + + log.info(" property ${prop.name}(${prop.id}) type:${prop.type} flags:${prop.flags}"); } // some checks on the entity's integrity @@ -126,20 +118,7 @@ class EntityGenerator extends GeneratorForAnnotation { throw InvalidGenerationSourceError("in target ${elementBare.name}: has no properties annotated with @Id"); } - // merge existing model and annotated model that was just read, then write new final model to file - mergeEntity(modelInfo, readEntity); - _writeModelInfo(modelInfo); - - readEntity = modelInfo.findEntityByName(element.name); - if (readEntity == null) return code; - - // main code for instance builders and readers - code += CodeChunks.instanceBuildersReaders(readEntity); - - // for building queries - code += CodeChunks.queryConditionClasses(readEntity); - - return code; + return readEntity; } catch (e, s) { log.warning(s); rethrow; From 77e84b088de86eec0922587a3b8b04595e26ea05 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 20 Nov 2019 18:25:40 +0100 Subject: [PATCH 03/10] prepare generator model.json building in the new builder --- generator/build.yaml | 5 +- generator/lib/src/code_builder.dart | 78 +++++----- generator/lib/src/entity_resolver.dart | 140 +++++++++--------- .../objectbox-model.json | 0 4 files changed, 117 insertions(+), 106 deletions(-) rename objectbox-model.json => test/objectbox-model.json (100%) diff --git a/generator/build.yaml b/generator/build.yaml index 3ef9bc02f..0234e47a9 100644 --- a/generator/build.yaml +++ b/generator/build.yaml @@ -29,6 +29,9 @@ builders: builder_factories: ["codeGeneratorFactory"] # build_extensions: Required. A map from input extension to the list of output extensions that may be created # for that input. This must match the merged buildExtensions maps from each Builder in builder_factories. - build_extensions: {"$lib$": ["/objectbox_model.dart"]} + build_extensions: + "$lib$": ["/objectbox_model.dart"] + "$test": ["/objectbox_model.dart"] + required_inputs: ['.objectbox.info'] auto_apply: dependents build_to: source diff --git a/generator/lib/src/code_builder.dart b/generator/lib/src/code_builder.dart index cd5b7593f..1308b67a4 100644 --- a/generator/lib/src/code_builder.dart +++ b/generator/lib/src/code_builder.dart @@ -1,54 +1,66 @@ -import "dart:async"; +import 'dart:async'; +import 'dart:convert'; import 'package:build/build.dart'; import 'package:glob/glob.dart'; import 'package:path/path.dart' as path; +import 'package:objectbox/objectbox.dart'; +import 'entity_resolver.dart'; +import 'merge.dart'; /// CodeBuilder collects all ".objectbox.info" files created by EntityResolver and generates objectbox-model.json and /// objectbox_model.dart class CodeBuilder extends Builder { - @override - final buildExtensions = { - r'$lib$': ['objectbox_model.dart'] - }; + static final jsonFile = 'objectbox-model.json'; + static final codeFile = 'objectbox_model.dart'; - AssetId assetPath(BuildStep buildStep, String filename) { - return AssetId( - buildStep.inputId.package, - path.join('lib', filename), - ); - } + @override + final buildExtensions = {r'$lib$': _outputs, r'$test$': _outputs}; + static final _outputs = [jsonFile, codeFile]; @override FutureOr build(BuildStep buildStep) async { // will be only called once, for the whole directory + final dir = path.dirname(buildStep.inputId.path); - final files = []; - await for (final input in buildStep.findAssets(Glob('lib/**'))) { - files.add(input.path); + // map from file name to a "json" representation of entities + final files = Map>(); + final glob = Glob(path.join(dir, '**' + EntityResolver.suffix)); + await for (final input in buildStep.findAssets(glob)) { + files[input.path] = json.decode(await buildStep.readAsString(input)); } + if (files.isEmpty) return; - return buildStep.writeAsString(assetPath(buildStep, 'objectbox_model.dart'), "// TODO generated model code"); - } + log.info("Package: ${buildStep.inputId.package}"); + log.info("Found entity files: ${files.keys}"); + // load an existing model or initialize a new one + ModelInfo model; + final jsonId = AssetId(buildStep.inputId.package, path.join(dir, jsonFile)); + if (await buildStep.canRead(jsonId)) { + log.info("Reading model: ${jsonId.path}"); + model = ModelInfo.fromMap(json.decode(await buildStep.readAsString(jsonId))); + } else { + log.warning("Creating new model: ${jsonId.path}"); + model = ModelInfo.createDefault(); + } -//class EntityResolver extends GeneratorForAnnotation { -// static const modelJSON = "objectbox-model.json"; -// static const modelDart = "objectbox_model.dart"; + // collect all entities and sort them by name + final entities = List(); + for (final entitiesList in files.values) { + for (final entityMap in entitiesList) { + entities.add(ModelEntity.fromMap(entityMap)); + } + } + entities.sort((a, b) => a.name.compareTo(b.name)); -// Future _loadModelInfo() async { -// if ((await FileSystemEntity.type(modelJSON)) == FileSystemEntityType.notFound) { -// return ModelInfo.createDefault(); -// } -// return ModelInfo.fromMap(json.decode(await (File(modelJSON).readAsString()))); -// } -// -// void _writeModelInfo(ModelInfo modelInfo) async { -// final json = JsonEncoder.withIndent(" ").convert(modelInfo.toMap()); -// await File(modelJSON).writeAsString(json); -// -// final code = CodeChunks.modelInfoDefinition(modelInfo); -// await File(modelDart).writeAsString(code); -// } + // merge existing model and annotated model that was just read, then write new final model to file + entities.forEach((entity) => mergeEntity(model, entity)); + + // write model info + await buildStep.writeAsString(jsonId, JsonEncoder.withIndent(" ").convert(model.toMap())); + + // TODO write code + } // // // load existing model from JSON file if possible diff --git a/generator/lib/src/entity_resolver.dart b/generator/lib/src/entity_resolver.dart index 53940e1ff..1dec3299e 100644 --- a/generator/lib/src/entity_resolver.dart +++ b/generator/lib/src/entity_resolver.dart @@ -10,9 +10,10 @@ import "package:objectbox/src/modelinfo/index.dart"; /// EntityResolver finds all classes with an @Entity annotation and generates ".objectbox.info" files in build cache. /// It's using some tools from source_gen but defining its custom builder because source_gen expects only dart code. class EntityResolver extends Builder { + static const suffix = '.objectbox.info'; @override final buildExtensions = { - '.dart': ['.objectbox.info'] + '.dart': [suffix] }; final _annotationChecker = const TypeChecker.fromRuntime(obx.Entity); @@ -34,94 +35,89 @@ class EntityResolver extends Builder { if (entities.isEmpty) return; final json = JsonEncoder().convert(entities); - await buildStep.writeAsString(buildStep.inputId.changeExtension(".objectbox.info"), json); + await buildStep.writeAsString(buildStep.inputId.changeExtension(suffix), json); } ModelEntity generateForAnnotatedElement(Element elementBare, ConstantReader annotation) { - try { - if (elementBare is! ClassElement) { - throw InvalidGenerationSourceError("in target ${elementBare.name}: annotated element isn't a class"); - } - var element = elementBare as ClassElement; + if (elementBare is! ClassElement) { + throw InvalidGenerationSourceError("in target ${elementBare.name}: annotated element isn't a class"); + } + var element = elementBare as ClassElement; - // process basic entity (note that allModels.createEntity is not used, as the entity will be merged) - ModelEntity readEntity = ModelEntity(IdUid.empty(), null, element.name, [], null); - var entityUid = annotation.read("uid"); - if (entityUid != null && !entityUid.isNull) readEntity.id.uid = entityUid.intValue; + // process basic entity (note that allModels.createEntity is not used, as the entity will be merged) + ModelEntity readEntity = ModelEntity(IdUid.empty(), null, element.name, [], null); + var entityUid = annotation.read("uid"); + if (entityUid != null && !entityUid.isNull) readEntity.id.uid = entityUid.intValue; - log.info("entity ${readEntity.name}(${readEntity.id})"); + log.info("entity ${readEntity.name}(${readEntity.id})"); - // read all suitable annotated properties - bool hasIdProperty = false; - for (var f in element.fields) { - int fieldType, flags = 0; - int propUid; + // read all suitable annotated properties + bool hasIdProperty = false; + for (var f in element.fields) { + int fieldType, flags = 0; + int propUid; - if (_idChecker.hasAnnotationOfExact(f)) { - if (hasIdProperty) { - throw InvalidGenerationSourceError( + if (_idChecker.hasAnnotationOfExact(f)) { + if (hasIdProperty) { + throw InvalidGenerationSourceError( "in target ${elementBare.name}: has more than one properties annotated with @Id"); - } - if (f.type.toString() != "int") { - throw InvalidGenerationSourceError( + } + if (f.type.toString() != "int") { + throw InvalidGenerationSourceError( "in target ${elementBare.name}: field with @Id property has type '${f.type.toString()}', but it must be 'int'"); - } - - hasIdProperty = true; - - fieldType = OBXPropertyType.Long; - flags |= OBXPropertyFlag.ID; - - final _idAnnotation = _idChecker.firstAnnotationOfExact(f); - propUid = _idAnnotation.getField('uid').toIntValue(); - } else if (_propertyChecker.hasAnnotationOfExact(f)) { - final _propertyAnnotation = _propertyChecker.firstAnnotationOfExact(f); - propUid = _propertyAnnotation.getField('uid').toIntValue(); - fieldType = _propertyAnnotation.getField('type').toIntValue(); - flags = _propertyAnnotation.getField('flag').toIntValue() ?? 0; } - if (fieldType == null) { - var fieldTypeStr = f.type.toString(); - - if (fieldTypeStr == "int") { - // dart: 8 bytes - // ob: 8 bytes - fieldType = OBXPropertyType.Long; - } else if (fieldTypeStr == "String") { - fieldType = OBXPropertyType.String; - } else if (fieldTypeStr == "bool") { - // dart: 1 byte - // ob: 1 byte - fieldType = OBXPropertyType.Bool; - } else if (fieldTypeStr == "double") { - // dart: 8 bytes - // ob: 8 bytes - fieldType = OBXPropertyType.Double; - } else { - log.warning( - " skipping property '${f.name}' in entity '${element.name}', as it has the unsupported type '$fieldTypeStr'"); - continue; - } - } + hasIdProperty = true; - // create property (do not use readEntity.createProperty in order to avoid generating new ids) - ModelProperty prop = ModelProperty(IdUid.empty(), f.name, fieldType, flags, readEntity); - if (propUid != null) prop.id.uid = propUid; - readEntity.properties.add(prop); + fieldType = OBXPropertyType.Long; + flags |= OBXPropertyFlag.ID; - log.info(" property ${prop.name}(${prop.id}) type:${prop.type} flags:${prop.flags}"); + final _idAnnotation = _idChecker.firstAnnotationOfExact(f); + propUid = _idAnnotation.getField('uid').toIntValue(); + } else if (_propertyChecker.hasAnnotationOfExact(f)) { + final _propertyAnnotation = _propertyChecker.firstAnnotationOfExact(f); + propUid = _propertyAnnotation.getField('uid').toIntValue(); + fieldType = _propertyAnnotation.getField('type').toIntValue(); + flags = _propertyAnnotation.getField('flag').toIntValue() ?? 0; } - // some checks on the entity's integrity - if (!hasIdProperty) { - throw InvalidGenerationSourceError("in target ${elementBare.name}: has no properties annotated with @Id"); + if (fieldType == null) { + var fieldTypeStr = f.type.toString(); + + if (fieldTypeStr == "int") { + // dart: 8 bytes + // ob: 8 bytes + fieldType = OBXPropertyType.Long; + } else if (fieldTypeStr == "String") { + fieldType = OBXPropertyType.String; + } else if (fieldTypeStr == "bool") { + // dart: 1 byte + // ob: 1 byte + fieldType = OBXPropertyType.Bool; + } else if (fieldTypeStr == "double") { + // dart: 8 bytes + // ob: 8 bytes + fieldType = OBXPropertyType.Double; + } else { + log.warning( + " skipping property '${f.name}' in entity '${element.name}', as it has the unsupported type '$fieldTypeStr'"); + continue; + } } - return readEntity; - } catch (e, s) { - log.warning(s); - rethrow; + // create property (do not use readEntity.createProperty in order to avoid generating new ids) + ModelProperty prop = ModelProperty(IdUid.empty(), f.name, fieldType, flags, readEntity); + if (propUid != null) prop.id.uid = propUid; + readEntity.properties.add(prop); + + log.info(" property ${prop.name}(${prop.id}) type:${prop.type} flags:${prop.flags}"); + } + + // some checks on the entity's integrity + if (!hasIdProperty) { + throw InvalidGenerationSourceError("in target ${elementBare.name}: has no properties annotated with @Id"); } + + return readEntity; } } diff --git a/objectbox-model.json b/test/objectbox-model.json similarity index 100% rename from objectbox-model.json rename to test/objectbox-model.json From e80e7c79a4345c464d8d049294be1c84b3e1de03 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 21 Nov 2019 09:51:17 +0100 Subject: [PATCH 04/10] generate objectbox.g.dart --- generator/lib/src/code_builder.dart | 85 +++++++++++++----------- generator/lib/src/code_chunks.dart | 68 ++++++++++--------- lib/integration_test.dart | 2 +- lib/src/box.dart | 2 +- lib/src/model.dart | 11 +-- lib/src/modelinfo/entity_definition.dart | 6 +- lib/src/modelinfo/index.dart | 1 + lib/src/modelinfo/model_definition.dart | 9 +++ lib/src/store.dart | 14 ++-- objectbox_model.dart | 8 --- test/entity.dart | 2 - test/entity2.dart | 2 - test/query_test.dart | 2 +- test/test_env.dart | 5 +- 14 files changed, 108 insertions(+), 109 deletions(-) create mode 100644 lib/src/modelinfo/model_definition.dart delete mode 100644 objectbox_model.dart diff --git a/generator/lib/src/code_builder.dart b/generator/lib/src/code_builder.dart index 1308b67a4..6a0cd444d 100644 --- a/generator/lib/src/code_builder.dart +++ b/generator/lib/src/code_builder.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:convert'; import 'package:build/build.dart'; import 'package:glob/glob.dart'; @@ -6,36 +7,57 @@ import 'package:path/path.dart' as path; import 'package:objectbox/objectbox.dart'; import 'entity_resolver.dart'; import 'merge.dart'; +import 'code_chunks.dart'; /// CodeBuilder collects all ".objectbox.info" files created by EntityResolver and generates objectbox-model.json and /// objectbox_model.dart class CodeBuilder extends Builder { static final jsonFile = 'objectbox-model.json'; - static final codeFile = 'objectbox_model.dart'; + static final codeFile = 'objectbox.g.dart'; @override final buildExtensions = {r'$lib$': _outputs, r'$test$': _outputs}; - static final _outputs = [jsonFile, codeFile]; + + // we can't write `jsonFile` as part of the output because we want it persisted, not removed before each generation + static final _outputs = [codeFile]; + + String dir(BuildStep buildStep) => path.dirname(buildStep.inputId.path); @override FutureOr build(BuildStep buildStep) async { - // will be only called once, for the whole directory - final dir = path.dirname(buildStep.inputId.path); + // build() will be called only twice, once for the `lib` directory and once for the `test` directory // map from file name to a "json" representation of entities final files = Map>(); - final glob = Glob(path.join(dir, '**' + EntityResolver.suffix)); + final glob = Glob(path.join(dir(buildStep), '**' + EntityResolver.suffix)); await for (final input in buildStep.findAssets(glob)) { files[input.path] = json.decode(await buildStep.readAsString(input)); } if (files.isEmpty) return; + // collect all entities and sort them by name + final entities = List(); + for (final entitiesList in files.values) { + for (final entityMap in entitiesList) { + entities.add(ModelEntity.fromMap(entityMap)); + } + } + entities.sort((a, b) => a.name.compareTo(b.name)); + log.info("Package: ${buildStep.inputId.package}"); - log.info("Found entity files: ${files.keys}"); + log.info("Found ${entities.length} entities in: ${files.keys}"); + + // update the model JSON with the read entities + final model = await updateModel(entities, buildStep); + + // generate binding code + updateCode(model, files.keys.toList(growable: false), buildStep); + } + Future updateModel(List entities, BuildStep buildStep) async { // load an existing model or initialize a new one ModelInfo model; - final jsonId = AssetId(buildStep.inputId.package, path.join(dir, jsonFile)); + final jsonId = AssetId(buildStep.inputId.package, path.join(dir(buildStep), jsonFile)); if (await buildStep.canRead(jsonId)) { log.info("Reading model: ${jsonId.path}"); model = ModelInfo.fromMap(json.decode(await buildStep.readAsString(jsonId))); @@ -44,42 +66,29 @@ class CodeBuilder extends Builder { model = ModelInfo.createDefault(); } - // collect all entities and sort them by name - final entities = List(); - for (final entitiesList in files.values) { - for (final entityMap in entitiesList) { - entities.add(ModelEntity.fromMap(entityMap)); - } - } - entities.sort((a, b) => a.name.compareTo(b.name)); - // merge existing model and annotated model that was just read, then write new final model to file entities.forEach((entity) => mergeEntity(model, entity)); + // TODO remove ("retire") missing entities + // write model info - await buildStep.writeAsString(jsonId, JsonEncoder.withIndent(" ").convert(model.toMap())); + // Can't use output, it's removed before each build, though writing to FS is explicitly forbidden by package:build. + // await buildStep.writeAsString(jsonId, JsonEncoder.withIndent(" ").convert(model.toMap())); + await File(jsonId.path).writeAsString(JsonEncoder.withIndent(" ").convert(model.toMap())); - // TODO write code + return model; } -// -// // load existing model from JSON file if possible -// ModelInfo modelInfo = await _loadModelInfo(); -// -// var code = ""; - -// // merge existing model and annotated model that was just read, then write new final model to file -// mergeEntity(modelInfo, readEntity); -// _writeModelInfo(modelInfo); -// -// readEntity = modelInfo.findEntityByName(element.name); -// if (readEntity == null) return code; -// -// // main code for instance builders and readers -// code += CodeChunks.instanceBuildersReaders(readEntity); -// -// // for building queries -// code += CodeChunks.queryConditionClasses(readEntity); -// -// return code; + void updateCode(ModelInfo model, List infoFiles, BuildStep buildStep) async { + // transform "/lib/path/entity.objectbox.info" to "path/entity.dart" + final imports = infoFiles + .map((file) => file.replaceFirst(EntityResolver.suffix, ".dart").replaceFirst(dir(buildStep) + "/", "")) + .toList(); + + var code = CodeChunks.objectboxDart(model, imports); + + final codeId = AssetId(buildStep.inputId.package, path.join(dir(buildStep), codeFile)); + log.info("Generating code to: ${codeId.path}"); + await buildStep.writeAsString(codeId, code); + } } diff --git a/generator/lib/src/code_chunks.dart b/generator/lib/src/code_chunks.dart index a89d9d197..cd20198de 100644 --- a/generator/lib/src/code_chunks.dart +++ b/generator/lib/src/code_chunks.dart @@ -4,48 +4,50 @@ import "package:objectbox/src/bindings/constants.dart" show OBXPropertyType; import "package:source_gen/source_gen.dart" show InvalidGenerationSourceError; class CodeChunks { - // TODO ModelInfo, once per DB - static String modelInfoDefinition(ModelInfo modelInfo) => """ - import 'package:objectbox/objectbox.dart'; + static String objectboxDart(ModelInfo model, List imports) => """ + // GENERATED CODE - DO NOT MODIFY BY HAND + + import 'package:objectbox/objectbox.dart'; + export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file + import '${imports.join("';\n import '")}'; + + ModelDefinition getObjectBoxModel() { + final model = ModelInfo.fromMap(${JsonEncoder().convert(model.toMap())}); - ModelInfo getObjectBoxModel() => ModelInfo( - lastEntityId: IdUid(${modelInfo.lastEntityId.id}, ${modelInfo.lastEntityId.uid}), - lastRelationId: IdUid(${modelInfo.lastRelationId.id}, ${modelInfo.lastRelationId.uid}), - lastIndexId: IdUid(${modelInfo.lastIndexId.id}, ${modelInfo.lastIndexId.uid}), - ); + final bindings = Map(); + ${model.entities.map((entity) => "bindings[${entity.name}] = ${entityBinding(entity)};").join("\n")} + + return ModelDefinition(model, bindings); + } + + ${model.entities.map((entity) => queryConditionClasses(entity)).join("\n")} """; - static String instanceBuildersReaders(ModelEntity readEntity) { - String name = readEntity.name; + static String entityBinding(ModelEntity entity) { + String name = entity.name; return """ - ModelEntity _${name}_OBXModelGetter() { - return ModelEntity.fromMap(${JsonEncoder().convert(readEntity.toMap())}); - } - - $name _${name}_OBXBuilder(Map members) { + EntityDefinition<${name}>( + model: model.findEntityByUid(${entity.id.uid}), + reader: ($name inst) => { + ${entity.properties.map((p) => "\"${p.name}\": inst.${p.name}").join(",\n")} + }, + writer: (Map members) { $name r = $name(); - ${readEntity.properties.map((p) => "r.${p.name} = members[\"${p.name}\"];").join()} + ${entity.properties.map((p) => "r.${p.name} = members[\"${p.name}\"];").join()} return r; } - - Map _${name}_OBXReader($name inst) { - Map r = {}; - ${readEntity.properties.map((p) => "r[\"${p.name}\"] = inst.${p.name};").join()} - return r; - } - - const ${name}_OBXDefs = EntityDefinition<${name}>(_${name}_OBXModelGetter, _${name}_OBXReader, _${name}_OBXBuilder); + ) """; } - static String _queryConditionBuilder(ModelEntity readEntity) { + static String _queryConditionBuilder(ModelEntity entity) { final ret = []; - for (var f in readEntity.properties) { - final name = f.name; + for (var prop in entity.properties) { + final name = prop.name; // see OBXPropertyType String fieldType; - switch (f.type) { + switch (prop.type) { case OBXPropertyType.Bool: fieldType = "Boolean"; break; @@ -71,21 +73,21 @@ class CodeChunks { case OBXPropertyType.Long: continue integer; default: - throw InvalidGenerationSourceError("Unsupported property type (${f.type}): ${readEntity.name}.${name}"); + throw InvalidGenerationSourceError("Unsupported property type (${prop.type}): ${entity.name}.${name}"); } ret.add(""" - static final ${name} = Query${fieldType}Property(entityId:${readEntity.id.id}, propertyId:${f.id.id}, obxType:${f.type}); + static final ${name} = Query${fieldType}Property(entityId:${entity.id.id}, propertyId:${prop.id.id}, obxType:${prop.type}); """); } return ret.join(); } - static String queryConditionClasses(ModelEntity readEntity) { + static String queryConditionClasses(ModelEntity entity) { // TODO add entity.id check to throw an error Box if the wrong entity.property is used return """ - class ${readEntity.name}_ { - ${_queryConditionBuilder(readEntity)} + class ${entity.name}_ { + ${_queryConditionBuilder(entity)} }"""; } } diff --git a/lib/integration_test.dart b/lib/integration_test.dart index 620a5b47b..03ff23407 100644 --- a/lib/integration_test.dart +++ b/lib/integration_test.dart @@ -28,7 +28,7 @@ class IntegrationTest { modelInfo.lastEntityId = entity.id; modelInfo.validate(); - final model = Model(modelInfo.entities); + final model = Model(modelInfo); checkObx(bindings.obx_model_free(model.ptr)); } } diff --git a/lib/src/box.dart b/lib/src/box.dart index eda7fa581..040362089 100644 --- a/lib/src/box.dart +++ b/lib/src/box.dart @@ -27,7 +27,7 @@ class Box { Box(this._store) { EntityDefinition entityDefs = _store.entityDef(); - _modelEntity = entityDefs.getModel(); + _modelEntity = entityDefs.model; _entityReader = entityDefs.reader; _fbManager = OBXFlatbuffersManager(_modelEntity, entityDefs.writer); diff --git a/lib/src/model.dart b/lib/src/model.dart index a0b710e0b..039e6d230 100644 --- a/lib/src/model.dart +++ b/lib/src/model.dart @@ -12,19 +12,14 @@ class Model { get ptr => _cModel; - Model(List modelEntities) { + Model(ModelInfo model) { _cModel = checkObxPtr(bindings.obx_model(), "failed to create model"); try { - // transform classes into model descriptions and loop through them - modelEntities.forEach(addEntity); + model.entities.forEach(addEntity); // set last entity id - // TODO read last entity ID from the model - if (modelEntities.isNotEmpty) { - ModelEntity lastEntity = modelEntities[modelEntities.length - 1]; - bindings.obx_model_last_entity_id(_cModel, lastEntity.id.id, lastEntity.id.uid); - } + bindings.obx_model_last_entity_id(_cModel, model.lastEntityId.id, model.lastEntityId.uid); } catch (e) { bindings.obx_model_free(_cModel); _cModel = null; diff --git a/lib/src/modelinfo/entity_definition.dart b/lib/src/modelinfo/entity_definition.dart index bf48212e5..758ad95ea 100644 --- a/lib/src/modelinfo/entity_definition.dart +++ b/lib/src/modelinfo/entity_definition.dart @@ -1,15 +1,15 @@ -// Used by the generated code as a container for model loading callables import 'package:objectbox/src/modelinfo/modelentity.dart'; typedef ObjectReader = Map Function(T object); typedef ObjectWriter = T Function(Map properties); +/// Used by the generated code as a container for model loading callables class EntityDefinition { - final ModelEntity Function() getModel; + final ModelEntity model; final ObjectReader reader; final ObjectWriter writer; - const EntityDefinition(this.getModel, this.reader, this.writer); + const EntityDefinition({this.model, this.reader, this.writer}); Type type() { return T; diff --git a/lib/src/modelinfo/index.dart b/lib/src/modelinfo/index.dart index 277fb0f03..55cdc26b4 100644 --- a/lib/src/modelinfo/index.dart +++ b/lib/src/modelinfo/index.dart @@ -1,5 +1,6 @@ export "modelentity.dart"; export "iduid.dart"; export "entity_definition.dart"; +export "model_definition.dart"; export "modelinfo.dart"; export "modelproperty.dart"; diff --git a/lib/src/modelinfo/model_definition.dart b/lib/src/modelinfo/model_definition.dart new file mode 100644 index 000000000..793469e63 --- /dev/null +++ b/lib/src/modelinfo/model_definition.dart @@ -0,0 +1,9 @@ +import 'package:objectbox/src/modelinfo/modelinfo.dart'; +import 'package:objectbox/src/modelinfo/entity_definition.dart'; + +class ModelDefinition { + final ModelInfo model; + final Map bindings; + + const ModelDefinition(this.model, this.bindings); +} diff --git a/lib/src/store.dart b/lib/src/store.dart index c17aa7708..5e9229591 100644 --- a/lib/src/store.dart +++ b/lib/src/store.dart @@ -1,13 +1,10 @@ import "dart:ffi"; - +import "package:ffi/ffi.dart"; import "bindings/bindings.dart"; import "bindings/helpers.dart"; import "modelinfo/index.dart"; - import "model.dart"; -import "package:ffi/ffi.dart"; - enum TxMode { Read, Write, @@ -17,11 +14,10 @@ enum TxMode { /// specific type. class Store { Pointer _cStore; - Map _entityDefinitions = {}; + final ModelDefinition defs; - Store(List defs, {String directory, int maxDBSizeInKB, int fileMode, int maxReaders}) { - defs.forEach((d) => _entityDefinitions[d.type()] = d); - var model = Model(defs.map((d) => d.getModel()).toList()); + Store(this.defs, {String directory, int maxDBSizeInKB, int fileMode, int maxReaders}) { + var model = Model(defs.model); var opt = bindings.obx_opt(); checkObxPtr(opt, "failed to create store options"); @@ -56,7 +52,7 @@ class Store { } EntityDefinition entityDef() { - return _entityDefinitions[T]; + return defs.bindings[T]; } /// Executes a given function inside a transaction. diff --git a/objectbox_model.dart b/objectbox_model.dart deleted file mode 100644 index be1fa68be..000000000 --- a/objectbox_model.dart +++ /dev/null @@ -1,8 +0,0 @@ - import 'package:objectbox/objectbox.dart'; - - ModelInfo getObjectBoxModel() => ModelInfo( - lastEntityId: IdUid(3, 3569200127393812728), - lastRelationId: IdUid(0, 0), - lastIndexId: IdUid(0, 0), - ); - \ No newline at end of file diff --git a/test/entity.dart b/test/entity.dart index cf37cbc5e..b88c1f7a2 100644 --- a/test/entity.dart +++ b/test/entity.dart @@ -1,7 +1,5 @@ import "package:objectbox/objectbox.dart"; -part 'entity.g.dart'; - /// A dummy annotation to verify the code is generated properly even with annotations unknown to ObjectBox generator. class TestingUnknownAnnotation { const TestingUnknownAnnotation(); diff --git a/test/entity2.dart b/test/entity2.dart index c87945d3f..22da3b1bb 100644 --- a/test/entity2.dart +++ b/test/entity2.dart @@ -1,7 +1,5 @@ import "package:objectbox/objectbox.dart"; -part 'entity2.g.dart'; - // Testing a model for entities in multiple files is generated properly @Entity() class TestEntity2 { diff --git a/test/query_test.dart b/test/query_test.dart index 305f43807..39d80478f 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -1,7 +1,7 @@ import "package:test/test.dart"; -import "package:objectbox/objectbox.dart"; import "entity.dart"; import 'test_env.dart'; +import 'objectbox.g.dart'; void main() { TestEnv env; diff --git a/test/test_env.dart b/test/test_env.dart index 7f3968651..d1ad4799f 100644 --- a/test/test_env.dart +++ b/test/test_env.dart @@ -1,7 +1,6 @@ import "dart:io"; -import "package:objectbox/objectbox.dart"; import "entity.dart"; -import '../objectbox_model.dart'; +import 'objectbox.g.dart'; class TestEnv { final Directory dir; @@ -11,7 +10,7 @@ class TestEnv { TestEnv(String name) : dir = Directory("testdata-" + name) { if (dir.existsSync()) dir.deleteSync(recursive: true); - store = Store([TestEntity_OBXDefs], directory: dir.path); + store = Store(getObjectBoxModel(), directory: dir.path); box = Box(store); } From 73c6d45aa8106a854cbd740a72274c3800a97101 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 21 Nov 2019 10:53:30 +0100 Subject: [PATCH 05/10] model remove missing entities and properties properly --- generator/lib/src/code_builder.dart | 60 +++++++++++++++++++++-- generator/lib/src/merge.dart | 30 ------------ lib/src/modelinfo/modelentity.dart | 13 +---- lib/src/modelinfo/modelinfo.dart | 16 ++++++- test/objectbox-model.json | 74 ++++++----------------------- 5 files changed, 85 insertions(+), 108 deletions(-) delete mode 100644 generator/lib/src/merge.dart diff --git a/generator/lib/src/code_builder.dart b/generator/lib/src/code_builder.dart index 6a0cd444d..8f2af4805 100644 --- a/generator/lib/src/code_builder.dart +++ b/generator/lib/src/code_builder.dart @@ -6,7 +6,6 @@ import 'package:glob/glob.dart'; import 'package:path/path.dart' as path; import 'package:objectbox/objectbox.dart'; import 'entity_resolver.dart'; -import 'merge.dart'; import 'code_chunks.dart'; /// CodeBuilder collects all ".objectbox.info" files created by EntityResolver and generates objectbox-model.json and @@ -67,9 +66,7 @@ class CodeBuilder extends Builder { } // merge existing model and annotated model that was just read, then write new final model to file - entities.forEach((entity) => mergeEntity(model, entity)); - - // TODO remove ("retire") missing entities + merge(model, entities); // write model info // Can't use output, it's removed before each build, though writing to FS is explicitly forbidden by package:build. @@ -85,10 +82,63 @@ class CodeBuilder extends Builder { .map((file) => file.replaceFirst(EntityResolver.suffix, ".dart").replaceFirst(dir(buildStep) + "/", "")) .toList(); - var code = CodeChunks.objectboxDart(model, imports); + final code = CodeChunks.objectboxDart(model, imports); final codeId = AssetId(buildStep.inputId.package, path.join(dir(buildStep), codeFile)); log.info("Generating code to: ${codeId.path}"); await buildStep.writeAsString(codeId, code); } + + void merge(ModelInfo model, List entities) { + // update existing and add new, while collecting all entity IDs at the end + final currentEntityIds = Map(); + entities.forEach((entity) { + final id = mergeEntity(model, entity); + currentEntityIds[id.id] = true; + }); + + // remove ("retire") missing entities + model.entities.where((entity) => !currentEntityIds.containsKey(entity.id.id)).forEach((entity) { + log.warning("Entity ${entity.name}(${entity.id.toString()}) not found in the code, removing from the model"); + model.removeEntity(entity); + }); + + entities.forEach((entity) => mergeEntity(model, entity)); + } + + void mergeProperty(ModelEntity entity, ModelProperty prop) { + ModelProperty propInModel = entity.findSameProperty(prop); + if (propInModel == null) { + log.info("Found new property ${entity.name}.${prop.name}"); + entity.addProperty(prop); + } else { + propInModel.type = prop.type; + propInModel.flags = prop.flags; + } + } + + IdUid mergeEntity(ModelInfo modelInfo, ModelEntity entity) { + // "readEntity" only contains the entity info directly read from the annotations and Dart source (i.e. with missing ID, lastPropertyId etc.) + // "entityInModel" is the entity from the model with all correct id/uid, lastPropertyId etc. + ModelEntity entityInModel = modelInfo.findSameEntity(entity); + + if (entityInModel == null) { + log.info("Found new entity ${entity.name}"); + // in case the entity is created (i.e. when its given UID or name that does not yet exist), we are done, as nothing needs to be merged + final createdEntity = modelInfo.addEntity(entity); + return createdEntity.id; + } + + // here, the entity was found already and entityInModel and readEntity might differ, i.e. conflicts need to be resolved, so merge all properties first + entity.properties.forEach((p) => mergeProperty(entityInModel, p)); + + // then remove all properties not present anymore in readEntity + entityInModel.properties.where((p) => entity.findSameProperty(p) == null).forEach((p) { + log.warning( + "Property ${entity.name}.${p.name}(${p.id.toString()}) not found in the code, removing from the model"); + entityInModel.removeProperty(p); + }); + + return entityInModel.id; + } } diff --git a/generator/lib/src/merge.dart b/generator/lib/src/merge.dart deleted file mode 100644 index aeda9078f..000000000 --- a/generator/lib/src/merge.dart +++ /dev/null @@ -1,30 +0,0 @@ -import "package:objectbox/src/modelinfo/index.dart"; - -void _mergeProperty(ModelEntity entity, ModelProperty prop) { - ModelProperty propInModel = entity.findSameProperty(prop); - if (propInModel == null) { - entity.createCopiedProperty(prop); - } else { - propInModel.type = prop.type; - propInModel.flags = prop.flags; - } -} - -void mergeEntity(ModelInfo modelInfo, ModelEntity readEntity) { - // "readEntity" only contains the entity info directly read from the annotations and Dart source (i.e. with missing ID, lastPropertyId etc.) - // "entityInModel" is the entity from the model with all correct id/uid, lastPropertyId etc. - ModelEntity entityInModel = modelInfo.findSameEntity(readEntity); - - if (entityInModel == null) { - // in case the entity is created (i.e. when its given UID or name that does not yet exist), we are done, as nothing needs to be merged - modelInfo.createCopiedEntity(readEntity); - } else { - // here, the entity was found already and entityInModel and readEntity might differ, i.e. conflicts need to be resolved, so merge all properties first - readEntity.properties.forEach((p) => _mergeProperty(entityInModel, p)); - - // them remove all properties not present anymore in readEntity - entityInModel.properties - .where((p) => readEntity.findSameProperty(p) == null) - .forEach((p) => entityInModel.removeProperty(p)); - } -} diff --git a/lib/src/modelinfo/modelentity.dart b/lib/src/modelinfo/modelentity.dart index 8c9b9a5a7..b7f70a10f 100644 --- a/lib/src/modelinfo/modelentity.dart +++ b/lib/src/modelinfo/modelentity.dart @@ -111,30 +111,21 @@ class ModelEntity { return property; } - ModelProperty createCopiedProperty(ModelProperty prop) { + ModelProperty addProperty(ModelProperty prop) { ModelProperty ret = createProperty(prop.name, prop.id.uid); ret.type = prop.type; ret.flags = prop.flags; return ret; } - void _recalculateLastPropertyId() { - // assign id/uid of property with largest id to lastPropertyId - lastPropertyId = null; - properties.forEach((p) { - if (lastPropertyId == null || p.id.id > lastPropertyId.id) lastPropertyId = p.id; - }); - } - void removeProperty(ModelProperty prop) { - if (prop == null) return; + if (prop == null) throw Exception("prop == null"); ModelProperty foundProp = findSameProperty(prop); if (foundProp == null) { throw Exception("cannot remove property '${prop.name}' with id ${prop.id.toString()}: not found"); } properties = properties.where((p) => p != foundProp).toList(); model.retiredPropertyUids.add(prop.id.uid); - _recalculateLastPropertyId(); } bool containsUid(int searched) { diff --git a/lib/src/modelinfo/modelinfo.dart b/lib/src/modelinfo/modelinfo.dart index e8f996bd5..deb84d418 100644 --- a/lib/src/modelinfo/modelinfo.dart +++ b/lib/src/modelinfo/modelinfo.dart @@ -143,9 +143,9 @@ class ModelInfo { return ret; } - ModelEntity createCopiedEntity(ModelEntity other) { + ModelEntity addEntity(ModelEntity other) { ModelEntity ret = createEntity(other.name, other.id.uid); - other.properties.forEach((p) => ret.createCopiedProperty(p)); + other.properties.forEach((p) => ret.addProperty(p)); return ret; } @@ -161,6 +161,18 @@ class ModelInfo { return entity; } + void removeEntity(ModelEntity entity) { + if (entity == null) throw Exception("entity == null"); + + final foundEntity = findSameEntity(entity); + if (foundEntity == null) { + throw Exception("cannot remove entity '${entity.name}' with id ${entity.id.toString()}: not found"); + } + entities = entities.where((p) => p != foundEntity).toList(); + retiredEntityUids.add(entity.id.uid); + entity.properties.forEach((prop) => retiredPropertyUids.add(prop.id.uid)); + } + int generateUid() { var rng = Random(); for (int i = 0; i < 1000; ++i) { diff --git a/test/objectbox-model.json b/test/objectbox-model.json index 4b0eab96e..658cfbaa9 100644 --- a/test/objectbox-model.json +++ b/test/objectbox-model.json @@ -61,64 +61,6 @@ } ] }, - { - "id": "2:2679953000475642792", - "lastPropertyId": "11:2900967122054840440", - "name": "TestEntityProperty", - "properties": [ - { - "id": "1:118121232448890483", - "name": "id", - "type": 6, - "flags": 1 - }, - { - "id": "2:8913396295192638938", - "name": "tBool", - "type": 1 - }, - { - "id": "3:7399685737415196420", - "name": "tLong", - "type": 6 - }, - { - "id": "4:5773372525238577573", - "name": "tDouble", - "type": 8 - }, - { - "id": "5:1652533532846680965", - "name": "tString", - "type": 9 - }, - { - "id": "7:2032187304851276864", - "name": "tShort", - "type": 3 - }, - { - "id": "8:263290714597678490", - "name": "tChar", - "type": 4 - }, - { - "id": "9:2191004797635629014", - "name": "tInt", - "type": 5 - }, - { - "id": "10:6174275661850707374", - "name": "tFloat", - "type": 7 - }, - { - "id": "11:2900967122054840440", - "name": "tByte", - "type": 2 - } - ] - }, { "id": "3:3569200127393812728", "lastPropertyId": "1:2429256362396080523", @@ -139,14 +81,26 @@ "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, - "retiredEntityUids": [], + "retiredEntityUids": [ + 2679953000475642792 + ], "retiredIndexUids": [], "retiredPropertyUids": [ 5849170199816666167, 6119953496456269132, 1043358010384181585, 8886007565639578393, - 1874578842943403707 + 1874578842943403707, + 118121232448890483, + 8913396295192638938, + 7399685737415196420, + 5773372525238577573, + 1652533532846680965, + 2032187304851276864, + 263290714597678490, + 2191004797635629014, + 6174275661850707374, + 2900967122054840440 ], "retiredRelationUids": [], "version": 1 From fdb9f8a88f90cfe22a73f98d4416184cd18e9347 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 21 Nov 2019 10:59:16 +0100 Subject: [PATCH 06/10] format the generated code --- generator/lib/src/code_builder.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/generator/lib/src/code_builder.dart b/generator/lib/src/code_builder.dart index 8f2af4805..d7954b0d7 100644 --- a/generator/lib/src/code_builder.dart +++ b/generator/lib/src/code_builder.dart @@ -5,6 +5,7 @@ import 'package:build/build.dart'; import 'package:glob/glob.dart'; import 'package:path/path.dart' as path; import 'package:objectbox/objectbox.dart'; +import 'package:dart_style/dart_style.dart'; import 'entity_resolver.dart'; import 'code_chunks.dart'; @@ -82,7 +83,8 @@ class CodeBuilder extends Builder { .map((file) => file.replaceFirst(EntityResolver.suffix, ".dart").replaceFirst(dir(buildStep) + "/", "")) .toList(); - final code = CodeChunks.objectboxDart(model, imports); + var code = CodeChunks.objectboxDart(model, imports); + code = DartFormatter().format(code); final codeId = AssetId(buildStep.inputId.package, path.join(dir(buildStep), codeFile)); log.info("Generating code to: ${codeId.path}"); From 2521dfe4ab3844e684db8003c67c5108fde212da Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 25 Nov 2019 14:17:15 +0100 Subject: [PATCH 07/10] move code/implementation docs closer to the code --- doc/code-generation.md | 37 ---------------------- doc/modelinfo.md | 17 ---------- generator/build.yaml | 4 +-- generator/code-generation.md | 43 ++++++++++++++++++++++++++ generator/lib/objectbox_generator.dart | 5 --- lib/src/modelinfo/iduid.dart | 5 +++ lib/src/modelinfo/modelentity.dart | 2 ++ lib/src/modelinfo/modelinfo.dart | 4 +++ lib/src/modelinfo/modelproperty.dart | 1 + 9 files changed, 57 insertions(+), 61 deletions(-) delete mode 100644 doc/code-generation.md delete mode 100644 doc/modelinfo.md create mode 100644 generator/code-generation.md diff --git a/doc/code-generation.md b/doc/code-generation.md deleted file mode 100644 index 25c4f935f..000000000 --- a/doc/code-generation.md +++ /dev/null @@ -1,37 +0,0 @@ -# ObjectBox Dart – Code generation - -To make it possible to read and write ObjectBox entity instances as easily as possible, wrapper code needs to be generated. Such code is only generated for Dart classes which have been annotated to indicate that they represent an ObjectBox entity (i.e. using [`@Entity`](/lib/src/annotations.dart#L1)). For a Dart source file called `myentity.dart`, which contains an entity definition, a file called `myentity.g.dart` is generated in the same directory by invoking the command `pub run build_runner build`. - -Unfortunately, only few documentation exists on how to generate code using Dart's `build`, `source_gen` and `build_runner`, so the approach taken here by `objectbox_generator` is documented in the following. - -## Basics - -In order to set up code generation, a new package needs to be created exclusively for this task. Here, it it called `objectbox_generator`. This package needs to contain a file called [`build.yaml`](/generator/build.yaml) as well as an entry point for the builder, [`objectbox_generator.dart`](/generator/lib/objectbox_generator.dart), and a generator specifically for one annotation class, [`generator.dart`](/generator/lib/src/entity_binding.dart). The latter needs to contain a class which extends `GeneratorForAnnotation` and overrides `Future generateForAnnotatedElement(Element elementBare, ConstantReader annotation, BuildStep buildStep)`, which returns a string containing the generated code for a single annotation instance. - -It is then possible to traverse through the annotated class in `generateForAnnotatedElement` and e.g. determine all class member fields and their types. Additionally, such member fields can be annotated themselves, but because here, only the `@Entity` annotation is explicitly handled using a separate generator class, member annotations can be read and processed in line. - -## Merging - -After a class, e.g. `TestEntity` in [box_test.dart](/test/box_test.dart#L6), has been fully read, it needs to be compared against and merged with the existing model definition of this class from `objectbox-model.json`. This is done by the function `mergeEntity` in [`merge.dart`](/generator/lib/src/merge.dart). This function takes the parameters `modelInfo`, the existing JSON model, and `readEntity`, the model class definition currently read from a user-provided Dart source file. For some more information on the merging process, see the existing documentation on [Data Model Updates](https://docs.objectbox.io/advanced/data-model-updates); it should also be helpful to refer to the comments in `merge.dart`. - -Also note that in this step, IDs and UIDs are generated automatically for new instances. UIDs are always random, IDs are assigned in ascending order. UIDs may never be reused, e.g. after a property has been removed. This is why `ModelInfo` contains, among others, a member variable called `retiredPropertyUids`, which contains an array of all UIDs which have formerly been assigned to properties, and which are now unavailable to all entities. - -Eventually, `mergeEntity` either throws an error in case the model cannot be merged (e.g. because of ambiguities) or, after having returned normally, it has modified its `modelInfo` parameter to include the entity changes. - -## Testing - -For accomplishing actually automated testing capabilities for `objectbox_generator`, various wrapper classes are needed, as the `build` package is only designed to generate output _files_; yet, during testing, it is necessary to dump generated code to string variables, so they can be compared easily by Dart's `test` framework. - -The entry function for generator testing is the main function of [`generator_test.dart`](/generator/test/generator_test.dart). It makes sure that any existing file called `objectbox-model.json` is removed before every test, because we want a fresh start each time. - -### Helper classes - -The `build` package internally uses designated classes for reading from and writing to files or, to be more general, any kind of _assets_. In this case, we do not want to involve any kind of files as output and only very specific files as input, so it is necessary to create our own versions of the so-called `AssetReader` and `AssetWriter`. - -In [`helpers.dart`](/generator/test/helpers.dart), `_InMemoryAssetWriter` is supposed to receive a single output string and store it in memory. Eventually, the string it stores will be the output of [`EntityGenerator`](/generator/lib/src/entity_binding.dart#L15). - -On the other hand, `_SingleFileAssetReader` shall read a single input Dart source file from the [`test/cases`](/generator/test/cases) directory. Note that currently, test cases have the rather ugly file extension `.dart_testcase`, such as [`single_entity.dart_testcase`](/generator/test/cases/single_entity/single_entity.dart_testcase). This is a workaround, because otherwise, running `pub run build_runner build` in the repository's root directory would generate `.g.dart` files from _all_ `.dart` files in the repository. An option to exclude certain directories from `build_runner` is yet to be found. - -### Executing the tests - -Eventually, the function `runBuilder` [can be executed](/generator/test/helpers.dart#L62), which is part of the `build` package. It encapsulates everything related to generating the final output. Thus, after it is finished and in case generation was successful, the `_InMemoryAssetWriter` instance contains the generated code, which can then be compared against the expected code. diff --git a/doc/modelinfo.md b/doc/modelinfo.md deleted file mode 100644 index d2a530748..000000000 --- a/doc/modelinfo.md +++ /dev/null @@ -1,17 +0,0 @@ -# ObjectBox Dart – Model info - -In order to represent the model stored in `objectbox-model.json` in Dart, several classes have been introduced. They can be found [here](/lib/src/modelinfo). Conceptually, these classes are comparable to how models are handled in ObjectBox Java and ObjectBox Go; eventually, ObjectBox Dart models will be fully compatible to them. This is also why for explanations on most concepts related to ObjectBox models, you can refer to the [existing documentation](https://docs.objectbox.io/advanced). - -Nonetheless, the concrete implementation in this repository is documented in the following. - -## IdUid - -[IdUid](/lib/src/modelinfo/iduid.dart) represents a compound of an ID, which is locally unique, i.e. inside an entity, and a UID, which is globally unique, i.e. for the entire model. When this is serialized, the two numerical values are concatenated using a colon (`:`). See the documentation for more information on [IDs](https://docs.objectbox.io/advanced/meta-model-ids-and-uids#ids) and [UIDs](https://docs.objectbox.io/advanced/meta-model-ids-and-uids#uids). - -## Model classes - -- [`ModelProperty`](/lib/src/modelinfo/modelproperty.dart) describes a single property of an entity, i.e. its id, name, type and flags -- [`ModelEntity`](/lib/src/modelinfo/modelentity.dart) describes an entity of a model and consists of instances of `ModelProperty` as well as an id, name and last property id -- [`ModelInfo`](/lib/src/modelinfo/modelinfo.dart) logically contains an entire ObjectBox model file like [this one](/objectbox-model.json) and thus consists of an array of `ModelEntity` as well as various meta information for ObjectBox and model version information - -Such model meta information is only actually needed when generating `objectbox-model.json`, i.e. when `objectbox_generator` is invoked. This is the case in [`generator.dart`](/generator/lib/src/entity_binding.dart#L24). In [generated code](/generator/lib/src/code_chunks.dart#L12), the JSON file is loaded in the same way, but only the `ModelEntity` instances are kept. diff --git a/generator/build.yaml b/generator/build.yaml index 0234e47a9..579256d47 100644 --- a/generator/build.yaml +++ b/generator/build.yaml @@ -30,8 +30,8 @@ builders: # build_extensions: Required. A map from input extension to the list of output extensions that may be created # for that input. This must match the merged buildExtensions maps from each Builder in builder_factories. build_extensions: - "$lib$": ["/objectbox_model.dart"] - "$test": ["/objectbox_model.dart"] + "$lib$": ["objectbox.g.dart"] + "$test": ["objectbox.g.dart"] required_inputs: ['.objectbox.info'] auto_apply: dependents build_to: source diff --git a/generator/code-generation.md b/generator/code-generation.md new file mode 100644 index 000000000..c9706bbfb --- /dev/null +++ b/generator/code-generation.md @@ -0,0 +1,43 @@ +# ObjectBox Dart – Code generation + +To make it possible to read and write ObjectBox entity instances as easily as possible, wrapper code needs to be +generated. Such code is only generated for Dart classes which have been annotated to indicate that they represent +an ObjectBox entity (i.e. using `@Entity`). The code is generated by executing `pub run build_runner build`. + +See docs: +* https://github.com/dart-lang/build/blob/master/docs/writing_a_builder.md +* https://github.com/dart-lang/build/blob/master/docs/writing_an_aggregate_builder.md +* https://pub.dev/packages/build_config#configuring-builders-applied-to-your-package + +## Basics + +In order to set up code generation, a new package needs to be created exclusively for this task. +Here, it it called `objectbox_generator`. This package needs to contain a [`build.yaml`](build.yaml) as well as an entry +point for the builder, [`objectbox_generator.dart`](lib/objectbox_generator.dart) which defines code-generator factories +for two generators used in build.yaml. + +## Merging + +After all entities have been found in the project, they're compared to an existing model definition in +an objectbox-model.json and merged. Also note that in this step, IDs and UIDs are generated automatically for new +instances. UIDs are always random, IDs are assigned in ascending order. UIDs may never be reused, e.g. after a property +has been removed, and previously used UIDs of remove instances (entities, properties, indexes, ...) are stored in +the JSON file. + +## Testing + +For accomplishing actually automated testing capabilities for `objectbox_generator`, various wrapper classes are needed, as the `build` package is only designed to generate output _files_; yet, during testing, it is necessary to dump generated code to string variables, so they can be compared easily by Dart's `test` framework. + +The entry function for generator testing is the main function of [`generator_test.dart`](/generator/test/generator_test.dart). It makes sure that any existing file called `objectbox-model.json` is removed before every test, because we want a fresh start each time. + +### Helper classes + +The `build` package internally uses designated classes for reading from and writing to files or, to be more general, any kind of _assets_. In this case, we do not want to involve any kind of files as output and only very specific files as input, so it is necessary to create our own versions of the so-called `AssetReader` and `AssetWriter`. + +In [`helpers.dart`](/generator/test/helpers.dart), `_InMemoryAssetWriter` is supposed to receive a single output string and store it in memory. Eventually, the string it stores will be the output of [`EntityGenerator`](/generator/lib/src/entity_binding.dart#L15). + +On the other hand, `_SingleFileAssetReader` shall read a single input Dart source file from the [`test/cases`](/generator/test/cases) directory. Note that currently, test cases have the rather ugly file extension `.dart_testcase`, such as [`single_entity.dart_testcase`](/generator/test/cases/single_entity/single_entity.dart_testcase). This is a workaround, because otherwise, running `pub run build_runner build` in the repository's root directory would generate `.g.dart` files from _all_ `.dart` files in the repository. An option to exclude certain directories from `build_runner` is yet to be found. + +### Executing the tests + +Eventually, the function `runBuilder` [can be executed](/generator/test/helpers.dart#L62), which is part of the `build` package. It encapsulates everything related to generating the final output. Thus, after it is finished and in case generation was successful, the `_InMemoryAssetWriter` instance contains the generated code, which can then be compared against the expected code. diff --git a/generator/lib/objectbox_generator.dart b/generator/lib/objectbox_generator.dart index 0498610b8..71e5140ad 100644 --- a/generator/lib/objectbox_generator.dart +++ b/generator/lib/objectbox_generator.dart @@ -2,11 +2,6 @@ import "package:build/build.dart"; import "src/entity_resolver.dart"; import "src/code_builder.dart"; -// See docs -// https://github.com/dart-lang/build/blob/master/docs/writing_a_builder.md -// https://github.com/dart-lang/build/blob/master/docs/writing_an_aggregate_builder.md -// https://pub.dev/packages/build_config#configuring-builders-applied-to-your-package - Builder entityResolverFactory(BuilderOptions options) => EntityResolver(); Builder codeGeneratorFactory(BuilderOptions options) => CodeBuilder(); diff --git a/lib/src/modelinfo/iduid.dart b/lib/src/modelinfo/iduid.dart index 17f4a20f3..ae409ddba 100644 --- a/lib/src/modelinfo/iduid.dart +++ b/lib/src/modelinfo/iduid.dart @@ -1,3 +1,8 @@ +/// IdUid represents a compound of an ID, which is locally unique, i.e. inside an entity, and a UID, which is globally +/// unique, i.e. for the entire model. It is serialized as two numerical values concatenated using a colon (`:`). +/// See the documentation for more information on +/// * [IDs](https://docs.objectbox.io/advanced/meta-model-ids-and-uids#ids) +/// * [UIDs](https://docs.objectbox.io/advanced/meta-model-ids-and-uids#uids) class IdUid { int _id, _uid; diff --git a/lib/src/modelinfo/modelentity.dart b/lib/src/modelinfo/modelentity.dart index b7f70a10f..bff436d52 100644 --- a/lib/src/modelinfo/modelentity.dart +++ b/lib/src/modelinfo/modelentity.dart @@ -4,6 +4,8 @@ import "modelinfo.dart"; import "modelproperty.dart"; import "package:objectbox/src/bindings/constants.dart"; +/// ModelEntity describes an entity of a model and consists of instances of `ModelProperty` as well as an other entity +/// information: id, name and last property id. class ModelEntity { IdUid id, lastPropertyId; String name; diff --git a/lib/src/modelinfo/modelinfo.dart b/lib/src/modelinfo/modelinfo.dart index deb84d418..f2c464dcd 100644 --- a/lib/src/modelinfo/modelinfo.dart +++ b/lib/src/modelinfo/modelinfo.dart @@ -7,6 +7,10 @@ import "iduid.dart"; const _minModelVersion = 5; const _maxModelVersion = 5; +/// In order to represent the model stored in `objectbox-model.json` in Dart, several classes have been introduced. +/// Conceptually, these classes are comparable to how models are handled in ObjectBox Java and ObjectBox Go; eventually, +/// ObjectBox Dart models will be fully compatible to them. This is also why for explanations on most concepts related +/// to ObjectBox models, you can refer to the [existing documentation](https://docs.objectbox.io/advanced). class ModelInfo { static const notes = [ "KEEP THIS FILE! Check it into a version control system (VCS) like git.", diff --git a/lib/src/modelinfo/modelproperty.dart b/lib/src/modelinfo/modelproperty.dart index dc35b9a35..2486b2201 100644 --- a/lib/src/modelinfo/modelproperty.dart +++ b/lib/src/modelinfo/modelproperty.dart @@ -1,6 +1,7 @@ import "modelentity.dart"; import "iduid.dart"; +/// ModelProperty describes a single property of an entity, i.e. its id, name, type and flags. class ModelProperty { IdUid id; String name; From 48e0ea6013595ae78b19fac5b0840485cbc20d92 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 25 Nov 2019 14:23:09 +0100 Subject: [PATCH 08/10] disable generator tests temporarily, pending rework --- .github/workflows/dart.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 10748f94d..f1d429372 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -12,9 +12,10 @@ jobs: - name: Install dependencies working-directory: generator run: pub get - - name: Run tests - working-directory: generator - run: pub run test +# Disabled temporarily, pending rework in #37 +# - name: Run tests +# working-directory: generator +# run: pub run test lib: needs: generator From 468ae24b1a11ae65f17b25de559621b292861824 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 25 Nov 2019 15:06:05 +0100 Subject: [PATCH 09/10] reduce the size of the generated code --- generator/lib/src/code_builder.dart | 6 +++--- generator/lib/src/code_chunks.dart | 2 +- lib/src/modelinfo/modelinfo.dart | 32 ++++++++++++++++------------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/generator/lib/src/code_builder.dart b/generator/lib/src/code_builder.dart index d7954b0d7..06ca3069e 100644 --- a/generator/lib/src/code_builder.dart +++ b/generator/lib/src/code_builder.dart @@ -59,10 +59,10 @@ class CodeBuilder extends Builder { ModelInfo model; final jsonId = AssetId(buildStep.inputId.package, path.join(dir(buildStep), jsonFile)); if (await buildStep.canRead(jsonId)) { - log.info("Reading model: ${jsonId.path}"); + log.info("Using model: ${jsonId.path}"); model = ModelInfo.fromMap(json.decode(await buildStep.readAsString(jsonId))); } else { - log.warning("Creating new model: ${jsonId.path}"); + log.warning("Creating model: ${jsonId.path}"); model = ModelInfo.createDefault(); } @@ -87,7 +87,7 @@ class CodeBuilder extends Builder { code = DartFormatter().format(code); final codeId = AssetId(buildStep.inputId.package, path.join(dir(buildStep), codeFile)); - log.info("Generating code to: ${codeId.path}"); + log.info("Generating code: ${codeId.path}"); await buildStep.writeAsString(codeId, code); } diff --git a/generator/lib/src/code_chunks.dart b/generator/lib/src/code_chunks.dart index cd20198de..636f11635 100644 --- a/generator/lib/src/code_chunks.dart +++ b/generator/lib/src/code_chunks.dart @@ -12,7 +12,7 @@ class CodeChunks { import '${imports.join("';\n import '")}'; ModelDefinition getObjectBoxModel() { - final model = ModelInfo.fromMap(${JsonEncoder().convert(model.toMap())}); + final model = ModelInfo.fromMap(${JsonEncoder().convert(model.toMap(forCodeGen: true))}); final bindings = Map(); ${model.entities.map((entity) => "bindings[${entity.name}] = ${entityBinding(entity)};").join("\n")} diff --git a/lib/src/modelinfo/modelinfo.dart b/lib/src/modelinfo/modelinfo.dart index f2c464dcd..bd05d45d4 100644 --- a/lib/src/modelinfo/modelinfo.dart +++ b/lib/src/modelinfo/modelinfo.dart @@ -59,10 +59,10 @@ class ModelInfo { lastSequenceId = IdUid.fromString(data["lastSequenceId"]); modelVersion = data["modelVersion"]; modelVersionParserMinimum = data["modelVersionParserMinimum"]; - retiredEntityUids = data["retiredEntityUids"].map((x) => x as int).toList(); - retiredIndexUids = data["retiredIndexUids"].map((x) => x as int).toList(); - retiredPropertyUids = data["retiredPropertyUids"].map((x) => x as int).toList(); - retiredRelationUids = data["retiredRelationUids"].map((x) => x as int).toList(); + retiredEntityUids = List.from(data["retiredEntityUids"] ?? []); + retiredIndexUids = List.from(data["retiredIndexUids"] ?? []); + retiredPropertyUids = List.from(data["retiredPropertyUids"] ?? []); + retiredRelationUids = List.from(data["retiredRelationUids"] ?? []); version = data["version"]; validate(); } @@ -108,23 +108,27 @@ class ModelInfo { } } - Map toMap() { + Map toMap({bool forCodeGen = false}) { Map ret = {}; - ret["_note1"] = notes[0]; - ret["_note2"] = notes[1]; - ret["_note3"] = notes[2]; + if (!forCodeGen) { + ret["_note1"] = notes[0]; + ret["_note2"] = notes[1]; + ret["_note3"] = notes[2]; + } ret["entities"] = entities.map((p) => p.toMap()).toList(); ret["lastEntityId"] = lastEntityId.toString(); ret["lastIndexId"] = lastIndexId.toString(); ret["lastRelationId"] = lastRelationId.toString(); ret["lastSequenceId"] = lastSequenceId.toString(); ret["modelVersion"] = modelVersion; - ret["modelVersionParserMinimum"] = modelVersionParserMinimum; - ret["retiredEntityUids"] = retiredEntityUids; - ret["retiredIndexUids"] = retiredIndexUids; - ret["retiredPropertyUids"] = retiredPropertyUids; - ret["retiredRelationUids"] = retiredRelationUids; - ret["version"] = version; + if (!forCodeGen) { + ret["modelVersionParserMinimum"] = modelVersionParserMinimum; + ret["retiredEntityUids"] = retiredEntityUids; + ret["retiredIndexUids"] = retiredIndexUids; + ret["retiredPropertyUids"] = retiredPropertyUids; + ret["retiredRelationUids"] = retiredRelationUids; + ret["version"] = version; + } return ret; } From 7a2ef322b6b5eb75a8f5f8f5f9fcff5c14a80655 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 25 Nov 2019 15:16:40 +0100 Subject: [PATCH 10/10] update examples to the new code generator --- example/flutter/objectbox_demo/lib/main.dart | 4 ++-- .../{ => lib}/objectbox-model.json | 0 example/flutter/objectbox_demo/pubspec.yaml | 13 +++++++++---- .../flutter/objectbox_demo_desktop/lib/main.dart | 6 +++--- .../{ => lib}/objectbox-model.json | 0 .../flutter/objectbox_demo_desktop/pubspec.yaml | 16 ++++++++++------ 6 files changed, 24 insertions(+), 15 deletions(-) rename example/flutter/objectbox_demo/{ => lib}/objectbox-model.json (100%) rename example/flutter/objectbox_demo_desktop/{ => lib}/objectbox-model.json (100%) diff --git a/example/flutter/objectbox_demo/lib/main.dart b/example/flutter/objectbox_demo/lib/main.dart index ffd6686da..f7183c1a8 100644 --- a/example/flutter/objectbox_demo/lib/main.dart +++ b/example/flutter/objectbox_demo/lib/main.dart @@ -3,7 +3,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; -part "main.g.dart"; +import 'objectbox.g.dart'; @Entity() class Note { @@ -65,7 +65,7 @@ class _OBDemoHomePageState extends State { @override void initState() { getApplicationDocumentsDirectory().then((dir) { - _store = Store([Note_OBXDefs], directory: dir.path + "/objectbox"); + _store = Store(getObjectBoxModel(), directory: dir.path + "/objectbox"); _box = Box(_store); List notesFromDb = _box.getAll(); setState(() => _notes = notesFromDb); diff --git a/example/flutter/objectbox_demo/objectbox-model.json b/example/flutter/objectbox_demo/lib/objectbox-model.json similarity index 100% rename from example/flutter/objectbox_demo/objectbox-model.json rename to example/flutter/objectbox_demo/lib/objectbox-model.json diff --git a/example/flutter/objectbox_demo/pubspec.yaml b/example/flutter/objectbox_demo/pubspec.yaml index 4ad068ce9..5d32e3607 100644 --- a/example/flutter/objectbox_demo/pubspec.yaml +++ b/example/flutter/objectbox_demo/pubspec.yaml @@ -11,15 +11,20 @@ dependencies: cupertino_icons: ^0.1.2 path_provider: any intl: any - objectbox: - path: ../../.. + objectbox: ^0.5.0 dev_dependencies: flutter_test: sdk: flutter build_runner: ^1.0.0 - objectbox_generator: - path: ../../../generator + objectbox_generator: ^0.5.0 flutter: uses-material-design: true + +# --------------------------------- +dependency_overrides: + objectbox: + path: ../../.. + objectbox_generator: + path: ../../../generator \ No newline at end of file diff --git a/example/flutter/objectbox_demo_desktop/lib/main.dart b/example/flutter/objectbox_demo_desktop/lib/main.dart index 1c9a7705b..69c9b2f6e 100644 --- a/example/flutter/objectbox_demo_desktop/lib/main.dart +++ b/example/flutter/objectbox_demo_desktop/lib/main.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart' show debugDefaultTargetPlatformOverride; import 'package:flutter/material.dart'; -import "package:objectbox/objectbox.dart"; -part "main.g.dart"; +import 'package:objectbox/objectbox.dart'; +import 'objectbox.g.dart'; @Entity() class Note { @@ -68,7 +68,7 @@ class _MyHomePageState extends State { @override void initState() { - _store = Store([Note_OBXDefs]); + _store = Store(getObjectBoxModel()); _box = Box(_store); super.initState(); } diff --git a/example/flutter/objectbox_demo_desktop/objectbox-model.json b/example/flutter/objectbox_demo_desktop/lib/objectbox-model.json similarity index 100% rename from example/flutter/objectbox_demo_desktop/objectbox-model.json rename to example/flutter/objectbox_demo_desktop/lib/objectbox-model.json diff --git a/example/flutter/objectbox_demo_desktop/pubspec.yaml b/example/flutter/objectbox_demo_desktop/pubspec.yaml index 440f0aea2..b77bd524e 100644 --- a/example/flutter/objectbox_demo_desktop/pubspec.yaml +++ b/example/flutter/objectbox_demo_desktop/pubspec.yaml @@ -10,18 +10,14 @@ environment: dependencies: flutter: sdk: flutter - cupertino_icons: ^0.1.0 - - objectbox: - path: ../../.. + objectbox: ^0.5.0 dev_dependencies: flutter_test: sdk: flutter build_runner: ^1.0.0 - objectbox_generator: - path: ../../../generator + objectbox_generator: ^0.5.0 flutter: uses-material-design: true @@ -42,3 +38,11 @@ flutter: weight: 700 - asset: fonts/Roboto/Roboto-Black.ttf weight: 900 + + +# --------------------------------- +dependency_overrides: + objectbox: + path: ../../.. + objectbox_generator: + path: ../../../generator \ No newline at end of file