diff --git a/code_generator/README.md b/code_generator/README.md index 9028341..83bf273 100644 --- a/code_generator/README.md +++ b/code_generator/README.md @@ -36,6 +36,8 @@ Usage: flutter_storyblok_code_generator generate [arguments] -h, --help Print this usage information. -s, --space_id (mandatory) Your Storyblok Space ID -p, --personal_access_token (mandatory) Your Personal Access Token, not your Space access token +-l, --space_location The server location of the space + [eu (default), us, ca, ap, cn] -r, --rate_limit Your rate limit (depending on your plan) (defaults to "3") -o, --output_path A directory path where the output file "bloks.generated.dart" will be created diff --git a/code_generator/bin/flutter_storyblok_code_generator.dart b/code_generator/bin/flutter_storyblok_code_generator.dart index be64a95..a476a9b 100644 --- a/code_generator/bin/flutter_storyblok_code_generator.dart +++ b/code_generator/bin/flutter_storyblok_code_generator.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:args/command_runner.dart'; +import 'package:flutter_storyblok/models.dart' as sb; import 'package:flutter_storyblok_code_generator/flutter_storyblok_code_generator.dart'; import 'package:flutter_storyblok_code_generator/src/utils/utils.dart'; @@ -26,6 +27,7 @@ void main(List args) async { class GenerateCommand extends Command { static const _optionSpaceId = "space_id"; static const _optionPAT = "personal_access_token"; + static const _optionLocation = "space_location"; static const _optionRateLimit = "rate_limit"; static const _nameOutputPath = "output_path"; static const _outPutFileName = "bloks.generated.dart"; @@ -43,6 +45,14 @@ class GenerateCommand extends Command { help: "Your Personal Access Token, not your Space access token", mandatory: true, ); + argParser.addOption( + _optionLocation, + abbr: "l", + help: "The server location of the space", + mandatory: false, + allowed: sb.Region.values.map((e) => e.name), + defaultsTo: sb.Region.eu.name, + ); argParser.addOption( _optionRateLimit, abbr: "r", @@ -73,8 +83,9 @@ class GenerateCommand extends Command { final pat = results[_optionPAT] as String; final rateLimit = results[_optionRateLimit] as String; final outputPath = results[_nameOutputPath] as String; + final region = results[_optionLocation] as String; - final apiClient = StoryblokHttpClient(spaceId, pat, int.parse(rateLimit)); + final apiClient = StoryblokHttpClient(spaceId, pat, sb.Region.values.byName(region), int.parse(rateLimit)); final datasourcesWithEntriesFuture = apiClient.getDatasourcesWithEntries(); final componentsFuture = apiClient.getComponents(); diff --git a/code_generator/lib/src/http_client.dart b/code_generator/lib/src/http_client.dart index 486e2bc..4b85556 100644 --- a/code_generator/lib/src/http_client.dart +++ b/code_generator/lib/src/http_client.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter_storyblok/models.dart' as sb; import 'package:flutter_storyblok_code_generator/src/models/component.dart'; import 'package:flutter_storyblok_code_generator/src/models/datasource.dart'; import 'package:flutter_storyblok_code_generator/src/models/datasource_entry.dart'; @@ -14,11 +15,13 @@ class StoryblokHttpClient { StoryblokHttpClient( this.spaceId, this.authorization, + this.region, int rateLimit, ) : _rateLimit = _RateLimit(rateLimit); final String spaceId; final String authorization; + final sb.Region region; final _RateLimit _rateLimit; Future> getComponents() async { @@ -58,14 +61,14 @@ class StoryblokHttpClient { await _rateLimit(); final response = await http.get( - Uri.https("mapi.storyblok.com", "/v1/spaces/$spaceId/$path", params), + region.buildUri(path: "v1/spaces/$spaceId/$path", queryParameters: params), headers: {"Authorization": authorization}, ); final json = jsonDecode(response.body); if (json is JSONMap) { return json; } else if (json is List) { - throwMessage("Failed to fetch $path make sure space_id is correct. Error body: $json"); + throwMessage("Failed to fetch $path make sure space_id and region is correct. Error body: $json"); } else { throwMessage("Unknown error"); } @@ -100,3 +103,26 @@ class _RateLimit { _currentThrottleCounter += 1; } } + +extension RegionCodeGen on sb.Region { + String get baseUrl { + switch (this) { + case sb.Region.eu: + return "https://mapi.storyblok.com"; + case sb.Region.us: + return "https://api-us.storyblok.com"; + case sb.Region.ap: + return "https://api-ap.storyblok.com"; + case sb.Region.ca: + return "https://api-ca.storyblok.com"; + case sb.Region.cn: + return "https://app.storyblokchina.cn"; + default: + return "https://mapi.storyblok.com"; + } + } + + Uri buildUri({required String path, JSONMap? queryParameters}) { + return Uri.parse("$baseUrl/$path").replace(queryParameters: queryParameters); + } +} diff --git a/code_generator/test/regions_test.dart b/code_generator/test/regions_test.dart new file mode 100644 index 0000000..e01d3c9 --- /dev/null +++ b/code_generator/test/regions_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_storyblok/models.dart' as sb; +import 'package:flutter_storyblok_code_generator/src/http_client.dart'; +import 'package:test/test.dart'; + +void main() { + test('Test region url build', () { + sb.Region.values.forEach(_expectRegion); + }); +} + +void _expectRegion(sb.Region region) { + switch (region) { + case sb.Region.eu: + expect( + region.buildUri(path: "somePath"), + Uri.parse("https://mapi.storyblok.com/somePath"), + ); + expect( + region.buildUri(path: "somePath", queryParameters: {"foo": "bar"}), + Uri.parse("https://mapi.storyblok.com/somePath?foo=bar"), + ); + case sb.Region.us: + expect( + region.buildUri(path: "somePath"), + Uri.parse("https://api-us.storyblok.com/somePath"), + ); + expect( + region.buildUri(path: "somePath", queryParameters: {"foo": "bar"}), + Uri.parse("https://api-us.storyblok.com/somePath?foo=bar"), + ); + case sb.Region.ca: + expect( + region.buildUri(path: "somePath"), + Uri.parse("https://api-ca.storyblok.com/somePath"), + ); + expect( + region.buildUri(path: "somePath", queryParameters: {"foo": "bar"}), + Uri.parse("https://api-ca.storyblok.com/somePath?foo=bar"), + ); + case sb.Region.ap: + expect( + region.buildUri(path: "somePath"), + Uri.parse("https://api-ap.storyblok.com/somePath"), + ); + expect( + region.buildUri(path: "somePath", queryParameters: {"foo": "bar"}), + Uri.parse("https://api-ap.storyblok.com/somePath?foo=bar"), + ); + case sb.Region.cn: + expect( + region.buildUri(path: "somePath"), + Uri.parse("https://app.storyblokchina.cn/somePath"), + ); + expect( + region.buildUri(path: "somePath", queryParameters: {"foo": "bar"}), + Uri.parse("https://app.storyblokchina.cn/somePath?foo=bar"), + ); + } +} diff --git a/lib/models.dart b/lib/models.dart index 5c82c97..e8cbebc 100644 --- a/lib/models.dart +++ b/lib/models.dart @@ -7,3 +7,4 @@ export 'src/models/pagination.dart'; export 'src/models/resolve_links.dart'; export 'src/models/story_identifier.dart'; export 'src/models/story.dart'; +export 'src/models/region.dart'; diff --git a/lib/src/models/region.dart b/lib/src/models/region.dart new file mode 100644 index 0000000..9cfc843 --- /dev/null +++ b/lib/src/models/region.dart @@ -0,0 +1,7 @@ +enum Region { + eu, + us, + ca, + ap, + cn; +} diff --git a/lib/src/storyblok_client.dart b/lib/src/storyblok_client.dart index 0a9a86d..86c7c90 100644 --- a/lib/src/storyblok_client.dart +++ b/lib/src/storyblok_client.dart @@ -6,15 +6,14 @@ import 'package:http/http.dart' as http; /// Used to fetch content from the Storyblok Content Delivery API final class StoryblokClient { - static const _apiHost = "api.storyblok.com"; - - static const _pathStories = "v2/cdn/stories"; - static const _pathDatasources = "v2/cdn/datasources"; - static const _pathDatasourceEntries = "v2/cdn/datasource_entries"; - static const _pathLinks = "v2/cdn/links"; - static const _pathTags = "v2/cdn/tags"; + static const _pathStories = "stories"; + static const _pathDatasources = "datasources"; + static const _pathDatasourceEntries = "datasource_entries"; + static const _pathLinks = "links"; + static const _pathTags = "tags"; StoryblokClient({ + Region region = Region.eu, required String accessToken, ContentVersion? version, bool useCacheInvalidation = true, @@ -24,8 +23,10 @@ final class StoryblokClient { }, _version = version, _useCacheInvalidation = useCacheInvalidation, - _storyContentBuilder = storyContentBuilder; + _storyContentBuilder = storyContentBuilder, + _region = region; + final Region _region; final ContentVersion? _version; final Map _baseParameters; final bool _useCacheInvalidation; @@ -274,11 +275,11 @@ final class StoryblokClient { Map? queryParameters, }) async { final cacheVersion = _cacheVersion; + // TODO: Rate limit https://www.storyblok.com/docs/api/content-delivery/v2/getting-started/rate-limit - final uri = Uri.https( - _apiHost, - path, - { + final uri = _region.buildUri( + path: path, + queryParameters: { ..._baseParameters, if (queryParameters != null) ...queryParameters, }, @@ -312,3 +313,24 @@ extension _DateTimeFormat on DateTime { return "$year-$month-$day $hour:$minute"; } } + +extension RegionUrl on Region { + String get baseUrl { + switch (this) { + case Region.eu: + return "https://api.storyblok.com/v2/cdn"; + case Region.us: + return "https://api-us.storyblok.com/v2/cdn"; + case Region.ca: + return "https://api-ca.storyblok.com/v2/cdn"; + case Region.ap: + return "https://api-ap.storyblok.com/v2/cdn"; + case Region.cn: + return "https://app.storyblokchina.cn"; + } + } + + Uri buildUri({required String path, Map? queryParameters}) { + return Uri.parse("$baseUrl/$path").replace(queryParameters: queryParameters); + } +} diff --git a/test/regions_test.dart b/test/regions_test.dart new file mode 100644 index 0000000..08d7bf6 --- /dev/null +++ b/test/regions_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_storyblok/models.dart'; +import 'package:flutter_storyblok/src/storyblok_client.dart'; +import 'package:test/test.dart'; + +void main() { + test('Test region url build', () { + Region.values.forEach(_expectRegion); + }); +} + +void _expectRegion(Region region) { + switch (region) { + case Region.eu: + expect( + region.buildUri(path: "somePath"), + Uri.parse("https://api.storyblok.com/v2/cdn/somePath"), + ); + expect( + region.buildUri(path: "somePath", queryParameters: {"foo": "bar"}), + Uri.parse("https://api.storyblok.com/v2/cdn/somePath?foo=bar"), + ); + case Region.us: + expect( + region.buildUri(path: "somePath"), + Uri.parse("https://api-us.storyblok.com/v2/cdn/somePath"), + ); + expect( + region.buildUri(path: "somePath", queryParameters: {"foo": "bar"}), + Uri.parse("https://api-us.storyblok.com/v2/cdn/somePath?foo=bar"), + ); + case Region.ca: + expect( + region.buildUri(path: "somePath"), + Uri.parse("https://api-ca.storyblok.com/v2/cdn/somePath"), + ); + expect( + region.buildUri(path: "somePath", queryParameters: {"foo": "bar"}), + Uri.parse("https://api-ca.storyblok.com/v2/cdn/somePath?foo=bar"), + ); + case Region.ap: + expect( + region.buildUri(path: "somePath"), + Uri.parse("https://api-ap.storyblok.com/v2/cdn/somePath"), + ); + expect( + region.buildUri(path: "somePath", queryParameters: {"foo": "bar"}), + Uri.parse("https://api-ap.storyblok.com/v2/cdn/somePath?foo=bar"), + ); + case Region.cn: + expect( + region.buildUri(path: "somePath"), + Uri.parse("https://app.storyblokchina.cn/somePath"), + ); + expect( + region.buildUri(path: "somePath", queryParameters: {"foo": "bar"}), + Uri.parse("https://app.storyblokchina.cn/somePath?foo=bar"), + ); + } +}