Skip to content

Commit 7c5536d

Browse files
authored
Merge pull request #25 from objectbox/18-automatic-id-generation
Automatically generate IDs and UIDs for entities and properties
2 parents 6e0a6ba + 956f5a8 commit 7c5536d

File tree

77 files changed

+1727
-401
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+1727
-401
lines changed

.github/workflows/dart.yml

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,31 @@ name: Dart CI
33
on: [push, pull_request]
44

55
jobs:
6-
build:
6+
generator:
7+
runs-on: ubuntu-latest
8+
container:
9+
image: google/dart:latest
10+
steps:
11+
- uses: actions/checkout@v1
12+
- name: Install dependencies
13+
working-directory: bin/objectbox_model_generator
14+
run: pub get
15+
- name: Run tests
16+
working-directory: bin/objectbox_model_generator
17+
run: pub run test
718

19+
lib:
20+
needs: generator
821
runs-on: ubuntu-latest
9-
1022
container:
1123
image: google/dart:latest
12-
1324
steps:
14-
- uses: actions/checkout@v1
15-
- name: Install ObjectBox C-API
16-
run: ./install.sh
17-
- name: Install dependencies
18-
run: pub get
19-
- name: Generate ObjectBox models
20-
run: pub run build_runner build
21-
- name: Run tests
22-
run: pub run test
25+
- uses: actions/checkout@v1
26+
- name: Install ObjectBox C-API
27+
run: ./install.sh
28+
- name: Install dependencies
29+
run: pub get
30+
- name: Generate ObjectBox models
31+
run: pub run build_runner build
32+
- name: Run tests
33+
run: pub run test

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
**/pubspec.lock
55
misc/
66
.idea/
7-
**/*.g.dart
87
download/
98
lib/*.dll
109
lib/*.dylib
1110
lib/*.so
1211
lib/*.a
1312
.vscode/
13+
**/*.g.dart
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import "package:objectbox/src/modelinfo/index.dart";
2+
3+
class CodeChunks {
4+
static String modelInfoLoader(String allModelsJsonFilename) => """
5+
Map<int, ModelEntity> _allOBXModelEntities = null;
6+
7+
void _loadOBXModelEntities() {
8+
if (FileSystemEntity.typeSync("objectbox-model.json") == FileSystemEntityType.notFound)
9+
throw Exception("objectbox-model.json not found");
10+
11+
_allOBXModelEntities = {};
12+
ModelInfo modelInfo = ModelInfo.fromMap(json.decode(new File("objectbox-model.json").readAsStringSync()));
13+
modelInfo.entities.forEach((e) => _allOBXModelEntities[e.id.uid] = e);
14+
}
15+
16+
ModelEntity _getOBXModelEntity(int entityUid) {
17+
if (_allOBXModelEntities == null) _loadOBXModelEntities();
18+
if (!_allOBXModelEntities.containsKey(entityUid))
19+
throw Exception("entity uid missing in objectbox-model.json: \$entityUid");
20+
return _allOBXModelEntities[entityUid];
21+
}
22+
""";
23+
24+
static String instanceBuildersReaders(ModelEntity readEntity) {
25+
String name = readEntity.name;
26+
return """
27+
ModelEntity _${name}_OBXModelGetter() {
28+
return _getOBXModelEntity(${readEntity.id.uid});
29+
}
30+
31+
$name _${name}_OBXBuilder(Map<String, dynamic> members) {
32+
$name r = new $name();
33+
${readEntity.properties.map((p) => "r.${p.name} = members[\"${p.name}\"];").join()}
34+
return r;
35+
}
36+
37+
Map<String, dynamic> _${name}_OBXReader($name inst) {
38+
Map<String, dynamic> r = {};
39+
${readEntity.properties.map((p) => "r[\"${p.name}\"] = inst.${p.name};").join()}
40+
return r;
41+
}
42+
43+
const ${name}_OBXDefs = EntityDefinition<${name}>(_${name}_OBXModelGetter, _${name}_OBXReader, _${name}_OBXBuilder);
44+
""";
45+
}
46+
}
Lines changed: 107 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,128 @@
11
import "dart:async";
2+
import "dart:convert";
3+
import "dart:io";
24
import "package:analyzer/dart/element/element.dart";
35
import "package:build/src/builder/build_step.dart";
46
import "package:source_gen/source_gen.dart";
57

6-
import "package:objectbox/objectbox.dart";
8+
import "package:objectbox/objectbox.dart" as obx;
79
import "package:objectbox/src/bindings/constants.dart";
810

9-
class EntityGenerator extends GeneratorForAnnotation<Entity> {
11+
import "code_chunks.dart";
12+
import "merge.dart";
13+
import "package:objectbox/src/modelinfo/index.dart";
14+
15+
class EntityGenerator extends GeneratorForAnnotation<obx.Entity> {
16+
static const ALL_MODELS_JSON = "objectbox-model.json";
17+
18+
// 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
19+
List<String> entityHeaderDone = [];
20+
21+
Future<ModelInfo> _loadModelInfo() async {
22+
if ((await FileSystemEntity.type(ALL_MODELS_JSON)) == FileSystemEntityType.notFound)
23+
return ModelInfo.createDefault();
24+
return ModelInfo.fromMap(json.decode(await (new File(ALL_MODELS_JSON).readAsString())));
25+
}
26+
1027
@override
11-
FutureOr<String> generateForAnnotatedElement(Element elementBare, ConstantReader annotation, BuildStep buildStep) {
12-
if (elementBare is! ClassElement)
13-
throw InvalidGenerationSourceError("in target ${elementBare.name}: annotated element isn't a class");
14-
15-
// get basic entity info
16-
var entity = Entity(id: annotation.read('id').intValue, uid: annotation.read('uid').intValue);
17-
var element = elementBare as ClassElement;
18-
var ret = """
19-
const _${element.name}_OBXModel = {
20-
"entity": {
21-
"name": "${element.name}",
22-
"id": ${entity.id},
23-
"uid": ${entity.uid}
24-
},
25-
"properties": [
26-
""";
27-
28-
// read all suitable annotated properties
29-
var props = [];
30-
String idPropertyName;
31-
for (var f in element.fields) {
32-
if (f.metadata == null || f.metadata.length != 1) // skip unannotated fields
33-
continue;
34-
var annotElmt = f.metadata[0].element as ConstructorElement;
35-
var annotType = annotElmt.returnType.toString();
36-
var annotVal = f.metadata[0].computeConstantValue();
37-
var fieldTypeObj = annotVal.getField("type");
38-
int fieldType = fieldTypeObj == null ? null : fieldTypeObj.toIntValue();
39-
40-
var prop = {
41-
"name": f.name,
42-
"id": annotVal.getField("id").toIntValue(),
43-
"uid": annotVal.getField("uid").toIntValue(),
44-
"flags": 0,
45-
};
46-
47-
if (annotType == "Id") {
48-
if (idPropertyName != null)
49-
throw InvalidGenerationSourceError(
50-
"in target ${elementBare.name}: has more than one properties annotated with @Id");
51-
if (fieldType != null)
52-
throw InvalidGenerationSourceError(
53-
"in target ${elementBare.name}: programming error: @Id property may not specify a type");
54-
if (f.type.toString() != "int")
55-
throw InvalidGenerationSourceError(
56-
"in target ${elementBare.name}: field with @Id property has type '${f.type.toString()}', but it must be 'int'");
57-
58-
fieldType = OBXPropertyType.Long;
59-
prop["flags"] = OBXPropertyFlag.ID;
60-
idPropertyName = f.name;
61-
} else if (annotType == "Property") {
62-
// nothing special here
63-
} else {
64-
// skip unknown annotations
65-
continue;
66-
}
28+
Future<String> generateForAnnotatedElement(
29+
Element elementBare, ConstantReader annotation, BuildStep buildStep) async {
30+
try {
31+
if (elementBare is! ClassElement)
32+
throw InvalidGenerationSourceError("in target ${elementBare.name}: annotated element isn't a class");
33+
var element = elementBare as ClassElement;
6734

68-
if (fieldType == null) {
69-
var fieldTypeStr = f.type.toString();
70-
if (fieldTypeStr == "int")
71-
fieldType = OBXPropertyType.Int;
72-
else if (fieldTypeStr == "String")
73-
fieldType = OBXPropertyType.String;
74-
else {
75-
print(
76-
"warning: skipping field '${f.name}' in entity '${element.name}', as it has the unsupported type '$fieldTypeStr'");
77-
continue;
78-
}
35+
// load existing model from JSON file if possible
36+
String inputFileId = buildStep.inputId.toString();
37+
ModelInfo allModels = await _loadModelInfo();
38+
39+
// optionally add header for loading the .g.json file
40+
var ret = "";
41+
if (entityHeaderDone.indexOf(inputFileId) == -1) {
42+
ret += CodeChunks.modelInfoLoader(ALL_MODELS_JSON);
43+
entityHeaderDone.add(inputFileId);
7944
}
8045

81-
prop["type"] = fieldType;
82-
props.add(prop);
83-
ret += """
84-
{
85-
"name": "${prop['name']}",
86-
"id": ${prop['id']},
87-
"uid": ${prop['uid']},
88-
"type": ${prop['type']},
89-
"flags": ${prop['flags']},
90-
},
91-
""";
92-
}
46+
// process basic entity (note that allModels.createEntity is not used, as the entity will be merged)
47+
ModelEntity readEntity = new ModelEntity(IdUid.empty(), null, element.name, [], allModels);
48+
var entityUid = annotation.read("uid");
49+
if (entityUid != null && !entityUid.isNull) readEntity.id.uid = entityUid.intValue;
50+
51+
// read all suitable annotated properties
52+
bool hasIdProperty = false;
53+
for (var f in element.fields) {
54+
int fieldType, flags = 0;
55+
int propUid;
9356

94-
// some checks on the entity's integrity
95-
if (idPropertyName == null)
96-
throw InvalidGenerationSourceError("in target ${elementBare.name}: has no properties annotated with @Id");
57+
if (f.metadata != null && f.metadata.length == 1) {
58+
var annotElmt = f.metadata[0].element as ConstructorElement;
59+
var annotType = annotElmt.returnType.toString();
60+
var annotVal = f.metadata[0].computeConstantValue();
61+
var fieldTypeAnnot = null; // for the future, with custom type sizes allowed: annotVal.getField("type");
62+
fieldType = fieldTypeAnnot == null ? null : fieldTypeAnnot.toIntValue();
63+
propUid = annotVal.getField("uid").toIntValue();
9764

98-
// main code for instance builders and readers
99-
ret += """
100-
],
101-
"idPropertyName": "${idPropertyName}",
102-
};
65+
// find property flags
66+
if (annotType == "Id") {
67+
if (hasIdProperty)
68+
throw InvalidGenerationSourceError(
69+
"in target ${elementBare.name}: has more than one properties annotated with @Id");
70+
if (fieldType != null)
71+
throw InvalidGenerationSourceError(
72+
"in target ${elementBare.name}: programming error: @Id property may not specify a type");
73+
if (f.type.toString() != "int")
74+
throw InvalidGenerationSourceError(
75+
"in target ${elementBare.name}: field with @Id property has type '${f.type.toString()}', but it must be 'int'");
10376

104-
${element.name} _${element.name}_OBXBuilder(Map<String, dynamic> members) {
105-
${element.name} r = new ${element.name}();
106-
${props.map((p) => "r.${p['name']} = members[\"${p['name']}\"];").join()}
107-
return r;
77+
fieldType = OBXPropertyType.Long;
78+
flags |= OBXPropertyFlag.ID;
79+
hasIdProperty = true;
80+
} else if (annotType == "Property") {
81+
// nothing special
82+
} else {
83+
// skip unknown annotations
84+
print(
85+
"warning: skipping field '${f.name}' in entity '${element.name}', as it has the unknown annotation type '$annotType'");
86+
continue;
87+
}
10888
}
10989

110-
Map<String, dynamic> _${element.name}_OBXReader(${element.name} inst) {
111-
Map<String, dynamic> r = {};
112-
${props.map((p) => "r[\"${p['name']}\"] = inst.${p['name']};").join()}
113-
return r;
90+
if (fieldType == null) {
91+
var fieldTypeStr = f.type.toString();
92+
if (fieldTypeStr == "int")
93+
fieldType = OBXPropertyType.Int;
94+
else if (fieldTypeStr == "String")
95+
fieldType = OBXPropertyType.String;
96+
else {
97+
print(
98+
"warning: skipping field '${f.name}' in entity '${element.name}', as it has the unsupported type '$fieldTypeStr'");
99+
continue;
100+
}
114101
}
115102

116-
const ${element.name}_OBXDefs = {
117-
"model": _${element.name}_OBXModel,
118-
"builder": _${element.name}_OBXBuilder,
119-
"reader": _${element.name}_OBXReader,
120-
};
121-
""";
103+
// create property (do not use readEntity.createProperty in order to avoid generating new ids)
104+
ModelProperty prop = new ModelProperty(IdUid.empty(), f.name, fieldType, flags, readEntity);
105+
if (propUid != null) prop.id.uid = propUid;
106+
readEntity.properties.add(prop);
107+
}
108+
109+
// some checks on the entity's integrity
110+
if (!hasIdProperty)
111+
throw InvalidGenerationSourceError("in target ${elementBare.name}: has no properties annotated with @Id");
122112

123-
return ret;
113+
// merge existing model and annotated model that was just read, then write new final model to file
114+
mergeEntity(allModels, readEntity);
115+
new File(ALL_MODELS_JSON).writeAsString(new JsonEncoder.withIndent(" ").convert(allModels.toMap()));
116+
readEntity = allModels.findEntityByName(element.name);
117+
if (readEntity == null) return ret;
118+
119+
// main code for instance builders and readers
120+
ret += CodeChunks.instanceBuildersReaders(readEntity);
121+
122+
return ret;
123+
} catch (e, s) {
124+
print(s);
125+
rethrow;
126+
}
124127
}
125128
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import "package:objectbox/src/modelinfo/index.dart";
2+
3+
void _mergeProperty(ModelEntity entity, ModelProperty prop) {
4+
ModelProperty propInModel = entity.findSameProperty(prop);
5+
if (propInModel == null) {
6+
entity.createCopiedProperty(prop);
7+
} else {
8+
propInModel.type = prop.type;
9+
propInModel.flags = prop.flags;
10+
}
11+
}
12+
13+
void mergeEntity(ModelInfo modelInfo, ModelEntity readEntity) {
14+
// "readEntity" only contains the entity info directly read from the annotations and Dart source (i.e. with missing ID, lastPropertyId etc.)
15+
// "entityInModel" is the entity from the model with all correct id/uid, lastPropertyId etc.
16+
ModelEntity entityInModel = modelInfo.findSameEntity(readEntity);
17+
18+
if (entityInModel == null) {
19+
// 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
20+
modelInfo.createCopiedEntity(readEntity);
21+
} else {
22+
// here, the entity was found already and entityInModel and readEntity might differ, i.e. conflicts need to be resolved, so merge all properties first
23+
readEntity.properties.forEach((p) => _mergeProperty(entityInModel, p));
24+
25+
// them remove all properties not present anymore in readEntity
26+
entityInModel.properties
27+
.where((p) => readEntity.findSameProperty(p) == null)
28+
.forEach((p) => entityInModel.removeProperty(p));
29+
}
30+
}

bin/objectbox_model_generator/pubspec.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ version: 0.3.0
33
description: >-
44
A preprocessor for Dart source files containing ObjectBox entity definitions.
55
environment:
6-
sdk: '>=2.5.0-dev.2.0 <3.0.0'
6+
sdk: ">=2.5.0-dev.2.0 <3.0.0"
77
dependencies:
8-
build: '>=0.12.0 <2.0.0'
8+
build: ">=0.12.0 <2.0.0"
99
source_gen: ^0.9.0
1010
objectbox:
1111
path: ../..
1212
dev_dependencies:
13-
build_runner: '>=0.9.0 <0.11.0'
13+
build_runner: ">=0.9.0 <0.11.0"
14+
test: ^1.0.0

0 commit comments

Comments
 (0)