Skip to content

Commit

Permalink
feat(http_sfv): HTTP structured field values
Browse files Browse the repository at this point in the history
Adds a Dart implementation of Structured Field Values for HTTP ([RFC8941](https://www.rfc-editor.org/rfc/rfc8941.html))
  • Loading branch information
dnys1 committed Sep 23, 2024
1 parent 02c003e commit ef9eb74
Show file tree
Hide file tree
Showing 29 changed files with 2,455 additions and 0 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/http_sfv.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: http_sfv
on:
pull_request:
paths:
- ".github/workflows/http_sfv.yaml"
- "packages/http_sfv/**"

# Prevent duplicate runs due to Graphite
# https://graphite.dev/docs/troubleshooting#why-are-my-actions-running-twice
concurrency:
group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }}-${{ github.ref == 'refs/heads/main' && github.sha || ''}}
cancel-in-progress: true

jobs:
check:
strategy:
fail-fast: true
matrix:
sdk:
- stable
- "3.3"
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Git Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # 4.1.7
with:
submodules: recursive
- name: Setup Dart
uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 # 1.6.5
with:
sdk: ${{ matrix.sdk }}
- name: Get Packages
working-directory: packages/http_sfv
run: dart pub get
- name: Analyze
working-directory: packages/http_sfv
run: dart analyze
- name: Format
working-directory: packages/http_sfv
run: dart format --set-exit-if-changed .
- name: Test
working-directory: packages/http_sfv
run: dart test
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "packages/http_sfv/structured-field-tests"]
path = packages/http_sfv/structured-field-tests
url = https://github.com/httpwg/structured-field-tests
7 changes: 7 additions & 0 deletions packages/http_sfv/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/

# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock
3 changes: 3 additions & 0 deletions packages/http_sfv/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.1.0

- Initial release
42 changes: 42 additions & 0 deletions packages/http_sfv/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# http_sfv (Structured Field Values)

A Dart implementation of the [Structured Field Values for HTTP (RFC 8941)](https://www.rfc-editor.org/rfc/rfc8941.html) specification.

## Usage

Use `StructuredFieldValue.decode` to parse a header string into a structured field value.

```dart
import 'package:http_sfv/http_sfv.dart';
void main() {
const header = '"foo";bar;baz=tok, (foo bar);bat';
final decoded = StructuredFieldValue.decode(
header,
type: StructuredFieldValueType.list,
);
print(decoded);
// Prints: List(Item(foo, bar: true, baz: tok), InnerList([Item(foo), Item(bar)], bat: true))
}
```

Use `StructuredFieldValue.encode` to convert a structured field value to a header string.

```dart
import 'package:http_sfv/http_sfv.dart';
void main() {
final dictionary = StructuredFieldDictionary({
'a': false,
'b': true,
'c': StructuredFieldItem(
true,
parameters: {
'foo': 'bar',
},
),
});
print(dictionary.encode());
// Prints: "a=?0, b, c;foo=bar"
}
```
1 change: 1 addition & 0 deletions packages/http_sfv/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:lints/recommended.yaml
42 changes: 42 additions & 0 deletions packages/http_sfv/example/http_sfv_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'package:http_sfv/http_sfv.dart';

void main() {
final dictionary = StructuredFieldDictionary({
'a': false,
'b': true,
'c': StructuredFieldItem(
true,
parameters: {
'foo': 'bar',
},
),
});
print('dictionary: ${dictionary.encode()}');
// Prints: "a=?0, b, c;foo=bar"

const header = '"foo";bar;baz=tok, (foo bar);bat';
final decoded = StructuredFieldValue.decode(
header,
type: StructuredFieldValueType.list,
);
print('list: $decoded');
// Prints: List(Item(foo, bar: true, baz: tok), InnerList([Item(foo), Item(bar)], bat: true))

final list = StructuredFieldList([
StructuredFieldItem(
'foo',
parameters: {
'bar': true,
'baz': Token('tok'),
},
),
StructuredFieldInnerList(
[Token('foo'), Token('bar')],
parameters: {
'bat': true,
},
),
]);
print(decoded == list);
// Prints: true
}
9 changes: 9 additions & 0 deletions packages/http_sfv/lib/http_sfv.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Support for doing something awesome.
///
/// More dartdocs go here.
library;

export 'src/item_value.dart' show StructuredFieldItemValue;
export 'src/key.dart';
export 'src/token.dart';
export 'src/value.dart';
121 changes: 121 additions & 0 deletions packages/http_sfv/lib/src/character.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import 'package:http_sfv/http_sfv.dart';

/// An ASCII character.
extension type const Character(int char) implements int {
static const Character space = Character(0x20); // ' '
static const Character tab = Character(0x09); // '\t'
static const Character doubleQuote = Character(0x22); // '"'
static const Character questionMark = Character(0x3F); // '?'
static const Character star = Character(0x2A); // '*'
static const Character colon = Character(0x3A); // ':'
static const Character zero = Character(0x30); // '0'
static const Character one = Character(0x31); // '1'
static const Character nine = Character(0x39); // '9'
static const Character upperA = Character(0x41); // 'A'
static const Character upperZ = Character(0x5A); // 'Z'
static const Character lowerA = Character(0x61); // 'a'
static const Character lowerZ = Character(0x7A); // 'z'
static const Character exclamationMark = Character(0x21); // '!'
static const Character numberSign = Character(0x23); // '#'
static const Character dollarSign = Character(0x24); // '$'
static const Character percent = Character(0x25); // '%'
static const Character and = Character(0x26); // '&'
static const Character singleQuote = Character(0x27); // '\''
static const Character plus = Character(0x2B); // '+'
static const Character minus = Character(0x2D); // '-'
static const Character dash = minus; // '-'
static const Character decimal = Character(0x2E); // '.'
static const Character caret = Character(0x5E); // '^'
static const Character underscore = Character(0x5F); // '_'
static const Character backtick = Character(0x60); // '`'
static const Character pipe = Character(0x7C); // '|'
static const Character tilde = Character(0x7E); // '~'
static const Character slash = Character(0x2F); // '/'
static const Character backslash = Character(0x5C); // '\'
static const Character semiColon = Character(0x3B); // ';'
static const Character equals = Character(0x3D); // '='
static const Character comma = Character(0x2C); // ','
static const Character openParen = Character(0x28); // '('
static const Character closeParen = Character(0x29); // ')'
static const Character at = Character(0x40); // '@'
static const Character maxAscii = Character(0x7F); // '\x7F'

static const Character lowerAlphaA = Character(0x61); // 'a'
static const Character lowerAlphaB = Character(0x62); // 'b'
static const Character lowerAlphaC = Character(0x63); // 'c'
static const Character lowerAlphaD = Character(0x64); // 'd'
static const Character lowerAlphaE = Character(0x65); // 'e'
static const Character lowerAlphaF = Character(0x66); // 'f'
static const Character upperAlphaA = Character(0x41); // 'A'
static const Character upperAlphaB = Character(0x42); // 'B'
static const Character upperAlphaC = Character(0x43); // 'C'
static const Character upperAlphaD = Character(0x44); // 'D'
static const Character upperAlphaE = Character(0x45); // 'E'
static const Character upperAlphaF = Character(0x46); // 'F'

/// An alpha character, e.g. A-Z or a-z.
bool get isAlpha =>
this >= upperA && this <= upperZ || this >= lowerA && this <= lowerZ;

/// A lowercase alpha character, e.g. a-z.
bool get isLowerAlpha => this >= lowerA && this <= lowerZ;

/// A digit character, e.g. 0-9.
bool get isDigit => this >= zero && this <= nine;

/// A valid hex character, e.g. 0-9, A-F, or a-f.
bool get isValidHex =>
isDigit ||
this >= upperAlphaA && this <= upperAlphaF ||
this >= lowerAlphaA && this <= lowerAlphaF;

/// An optional whitespace character, e.g. ' ' or '\t'.
bool get isOptionalWhitespace => this == space || this == tab;

/// Whether this is a valid [Token] character.
bool get isExtendedTokenCharacter {
if (isAlpha || isDigit) {
return true;
}
return this == exclamationMark ||
this == numberSign ||
this == dollarSign ||
this == percent ||
this == and ||
this == singleQuote ||
this == star ||
this == plus ||
this == minus ||
this == decimal ||
this == caret ||
this == underscore ||
this == backtick ||
this == pipe ||
this == tilde ||
this == colon ||
this == slash;
}

/// Whether this is a valid [Key] character.
bool get isKeyCharacter {
if (isLowerAlpha || isDigit) {
return true;
}
return this == underscore ||
this == minus ||
this == decimal ||
this == star;
}

/// A visible ASCII character (VCHAR), e.g. 0x21 (!) to 0x7E (~).
///
/// See: https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
bool get isVisibleAscii => this >= exclamationMark && this < maxAscii;

/// An ASCII character which is not a VCHAR or SP.
bool get isInvalidAscii => !isVisibleAscii && this != space;

/// Whether this is a valid base64 character.
bool get isValidBase64 =>
isAlpha || isDigit || this == plus || this == slash || this == equals;
}
Loading

0 comments on commit ef9eb74

Please sign in to comment.