-
Notifications
You must be signed in to change notification settings - Fork 390
Expand file tree
/
Copy pathextension_model.dart
More file actions
332 lines (299 loc) · 12.5 KB
/
extension_model.dart
File metadata and controls
332 lines (299 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
// Copyright 2023 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
/// @docImport 'package:flutter/material.dart';
library;
import 'package:collection/collection.dart';
// TODO(https://github.com/flutter/devtools/issues/7955): let extensions declare
// the type of tool they are providing: 'static-only', 'runtime-only', or
// 'static-and-runtime'.
/// Describes an extension that can be dynamically loaded into a custom screen
/// in DevTools.
class DevToolsExtensionConfig implements Comparable<DevToolsExtensionConfig> {
DevToolsExtensionConfig._({
required this.name,
required this.issueTrackerLink,
required this.version,
required this.materialIconCodePoint,
required this.requiresConnection,
required this.extensionAssetsPath,
required this.devtoolsOptionsUri,
required this.isPubliclyHosted,
required this.detectedFromStaticContext,
});
factory DevToolsExtensionConfig.parse(Map<String, Object?> json) {
// Default to true if this value is not specified in the JSON.
final requiresConnectionValue = json[requiresConnectionKey];
final requiresConnection =
requiresConnectionValue != false && requiresConnectionValue != 'false';
if (json
case {
// The exptected keys below are required fields in the extension's
// config.yaml file.
nameKey: final String name,
issueTrackerKey: final String issueTracker,
versionKey: final String version,
materialIconCodePointKey: final Object codePointFromJson,
// The expected keys below are not from the extension's config.yaml
// file; they are generated during the extension detection mechanism
// in the DevTools server.
extensionAssetsPathKey: final String extensionAssetsPath,
devtoolsOptionsUriKey: final String devtoolsOptionsUri,
isPubliclyHostedKey: final String isPubliclyHosted,
detectedFromStaticContextKey: final String detectedFromStaticContext,
// Note that the field [requiresConnectionKey] is not required for
// this check because it is optional.
}) {
final underscoresAndLetters = RegExp(r'^[a-z0-9_]*$');
if (!underscoresAndLetters.hasMatch(name)) {
throw StateError(
'The "name" field in the extension config.yaml should only contain '
'lowercase letters, numbers, and underscores but instead was '
'"$name". This should be a valid Dart package name that matches the '
'package name this extension belongs to.',
);
}
// Defaults to the code point for [Icons.extensions_outlined] if parsing
// fails.
final int codePoint;
const defaultCodePoint = 0xf03f;
if (codePointFromJson is String) {
codePoint = int.tryParse(codePointFromJson) ?? defaultCodePoint;
} else {
codePoint = codePointFromJson as int;
}
return DevToolsExtensionConfig._(
// These values are required fields in the extension's config.yaml file.
name: name,
issueTrackerLink: issueTracker,
version: version,
materialIconCodePoint: codePoint,
// These values are optional fields in the extension's config.yaml file
// and will use default values if not specified.
requiresConnection: requiresConnection,
// These values are generated by the DevTools server.
extensionAssetsPath: extensionAssetsPath,
devtoolsOptionsUri: devtoolsOptionsUri,
isPubliclyHosted: bool.parse(isPubliclyHosted),
detectedFromStaticContext: bool.parse(detectedFromStaticContext),
);
} else {
_assertGeneratedKeysPresent(json);
final jsonKeysFromConfigFile = Set.of(json.keys.toSet())
..removeAll([
..._serverGeneratedKeys,
..._optionalKeys,
]);
final diff = _requiredKeys.toSet().difference(
jsonKeysFromConfigFile,
);
if (diff.isNotEmpty) {
throw StateError(
'Missing required fields ${diff.toString()} in the extension '
'config.yaml.',
);
} else {
// All the required keys are present, but the value types did not match.
final sb = StringBuffer();
for (final entry in json.entries) {
sb.writeln(
' ${entry.key}: ${entry.value} (${entry.value.runtimeType})',
);
}
throw StateError(
'Unexpected value types in the extension config.yaml. Expected all '
'values to be of type String, but one or more had a different type:\n'
'${sb.toString()}',
);
}
}
}
// The following keys are required in the extension's config.yaml file.
static const nameKey = 'name';
static const issueTrackerKey = 'issueTracker';
static const versionKey = 'version';
static const materialIconCodePointKey = 'materialIconCodePoint';
static const _requiredKeys = [
nameKey,
issueTrackerKey,
versionKey,
materialIconCodePointKey,
];
// The following keys are optional in the extension's 'config.yaml' file.
static const requiresConnectionKey = 'requiresConnection';
static const _optionalKeys = [requiresConnectionKey];
// The following keys are never expected to be in the extension's config.yaml
// file. They are generated during the extension detection mechanism in the
// DevTools server.
static const extensionAssetsPathKey = 'extensionAssetsPath';
static const devtoolsOptionsUriKey = 'devtoolsOptionsUri';
static const isPubliclyHostedKey = 'isPubliclyHosted';
static const detectedFromStaticContextKey = 'detectedFromStaticContext';
static const _serverGeneratedKeys = [
extensionAssetsPathKey,
devtoolsOptionsUriKey,
isPubliclyHostedKey,
detectedFromStaticContextKey,
];
/// The package name that this extension is for.
///
/// This value should be defined by the extension's config.yaml file.
final String name;
// TODO(kenz): we might want to add validation to these issue tracker
// links to ensure they don't point to the DevTools repo or flutter repo.
// If an invalid issue tracker link is provided, we can default to
// 'pub.dev/packages/$name'.
/// The link to the issue tracker for this DevTools extension.
///
/// This value should be defined by the extension's config.yaml file.
///
/// This should not point to the flutter/devtools or flutter/flutter issue
/// trackers, but rather to the issue tracker for the package that provides
/// the extension, or to the repo where the extension is developed.
final String issueTrackerLink;
/// The version for the DevTools extension.
///
/// This value should be defined by the extension's config.yaml file.
///
/// This may match the version of the parent package or use a different
/// versioning system as decided by the extension author.
final String version;
/// The code point for the material icon that will parsed by Flutter's
/// [IconData] class for displaying in DevTools.
///
/// This value should be defined by the extension's config.yaml file. If the
/// provided value cannot be parsed, `defaultCodePoint` will be used.
///
/// This code point should be part of the 'MaterialIcons' font family.
/// See https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/icons.dart.
final int materialIconCodePoint;
/// Whether this extension requires a connected app to use.
///
/// This value can be defined by the extension's 'config.yaml' file. If it is
/// not defined, this will default to true.
final bool requiresConnection;
/// The absolute path to this extension's assets on disk.
///
/// This will most likely be in the user's pub cache, but may also be
/// somewhere else on the user's machine if, for example, a dependency is
/// specified as a path dependency.
///
/// This value will NOT be defined by the extension's config.yaml file; it
/// is derived on the DevTools server as part of the extension detection
/// mechanism.
final String extensionAssetsPath;
/// The `file://` URI to the `devtools_options.yaml` file that this
/// extension's enabled state will be stored at.
///
/// The should be equivalent to the package root that contains the
/// `.dart_tool/package_config.json` file where this extension was detected
/// from.
///
/// This value will NOT be defined by the extension's 'config.yaml' file; it
/// is derived on the DevTools server as part of the extension detection
/// mechanism.
final String devtoolsOptionsUri;
/// Whether this extension is distrubuted in a public package on pub.dev.
///
/// This value will NOT be defined by the extension's config.yaml file; it
/// is derived on the DevTools server as part of the extension detection
/// mechanism.
final bool isPubliclyHosted;
/// Whether this extension was detected from a static context.
///
/// A true value means that this extension was detected as a "static"
/// extension. A "static extension is one that was detected from a dependency
/// in the user's project roots, as defined by the Dart Tooling Daemon.
///
/// A false value means that this extension was detected as a "runtime"
/// extension. A "runtime" extension is one that was detected from one of the
/// running app's dependencies.
///
/// This value will NOT be defined by the extension's 'config.yaml' file; it
/// is derived on the DevTools server as part of the extension detection
/// mechanism.
final bool detectedFromStaticContext;
String get displayName => name.toLowerCase();
String get identifier => '${displayName}_$version';
String get analyticsSafeName => isPubliclyHosted ? name : 'private';
Map<String, Object?> toJson() => {
nameKey: name,
issueTrackerKey: issueTrackerLink,
versionKey: version,
materialIconCodePointKey: materialIconCodePoint,
requiresConnectionKey: requiresConnection.toString(),
extensionAssetsPathKey: extensionAssetsPath,
devtoolsOptionsUriKey: devtoolsOptionsUri,
isPubliclyHostedKey: isPubliclyHosted.toString(),
detectedFromStaticContextKey: detectedFromStaticContext.toString(),
};
@override
int compareTo(DevToolsExtensionConfig other) {
var compare = name.compareTo(other.name);
if (compare == 0) {
compare = extensionAssetsPath.compareTo(other.extensionAssetsPath);
if (compare == 0) {
return devtoolsOptionsUri.compareTo(other.devtoolsOptionsUri);
}
}
return compare;
}
@override
bool operator ==(Object other) {
return other is DevToolsExtensionConfig &&
other.name == name &&
other.issueTrackerLink == issueTrackerLink &&
other.version == version &&
other.materialIconCodePoint == materialIconCodePoint &&
other.requiresConnection == requiresConnection &&
other.extensionAssetsPath == extensionAssetsPath &&
other.devtoolsOptionsUri == devtoolsOptionsUri &&
other.isPubliclyHosted == isPubliclyHosted &&
other.detectedFromStaticContext == detectedFromStaticContext;
}
@override
int get hashCode => Object.hash(
name,
issueTrackerLink,
version,
materialIconCodePoint,
requiresConnection,
extensionAssetsPath,
devtoolsOptionsUri,
isPubliclyHosted,
detectedFromStaticContext,
);
static void _assertGeneratedKeysPresent(Map<String, Object?> json) {
final missingKeys = <String>[];
for (final key in _serverGeneratedKeys) {
if (!json.containsKey(key)) {
missingKeys.add(key);
}
}
if (missingKeys.isNotEmpty) {
throw StateError(
'Missing generated keys ${missingKeys.toString()} when trying to parse '
'DevToolsExtensionConfig object.',
);
}
}
}
/// Describes the enablement state of a DevTools extension.
enum ExtensionEnabledState {
/// The extension has been enabled manually by the user.
enabled,
/// The extension has been disabled manually by the user.
disabled,
/// The extension has been neither enabled nor disabled by the user.
none,
/// Something went wrong with reading or writing the activation state.
///
/// We should ignore extensions with this activation state.
error;
/// Parses [value] and returns the matching [ExtensionEnabledState] if found.
static ExtensionEnabledState from(String? value) {
return ExtensionEnabledState.values
.firstWhereOrNull((e) => e.name == value) ??
ExtensionEnabledState.none;
}
}