|
1 | 1 | import "dart:async"; |
| 2 | +import "dart:convert"; |
| 3 | +import "dart:io"; |
2 | 4 | import "package:analyzer/dart/element/element.dart"; |
3 | 5 | import "package:build/src/builder/build_step.dart"; |
4 | 6 | import "package:source_gen/source_gen.dart"; |
5 | 7 |
|
6 | | -import "package:objectbox/objectbox.dart"; |
| 8 | +import "package:objectbox/objectbox.dart" as obx; |
7 | 9 | import "package:objectbox/src/bindings/constants.dart"; |
8 | 10 |
|
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 | + |
10 | 27 | @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; |
67 | 34 |
|
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); |
79 | 44 | } |
80 | 45 |
|
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; |
93 | 56 |
|
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(); |
97 | 64 |
|
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'"); |
103 | 76 |
|
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 | + } |
108 | 88 | } |
109 | 89 |
|
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 | + } |
114 | 101 | } |
115 | 102 |
|
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"); |
122 | 112 |
|
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 | + } |
124 | 127 | } |
125 | 128 | } |
0 commit comments