New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
provide better defaults for mimetype to extension mapping #81
base: master
Are you sure you want to change the base?
Changes from 19 commits
86114bd
9f1ef55
c9e3e15
0f37a4b
f4c4a4f
a9ec842
02b9dcb
1ad29e9
84a8f43
1f2ab54
fbf8f68
6600a3f
6fe7ecb
d57b9fc
0d6c519
bb9fc15
7280615
40107ff
c384ef3
9ca2e5b
76f80f6
7b2804f
65c7016
e8a6ed5
f8642bd
8acfbe4
f20c40f
2be331f
60e9a7b
54da123
d190914
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file | ||
// for details. All rights reserved. Use of this source code is governed by a | ||
// BSD-style license that can be found in the LICENSE file. | ||
|
||
import 'default_extension_map.dart'; | ||
|
||
/// Add an override for common extensions since different extensions may map | ||
/// to the same MIME type. | ||
final Map<String, String> _preferredExtensionsMap = <String, String>{ | ||
'application/vnd.ms-excel': 'xls', | ||
'application/vnd.ms-powerpoint': 'ppt', | ||
'image/jpeg': 'jpg', | ||
'image/tiff': 'tif', | ||
'image/svg+xml': 'svg', | ||
'text/calendar': 'ics', | ||
'text/javascript': 'js', | ||
'text/plain': 'txt', | ||
'text/sgml': 'sgml', | ||
'text/x-pascal': 'pas', | ||
'video/mp4': 'mp4', | ||
'video/mpeg': 'mpg', | ||
'video/quicktime': 'mov', | ||
'video/x-matroska': 'mkv', | ||
}; | ||
|
||
/// Lookup file extension for a given MIME type. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name is a noun phrase, so treat this as a parameterized getter. That includes documenting it using a noun phrase too. /// File extension for a given MIME type, if available.
///
/// Returns `null` if [mimeType] has no associated extension.
/// If [mimeType] has multiple associated extensions,
/// the preferred extension is returned. Don't mention the map, it's not part of the public API, so a reader won't know what "the map" is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done (had to look up what a 'noun phrase' is, so not sure if I did right ;) ) |
||
/// | ||
/// If there are multiple extensions for [mimeType], return preferred extension | ||
/// if defined, or the first occurrence in the map. | ||
/// If no extension is found, `null` is returned. | ||
String? extensionFromMimeOrNull(String mimeType) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't have the "OrNull" on the name here. That's just repeating the type. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
final mimeTypeLC = mimeType.toLowerCase(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Avoid abbreviations, so mimeType = mimeType.toLowerCase(); and not rename.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought it would be bad practice to overwrite incoming params? |
||
if (_preferredExtensionsMap.containsKey(mimeTypeLC)) { | ||
return _preferredExtensionsMap[mimeTypeLC]!; | ||
} | ||
return defaultExtensionMap.entries | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd just inline that extension method here: for (var entry in defaultExtensionsMap.entries) {
if (entry.value == mimeTypeLC) return entry.key;
}
return null; It's far too much (unnecessary) syntactic overhead to add your own There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
.firstWhereOrNull((entry) => entry.value == mimeTypeLC) | ||
?.key; | ||
} | ||
|
||
String extensionFromMime(String mimeType, {String? orElse}) => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs documentation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw, I now remember I did this to preserve backward compatibility.. |
||
extensionFromMimeOrNull(mimeType) ?? orElse ?? mimeType; | ||
|
||
/// Allow for a user-specified MIME type-extension mapping that overrides the | ||
/// default. | ||
void addMimeType(String mimeType, String extension) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd be more comfortable with this PR if it didn't include this API. Why do we need this one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hi @natebosch , thanks for having a look this library does not know all possible file formats and I think it's neither realistic nor a goal of this lib to achieve that. The library also seems to be seeded with quite a random list of formats. So it can be quite a frequent situation in which a user of this library needs a format added. A pull request could be made, but the release cycle is not that fast and it's also a bit of overhead for maintainers. That's why I think it may be useful to have this method so users don't immediately get stuck. Similarly, this library already provides The 2nd usecase is one in which the user of the lib has a different preference than the one that is included. For example, if one prefers 'text/plain' to map to 'conf' as it currently does, one can still configure that old behavior. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for the slow response. I'm worried about having an We could either make this signature @devoncarew @lrhn WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally, I worry about global mutable state. I'd prefer a structure where you have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we'll be able to do any refactoring of existing usage. We could add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lrhn - Should I work on a PR with an implementation of a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can definitely "get by" with the top-level approach, but it's inherently fragile. I prefer to avoid adding fragile features. What I'd do, if I had all the time in the world, is to have an immutable registry, and a mutator "builder" type. abstract interface class MimeRegistry {
static get MimeRegistry currentRegistry => ... (probably something that can be zone overridden) ...;
factory MimeRegistry.empty() = ...;
int get defaultMagicNumbersMaxLength;
String lookup(String path, {List<int>? headerBytes});
String extensionFor(String mimeType);
MimeRegistryBuilder get build;
MimeRegistry rebuild(void Function(MimeRegistryBuilder) mutator);
}
abstract interface class MimeRegistryBuilder {
// I don't know what you'd want, but something like:
/// Adds mime type, if it doesn't already exist.
/// Adds [extension] to the extensions for [mimeType].
/// Set the header-recognizer function to [headerRecognizer].
void addMimeType(String mimeType, {String? extension, bool Function(List<int>) headerRecognizer});
void removeMimeType(String mimeType);
void addExtension(String mimeType, String additionalExtension);
void removeExtension(String mimeType, String extension);
// Maybe some inspection functions, but it's a little weird if those are not available on the registry itself.
Iterable<String> get allMimeTypes;
Iterable<String> allExtensionsFor(String mimeType);
bool Function(List<int>)? headerRecognizerFor(String mimeType);
} The builder is building on top of an existing registry, and it can either defer to it, lazily copy from it, or eagerly clone. But I might just be over-engineering. Always a risk 😉 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
What do you think we should do given that we have less time than that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same thing! The worry about lack of time for something else afterwards 😁 But the concrete problem we're trying to solve here seems to be solved without the Then, if we still need configurability, we can come back and look at it fresh, and not stall this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed |
||
_preferredExtensionsMap[mimeType.toLowerCase()] = extension.toLowerCase(); | ||
} | ||
|
||
extension _IterableExtension<T> on Iterable<T> { | ||
/// The first element satisfying [test], or `null` if there are none. | ||
T? firstWhereOrNull(bool Function(T element) test) { | ||
for (var element in this) { | ||
if (test(element)) return element; | ||
} | ||
return null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file | ||
// for details. All rights reserved. Use of this source code is governed by a | ||
// BSD-style license that can be found in the LICENSE file. | ||
|
||
import 'package:mime/mime.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
void main() { | ||
group('global-lookup-mime-type', () { | ||
test('valid-mime-type', () { | ||
expect(extensionFromMime('text/x-dart'), equals('dart')); | ||
expect(extensionFromMime('text/javascript'), equals('js')); | ||
expect(extensionFromMime('application/java-archive'), equals('jar')); | ||
expect(extensionFromMime('application/json'), equals('json')); | ||
expect(extensionFromMime('application/pdf'), equals('pdf')); | ||
expect(extensionFromMime('application/vnd.ms-excel'), equals('xls')); | ||
expect(extensionFromMime('application/xhtml+xml'), equals('xht')); | ||
expect(extensionFromMime('image/jpeg'), equals('jpg')); | ||
expect(extensionFromMime('image/png'), equals('png')); | ||
expect(extensionFromMime('text/css'), equals('css')); | ||
expect(extensionFromMime('text/html'), equals('htm')); | ||
expect(extensionFromMime('text/plain'), equals('txt')); | ||
expect(extensionFromMime('text/x-c'), equals('c')); | ||
}); | ||
|
||
test('invalid-mime-type', () { | ||
expect(extensionFromMime('invalid-mime-type'), 'invalid-mime-type'); | ||
expect(extensionFromMime('invalid/mime/type'), 'invalid/mime/type'); | ||
|
||
expect( | ||
extensionFromMime('invalid-mime-type', orElse: 'invalid'), 'invalid'); | ||
expect( | ||
extensionFromMime('invalid/mime/type', orElse: 'invalid'), 'invalid'); | ||
|
||
expect(extensionFromMimeOrNull('invalid-mime-type'), isNull); | ||
expect(extensionFromMimeOrNull('invalid/mime/type'), isNull); | ||
}); | ||
|
||
test('unknown-mime-type', () { | ||
expect(extensionFromMime('application/to-be-invented'), | ||
'application/to-be-invented'); | ||
|
||
expect(extensionFromMime('application/to-be-invented', orElse: 'unknown'), | ||
'unknown'); | ||
|
||
expect(extensionFromMimeOrNull('application/to-be-invented'), isNull); | ||
}); | ||
}); | ||
|
||
group('add-mime-type', () { | ||
test('new-mime-type', () { | ||
expect(extensionFromMimeOrNull('custom/type'), isNull); | ||
addMimeType('custom/type', 'ct'); | ||
expect(extensionFromMime('custom/type'), equals('ct')); | ||
}); | ||
|
||
test('overridden-mime-type', () { | ||
expect(extensionFromMime('image/jpeg'), equals('jpg')); | ||
addMimeType('image/jpeg', 'jpeg'); | ||
expect(extensionFromMime('image/jpeg'), equals('jpeg')); | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not possible for code to add to this map, right?
If so, it proably should be
const
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed, not anymore ;)
but applying the optimization made it no longer possible