Skip to content

Commit 3ae6d4b

Browse files
donny-dontnex3
authored andcommitted
Emulate shelf messaging (#54)
1 parent f975718 commit 3ae6d4b

File tree

4 files changed

+440
-223
lines changed

4 files changed

+440
-223
lines changed

lib/src/message.dart

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:convert';
7+
8+
import 'package:collection/collection.dart';
9+
import 'package:http_parser/http_parser.dart';
10+
11+
import 'body.dart';
12+
import 'http_unmodifiable_map.dart';
13+
import 'utils.dart';
14+
15+
/// Retrieves the [Body] contained in the [message].
16+
///
17+
/// This is meant for internal use by `http` so the message body is accessible
18+
/// for subclasses of [Message] but hidden elsewhere.
19+
Body getBody(Message message) => message._body;
20+
21+
/// Represents logic shared between [Request] and [Response].
22+
abstract class Message {
23+
/// The HTTP headers.
24+
///
25+
/// This is immutable. A copy of this with new headers can be created using
26+
/// [change].
27+
final Map<String, String> headers;
28+
29+
/// Extra context that can be used by middleware and handlers.
30+
///
31+
/// For requests, this is used to pass data to inner middleware and handlers;
32+
/// for responses, it's used to pass data to outer middleware and handlers.
33+
///
34+
/// Context properties that are used by a particular package should begin with
35+
/// that package's name followed by a period. For example, if there was a
36+
/// package `foo` which contained a middleware `bar` and it wanted to take
37+
/// a context property, its property would be `"foo.bar"`.
38+
///
39+
/// This is immutable. A copy of this with new context values can be created
40+
/// using [change].
41+
final Map<String, Object> context;
42+
43+
/// The streaming body of the message.
44+
///
45+
/// This can be read via [read] or [readAsString].
46+
final Body _body;
47+
48+
/// Creates a new [Message].
49+
///
50+
/// [body] is the message body. It may be either a [String], a [List<int>], a
51+
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
52+
/// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to
53+
/// UTF-8.
54+
///
55+
/// If [headers] is `null`, it's treated as empty.
56+
///
57+
/// If [encoding] is passed, the "encoding" field of the Content-Type header
58+
/// in [headers] will be set appropriately. If there is no existing
59+
/// Content-Type header, it will be set to "application/octet-stream".
60+
Message(body,
61+
{Encoding encoding,
62+
Map<String, String> headers,
63+
Map<String, Object> context})
64+
: this._(new Body(body, encoding), headers, context);
65+
66+
Message._(Body body, Map<String, String> headers, Map<String, Object> context)
67+
: _body = body,
68+
headers = new HttpUnmodifiableMap<String>(_adjustHeaders(headers, body),
69+
ignoreKeyCase: true),
70+
context =
71+
new HttpUnmodifiableMap<Object>(context, ignoreKeyCase: false);
72+
73+
/// If `true`, the stream returned by [read] won't emit any bytes.
74+
///
75+
/// This may have false negatives, but it won't have false positives.
76+
bool get isEmpty => _body.contentLength == 0;
77+
78+
/// The contents of the content-length field in [headers].
79+
///
80+
/// If not set, `null`.
81+
int get contentLength {
82+
if (_contentLengthCache != null) return _contentLengthCache;
83+
if (!headers.containsKey('content-length')) return null;
84+
_contentLengthCache = int.parse(headers['content-length']);
85+
return _contentLengthCache;
86+
}
87+
int _contentLengthCache;
88+
89+
/// The MIME type declared in [headers].
90+
///
91+
/// This is parsed from the Content-Type header in [headers]. It contains only
92+
/// the MIME type, without any Content-Type parameters.
93+
///
94+
/// If [headers] doesn't have a Content-Type header, this will be `null`.
95+
String get mimeType {
96+
var contentType = _contentType;
97+
if (contentType == null) return null;
98+
return contentType.mimeType;
99+
}
100+
101+
/// The encoding of the body returned by [read].
102+
///
103+
/// This is parsed from the "charset" parameter of the Content-Type header in
104+
/// [headers].
105+
///
106+
/// If [headers] doesn't have a Content-Type header or it specifies an
107+
/// encoding that [dart:convert] doesn't support, this will be `null`.
108+
Encoding get encoding {
109+
var contentType = _contentType;
110+
if (contentType == null) return null;
111+
if (!contentType.parameters.containsKey('charset')) return null;
112+
return Encoding.getByName(contentType.parameters['charset']);
113+
}
114+
115+
/// The parsed version of the Content-Type header in [headers].
116+
///
117+
/// This is cached for efficient access.
118+
MediaType get _contentType {
119+
if (_contentTypeCache != null) return _contentTypeCache;
120+
if (!headers.containsKey('content-type')) return null;
121+
_contentTypeCache = new MediaType.parse(headers['content-type']);
122+
return _contentTypeCache;
123+
}
124+
MediaType _contentTypeCache;
125+
126+
/// Returns the message body as byte chunks.
127+
///
128+
/// Throws a [StateError] if [read] or [readAsString] has already been called.
129+
Stream<List<int>> read() => _body.read();
130+
131+
/// Returns the message body as a string.
132+
///
133+
/// If [encoding] is passed, that's used to decode the body. Otherwise the
134+
/// encoding is taken from the Content-Type header. If that doesn't exist or
135+
/// doesn't have a "charset" parameter, UTF-8 is used.
136+
///
137+
/// Throws a [StateError] if [read] or [readAsString] has already been called.
138+
Future<String> readAsString([Encoding encoding]) {
139+
encoding ??= this.encoding ?? UTF8;
140+
return encoding.decodeStream(read());
141+
}
142+
143+
/// Creates a copy of this by copying existing values and applying specified
144+
/// changes.
145+
Message change(
146+
{Map<String, String> headers, Map<String, Object> context, body});
147+
}
148+
149+
/// Adds information about encoding to [headers].
150+
///
151+
/// Returns a new map without modifying [headers].
152+
Map<String, String> _adjustHeaders(Map<String, String> headers, Body body) {
153+
var sameEncoding = _sameEncoding(headers, body);
154+
if (sameEncoding) {
155+
if (body.contentLength == null ||
156+
getHeader(headers, 'content-length') == body.contentLength.toString()) {
157+
return headers ?? const HttpUnmodifiableMap.empty();
158+
} else if (body.contentLength == 0 &&
159+
(headers == null || headers.isEmpty)) {
160+
return const HttpUnmodifiableMap.empty();
161+
}
162+
}
163+
164+
var newHeaders = headers == null
165+
? new CaseInsensitiveMap<String>()
166+
: new CaseInsensitiveMap<String>.from(headers);
167+
168+
if (!sameEncoding) {
169+
if (newHeaders['content-type'] == null) {
170+
newHeaders['content-type'] =
171+
'application/octet-stream; charset=${body.encoding.name}';
172+
} else {
173+
var contentType = new MediaType.parse(newHeaders['content-type'])
174+
.change(parameters: {'charset': body.encoding.name});
175+
newHeaders['content-type'] = contentType.toString();
176+
}
177+
}
178+
179+
if (body.contentLength != null) {
180+
var coding = newHeaders['transfer-encoding'];
181+
if (coding == null || equalsIgnoreAsciiCase(coding, 'identity')) {
182+
newHeaders['content-length'] = body.contentLength.toString();
183+
}
184+
}
185+
186+
return newHeaders;
187+
}
188+
189+
/// Returns whether [headers] declares the same encoding as [body].
190+
bool _sameEncoding(Map<String, String> headers, Body body) {
191+
if (body.encoding == null) return true;
192+
193+
var contentType = getHeader(headers, 'content-type');
194+
if (contentType == null) return false;
195+
196+
var charset = new MediaType.parse(contentType).parameters['charset'];
197+
return Encoding.getByName(charset) == body.encoding;
198+
}

0 commit comments

Comments
 (0)