Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- Generate models from Figma's REST API specification ([@donny-dont](https://github.com/donny-dont))
- Update to v0.40.0 of REST API specification ([@donny-dont](https://github.com/donny-dont))
- Add `Slot` support ([@donny-dont](https://github.com/donny-dont))
- Expose additional error information from API ([@donny-dont](https://github.com/donny-dont))
- Require `sdk: ^3.8.0`

## 7.5.0
Expand Down
109 changes: 79 additions & 30 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,9 @@ class FigmaClient {
final uri = Uri.parse(url);

return _send('GET', uri, _authHeaders).then((res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
return jsonDecode(res.body);
} else {
throw FigmaException(code: res.statusCode, message: res.body);
}
_checkResponse(res);

return jsonDecode(res.body);
});
}

Expand Down Expand Up @@ -280,11 +278,9 @@ class FigmaClient {
final uri = Uri.https(base, '$version$path', query);

return _send('GET', uri, _authHeaders).then((res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
return jsonDecode(res.body);
} else {
throw FigmaException(code: res.statusCode, message: res.body);
}
_checkResponse(res);

return jsonDecode(res.body);
});
}

Expand All @@ -297,11 +293,9 @@ class FigmaClient {
final uri = Uri.https(base, '$version$path');

return _send('POST', uri, _authHeaders, body).then((res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
return jsonDecode(res.body);
} else {
throw FigmaException(code: res.statusCode, message: res.body);
}
_checkResponse(res);

return jsonDecode(res.body);
});
}

Expand All @@ -314,25 +308,52 @@ class FigmaClient {
final uri = Uri.https(base, '$version$path');

return _send('PUT', uri, _authHeaders, body).then((res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
return jsonDecode(res.body);
} else {
throw FigmaException(code: res.statusCode, message: res.body);
}
_checkResponse(res);

return jsonDecode(res.body);
});
}

/// Does a DELETE request towards the Figma API.
Future<dynamic> _deleteFigma(String version, String path) {
final uri = Uri.https(base, '$version$path');

return _send('DELETE', uri, _authHeaders).then((res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
return;
} else {
throw FigmaException(code: res.statusCode, message: res.body);
}
});
return _send('DELETE', uri, _authHeaders).then(_checkResponse);
}

void _checkResponse(Response res) {
final statusCode = res.statusCode;
if (statusCode >= 200 && statusCode < 300) {
return;
}

final errorResponse = jsonDecode(res.body)! as Map;
final message = errorResponse['message'] as String? ?? '';

// Rate limit errors contain additional data in the headers
if (statusCode == 429) {
final headers = res.headers;
final retryAfter = headers['Retry-After'];

throw FigmaRateLimitException(
retryAfter: retryAfter != null ? int.parse(retryAfter) : 0,
planTier: switch (headers['X-Figma-Plan-Tier']) {
'enterprise' => FigmaPlanTier.enterprise,
'org' => FigmaPlanTier.org,
'pro' => FigmaPlanTier.pro,
'student' => FigmaPlanTier.student,
_ => FigmaPlanTier.starter,
},
rateLimitType: headers['X-Figma-Rate-Limit-Type'] == 'high'
? FigmaRateLimitType.high
: FigmaRateLimitType.low,
upgradeLink: headers['X-Figma-Upgrade-Link'] ?? '',
code: statusCode,
message: message,
);
}

throw FigmaException(code: statusCode, message: message);
}

Map<String, String> get _authHeaders {
Expand All @@ -348,11 +369,39 @@ class FigmaClient {

/// An error from the [Figma API docs](https://www.figma.com/developers/api#errors).
class FigmaException implements Exception {
const FigmaException({required this.code, required this.message});

/// HTTP status code.
final int? code;
final int code;

/// Error message.
final String? message;
final String message;
}

class FigmaRateLimitException extends FigmaException {
const FigmaRateLimitException({
required this.retryAfter,
required this.planTier,
required this.rateLimitType,
required this.upgradeLink,
required super.code,
required super.message,
});

const FigmaException({this.code, this.message});
/// In seconds, how long before you should retry sending the request.
final int retryAfter;

/// The current plan tier of the resource the user is requesting.
final FigmaPlanTier planTier;

/// The type of rate limit the user is encountering, based on their seat type.
final FigmaRateLimitType rateLimitType;

/// A link to either the /pricing or /settings pages depending on the
/// plan/seat of the user.
final String upgradeLink;
}

enum FigmaPlanTier { enterprise, org, pro, starter, student }

enum FigmaRateLimitType { low, high }
5 changes: 4 additions & 1 deletion lib/src/client/io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Future<Response> _http2(

var status = 200;
final buffer = <int>[];
final responseHeaders = <String, String>{};

await for (final message in stream.incomingMessages) {
if (message is HeadersStreamMessage) {
Expand All @@ -42,6 +43,8 @@ Future<Response> _http2(
final value = utf8.decode(header.value);
if (name == ':status') {
status = int.parse(value);
} else {
responseHeaders[name] = value;
}
}
} else if (message is DataStreamMessage) {
Expand All @@ -51,5 +54,5 @@ Future<Response> _http2(

await transport.finish();

return Response(status, utf8.decode(buffer));
return Response(status, responseHeaders, utf8.decode(buffer));
}
11 changes: 8 additions & 3 deletions lib/src/client/shared.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ typedef SendRequest =
const String base = 'api.figma.com';

class Response {
const Response(this.statusCode, this.headers, this.body);

final int statusCode;
final Map<String, String> headers;
final String body;

const Response(this.statusCode, this.body);
}

Future<Response> http(
Expand All @@ -32,7 +33,11 @@ Future<Response> http(
..body = body ?? '';
final response = await client.send(request);
final responseBody = await response.stream.toBytes();
return Response(response.statusCode, utf8.decode(responseBody));
return Response(
response.statusCode,
response.headers,
utf8.decode(responseBody),
);
} finally {
client.close();
}
Expand Down
Loading