├── .gitignore ├── LICENSE ├── README.md ├── VERSION ├── bin ├── .gitignore ├── apigen.dart ├── googleapis_fetch.dart ├── googleapis_generate.dart └── mix.dart ├── contrib └── c9setup.sh ├── lib ├── base.mixologist.yaml ├── base_fields.mixologist.yaml ├── generator.dart ├── generator │ ├── config.dart │ ├── dart.dart │ ├── discovery │ │ ├── discovery_parser.dart │ │ └── json_marshaller.dart │ ├── emitter.dart │ ├── emitter_util.dart │ ├── generator.dart │ ├── ir.dart │ ├── protobuf │ │ ├── protobuf_marshaller.dart │ │ ├── protobuf_parser.dart │ │ └── service.dart │ ├── template_loader.dart │ └── util.dart ├── google │ └── protobuf │ │ ├── compiler │ │ └── plugin.pb.dart │ │ └── descriptor.pb.dart ├── impl.dart ├── impl_html.dart ├── impl_server.dart ├── indexed_cache.dart ├── mixins │ ├── base_map.dart │ ├── base_podo.dart │ ├── copy_clone.dart │ ├── dot_access.dart │ ├── global.dart │ ├── has_api_type.dart │ ├── immutable.dart │ ├── is_map.dart │ ├── lazy.dart │ ├── local.dart │ ├── nosuchmethod.dart │ ├── observable.dart │ └── patch_map.dart ├── mixologist.dart ├── mixologist │ ├── config.dart │ ├── generator.dart │ ├── import.dart │ ├── in.dart │ └── out.dart ├── raw.api.json ├── raw.streamy.yaml ├── runtime │ ├── api.dart │ ├── batching.dart │ ├── cache.dart │ ├── dedup.dart │ ├── entity_util.dart │ ├── equality.dart │ ├── error.dart │ ├── global.dart │ ├── http.dart │ ├── json.dart │ ├── lazy.dart │ ├── marshal.dart │ ├── multiplexer.dart │ ├── proxy.dart │ ├── request.dart │ ├── response.dart │ ├── root.dart │ ├── tracing.dart │ ├── transforms.dart │ ├── util.dart │ └── wire.dart ├── src │ └── fs │ │ ├── fs.dart │ │ ├── local_fs.dart │ │ └── transform_fs.dart ├── streamy.dart ├── templates │ ├── lazy_resource_getter.mustache │ ├── list.mustache │ ├── map.mustache │ ├── marshal.mustache │ ├── marshal_handle.mustache │ ├── marshal_mapbacked.mustache │ ├── object_add_global.mustache │ ├── object_clone.mustache │ ├── object_ctor.mustache │ ├── object_getter.mustache │ ├── object_patch.mustache │ ├── object_remove.mustache │ ├── object_setter.mustache │ ├── proto_marshaller_ctor.mustache │ ├── pubspec.mustache │ ├── request_clone.mustache │ ├── request_ctor.mustache │ ├── request_marshal_payload.mustache │ ├── request_method.mustache │ ├── request_param_getter.mustache │ ├── request_param_setter.mustache │ ├── request_remove.mustache │ ├── request_send.mustache │ ├── request_send_direct.mustache │ ├── request_unmarshal_response.mustache │ ├── root_begin_transaction.mustache │ ├── root_constructor.mustache │ ├── root_send.mustache │ ├── root_transaction_constructor.mustache │ ├── string_list.mustache │ ├── string_map.mustache │ ├── unmarshal.mustache │ └── unmarshal_json.mustache ├── testing │ ├── dynamic_entity.dart │ └── testing.dart └── transformer.dart ├── presubmit.sh ├── pub-server.go ├── pubspec.yaml ├── test ├── .gitignore ├── all_tests.dart ├── base_test.dart ├── benchmark.dart ├── benchmark.html ├── benchmark_html.dart ├── generated │ ├── .gitignore │ ├── README │ ├── addendum_addendum.json │ ├── addendum_test.dart │ ├── addendum_test.json │ ├── addendum_test.streamy.yaml │ ├── bank_api_test.json │ ├── bank_api_test.streamy.yaml │ ├── benchmark_test.json │ ├── benchmark_test.streamy.yaml │ ├── handler_test.dart │ ├── handler_test.json │ ├── handler_test.streamy.yaml │ ├── identifier_name_test.dart │ ├── identifier_name_test.json │ ├── identifier_name_test.streamy.yaml │ ├── illegal_names_test.dart │ ├── illegal_names_test.json │ ├── illegal_names_test.streamy.yaml │ ├── import_test.proto │ ├── import_test.streamy.yaml │ ├── method_get_test.dart │ ├── method_get_test.json │ ├── method_get_test.streamy.yaml │ ├── method_params_test.dart │ ├── method_params_test.json │ ├── method_params_test.streamy.yaml │ ├── method_post_test.dart │ ├── method_post_test.json │ ├── method_post_test.streamy.yaml │ ├── nested_resources_test.dart │ ├── nested_resources_test.json │ ├── nested_resources_test.streamy.yaml │ ├── proto_test.dart │ ├── proto_test.proto │ ├── proto_test.streamy.yaml │ ├── reserved_expansion_path_param_test.dart │ ├── reserved_expansion_path_param_test.json │ ├── reserved_expansion_path_param_test.streamy.yaml │ ├── schema_object_test.dart │ ├── schema_object_test.json │ ├── schema_object_test.streamy.yaml │ ├── service_test.dart │ ├── service_test.streamy.yaml │ └── service_test_interface.dart ├── generator │ └── emitter_test.dart ├── integration │ ├── README.md │ ├── apigen_test │ │ ├── bin │ │ │ └── main.dart │ │ └── pubspec.yaml │ ├── run_tests.sh │ └── transformer_test │ │ ├── bin │ │ └── main.dart │ │ ├── lib │ │ ├── bank_api_test.json │ │ └── bank_api_test.streamy.yaml │ │ └── pubspec.yaml ├── mixins │ ├── dot_access_test.dart │ ├── immutable_test.dart │ ├── is_map_test.dart │ └── patch_map_test.dart ├── project.dart ├── proto_tests.dart ├── runtime │ ├── batching_test.dart │ ├── branching_test.dart │ ├── cache_test.dart │ ├── dedup_test.dart │ ├── error_test.dart │ ├── http_test.dart │ ├── json_test.dart │ ├── marshaller_test.dart │ ├── multiplexer_test.dart │ ├── proxy_test.dart │ ├── request_test.dart │ ├── transaction_test.dart │ └── transforms_test.dart ├── streamy_test.dart ├── test_in_browser.dart ├── test_in_browser.html └── utils.dart └── web └── README /.gitignore: -------------------------------------------------------------------------------- 1 | packages 2 | .project 3 | pubspec.lock 4 | dart-sdk 5 | .c9revisions 6 | dartsdk-linux-64.tar.gz 7 | .settings 8 | test/benchmark_html.dart.* 9 | .idea 10 | /build 11 | # Generated by integration tests: 12 | test/integration/bankapi 13 | test/integration/apigen_test/build 14 | test/integration/transformer_test/build 15 | pub-server 16 | .pub 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This package is deprecated 2 | 3 | Consider the following alternatives: 4 | 5 | - [built_value](https://github.com/google/built_value.dart) provides advanced features for building any Dart client for JSON-speaking APIs 6 | - [googleapis](https://pub.dartlang.org/packages/googleapis) is a collection of pregenerated clients to various pubilc Google APIs. 7 | - [gRPC](https://pub.dartlang.org/packages/grpc) is a high-performance, open-source universal RPC framework (https://grpc.io/) 8 | 9 | ## What was Streamy? 10 | 11 | Streamy was a JSON RPC framework for applications written using [Dart programming language](http://dartlang.org). It generated a Dart client library from a [Google API Discovery](https://developers.google.com/discovery/) file. 12 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.2-dev.1 -------------------------------------------------------------------------------- /bin/.gitignore: -------------------------------------------------------------------------------- 1 | packages -------------------------------------------------------------------------------- /bin/googleapis_fetch.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Downloads all Google APIs from the Google Discovery service and saves them 3 | * in a specified location. 4 | * 5 | * After running this program you can run googleapis_generate.dart to generate 6 | * Streamy client API packages. 7 | */ 8 | import 'dart:convert'; 9 | import 'dart:io'; 10 | import 'dart:async'; 11 | import 'package:args/args.dart'; 12 | import 'package:quiver/strings.dart'; 13 | import 'package:quiver/async.dart'; 14 | 15 | var http = new HttpClient(); 16 | 17 | Directory outputDir; 18 | 19 | main(List args) { 20 | var errors = []; 21 | var argp = new ArgParser() 22 | ..addOption( 23 | 'output-dir', 24 | abbr: 'o', 25 | help: 'Directory where downloaded discovery files are to be stored.', 26 | callback: (v) { 27 | if (isBlank(v)) { 28 | errors.add('ERROR: missing option --output-dir'); 29 | return; 30 | } 31 | outputDir = new Directory(v); 32 | outputDir.create(recursive: true); 33 | }); 34 | argp.parse(args); 35 | 36 | if (!errors.isEmpty) { 37 | errors.forEach(print); 38 | print(argp.getUsage()); 39 | exit(1); 40 | } 41 | getUrlAsString('https://www.googleapis.com/discovery/v1/apis') 42 | .then((String discovery) { 43 | Map json = JSON.decoder.convert(discovery); 44 | List apis = json['items']; 45 | var apiNames = new Set(); 46 | 47 | Future fetchApi(Map api) { 48 | String url = api['discoveryRestUrl']; 49 | String name = api['name']; 50 | String version = api['version']; 51 | 52 | print('Fetching ${name}:${version} from ${url}'); 53 | apiNames.add(name); 54 | 55 | return getUrlAsString(url).then((d) { 56 | var discoveryFile = 57 | new File('${outputDir.path}/${name}_${version}.json'); 58 | discoveryFile.writeAsStringSync(d); 59 | }); 60 | } 61 | 62 | forEachAsync(apis, fetchApi).then((_) { 63 | print('------------------------------------'); 64 | print('Fetched ${apis.length} versions of API discovery documents'); 65 | print('From ${apiNames.length} unique APIs.'); 66 | }); 67 | }); 68 | } 69 | 70 | Future getUrlAsString(String url) => 71 | http.getUrl(Uri.parse(url)) 72 | .then((req) => req.close()) 73 | .then(readResponse); 74 | 75 | Future readResponse(HttpClientResponse resp) => 76 | resp.transform(const Utf8Decoder()) 77 | .fold(new StringBuffer(), (buf, e) => buf..write(e)) 78 | .then((buf) => buf.toString()); 79 | -------------------------------------------------------------------------------- /bin/googleapis_generate.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Generates Streamy client API packages for all discovery documents located in 3 | * a given directory. 4 | * 5 | * This program is a follow-up to googleapis_fetch.dart 6 | */ 7 | import 'dart:io'; 8 | import 'dart:async'; 9 | import 'package:args/args.dart'; 10 | import 'package:path/path.dart' as path; 11 | import 'package:quiver/async.dart'; 12 | import 'package:quiver/strings.dart'; 13 | import 'package:streamy/generator.dart'; 14 | 15 | Directory inputDir; 16 | Directory outputDir; 17 | String localStreamyLocation; 18 | 19 | main(List args) { 20 | var errors = []; 21 | var argp = new ArgParser() 22 | ..addOption( 23 | 'input-dir', 24 | abbr: 'i', 25 | help: 'Directory containing discovery documents.', 26 | callback: (v) { 27 | if (isBlank(v)) { 28 | errors.add('ERROR: Missing --input-dir option'); 29 | return; 30 | } 31 | inputDir = new Directory(v); 32 | if (!inputDir.existsSync()) { 33 | errors.add( 34 | 'ERROR: input directory ${inputDir.path} does not exist'); 35 | return; 36 | } 37 | }) 38 | ..addOption( 39 | 'output-dir', 40 | abbr: 'o', 41 | help: 'Output directory where all packages will be written to.', 42 | defaultsTo: '/tmp/googleapis', 43 | callback: (v) { 44 | outputDir = new Directory(v); 45 | outputDir.create(recursive: true); 46 | }) 47 | ..addOption( 48 | 'local-streamy-location', 49 | help: 'Path to a local Streamy package. If specified the local ' 50 | 'version will be used instead of pub version.', 51 | callback: (String value) { 52 | localStreamyLocation = value; 53 | }); 54 | argp.parse(args); 55 | 56 | if (!errors.isEmpty) { 57 | errors.forEach(print); 58 | print(argp.getUsage()); 59 | exit(1); 60 | } 61 | 62 | var discoveryFiles = inputDir.listSync() 63 | .where((f) => f.path.endsWith('.json')) 64 | .toList(); 65 | forEachAsync(discoveryFiles, processDiscovery).then((_) { 66 | print('----------------------------------'); 67 | print('Generated ${discoveryFiles.length} packages.'); 68 | print('Results written to ${outputDir.absolute.path}'); 69 | }); 70 | } 71 | 72 | Future processDiscovery(File discoveryFile) { 73 | String discoveryPath = discoveryFile.path; 74 | 75 | print('Generating ${discoveryPath}'); 76 | 77 | String basename = path.basename(discoveryPath); 78 | String prefix = path.basenameWithoutExtension(discoveryPath); 79 | String config = configYaml(basename, prefix); 80 | var rootDir = discoveryFile.parent; 81 | var configFile = new File('${rootDir.path}/${prefix}.streamy.yaml'); 82 | configFile.writeAsStringSync(config); 83 | 84 | 85 | return generateStreamyClientPackage( 86 | configFile, 87 | outputDir, 88 | packageName: prefix, 89 | localStreamyLocation: localStreamyLocation) 90 | ..catchError((e, s) { 91 | print('$e, $s'); 92 | }); 93 | } 94 | 95 | String configYaml(String discoveryFilePath, String prefix) => 96 | ''' 97 | discovery: ${discoveryFilePath} 98 | output: 99 | files: split 100 | prefix: ${prefix} 101 | base: 102 | class: Entity 103 | import: package:streamy/base.dart 104 | backing: map 105 | options: 106 | clone: true 107 | removers: true 108 | known: false 109 | '''; 110 | -------------------------------------------------------------------------------- /bin/mix.dart: -------------------------------------------------------------------------------- 1 | library streamy.mixologist.bin; 2 | 3 | import 'dart:async'; 4 | import 'dart:io' as io; 5 | import 'package:args/args.dart'; 6 | import 'package:path/path.dart' as path; 7 | import 'package:streamy/mixologist.dart' as mixologist; 8 | import 'package:streamy/src/fs/local_fs.dart'; 9 | import 'package:yaml/yaml.dart' as yaml; 10 | 11 | io.Directory outputDir; 12 | io.File configFile; 13 | io.Directory searchDir; 14 | 15 | main(List args) { 16 | parseArgs(args); 17 | mixologist.Config config; 18 | configFile.readAsString() 19 | .then((String configString) { 20 | config = mixologist.parseConfig(yaml.loadYaml(configString)); 21 | }) 22 | .then((_) => 23 | mixologist.mix(config, new LocalFileSystem(searchDir))) 24 | .then((String code) { 25 | var outputFilePath = path.join(outputDir.path, config.output); 26 | new io.Directory(path.dirname(outputFilePath)).createSync(recursive: true); 27 | var outputFile = new io.File(outputFilePath); 28 | return outputFile.writeAsString(code); 29 | }); 30 | } 31 | 32 | void parseArgs(List args) { 33 | var argp = new ArgParser() 34 | ..addOption('config-file', 35 | abbr: 'c', 36 | help: 'Path to *.mixologist.yaml configuration file', 37 | callback: (value) { 38 | configFile = new io.File(value); 39 | }) 40 | ..addOption('output-dir', 41 | abbr: 'o', 42 | help: 'Output directory', 43 | callback: (value) { 44 | outputDir = new io.Directory(value); 45 | }) 46 | ..addOption('search-dir', 47 | abbr: 's', 48 | help: 'Directory to look for mixins', 49 | defaultsTo: 'lib', 50 | callback: (value) { 51 | searchDir = new io.Directory(value); 52 | }); 53 | argp.parse(args); 54 | if (outputDir == null || configFile == null) { 55 | print(argp.getUsage()); 56 | return; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /contrib/c9setup.sh: -------------------------------------------------------------------------------- 1 | # Use this script to bootstrap your Cloud9 environment. This script: 2 | # - Tests SSH connection to GitHub 3 | # - Upgrades to the latest Dart SDK 4 | 5 | set +e 6 | echo "Testing connection to GitHub" 7 | ssh -T "git@github.com" 8 | SSH_RETURN_CODE=$? 9 | set -e 10 | 11 | # Because GitHub doesn't allow shell access the return code is 1 for success 12 | # and (afaik) 255 for failure. 13 | [ $SSH_RETURN_CODE -ne 1 ] && echo "Connection to GitHub failed" && exit 1 14 | 15 | echo "Downloading the latest Dart SDK" 16 | rm -f "./dartsdk-linux-64.tar.gz" 17 | wget --tries=3 "http://storage.googleapis.com/dart-editor-archive-integration/latest/dartsdk-linux-64.tar.gz" 18 | 19 | echo "Updating Dart SDK" 20 | rm -Rf ./dart-sdk 21 | tar -zxvf "./dartsdk-linux-64.tar.gz" 22 | 23 | echo "Make sure" `git config user.email` "is registered in your GitHub's Account Settings > Emails." 24 | echo "Make sure" `git config user.name` "is your GitHub user account name." 25 | echo "Make sure to add your Cloud9 SSH key to your GitHub's Account Settings > SSH Keys, which you can find on your Cloud9 Dashboard (look for 'Show your SSH key'." 26 | echo "Done." 27 | -------------------------------------------------------------------------------- /lib/base.mixologist.yaml: -------------------------------------------------------------------------------- 1 | output: base.dart 2 | library: streamy.base 3 | class: Entity 4 | paths: 5 | - mixins 6 | mixins: 7 | - base_map 8 | - is_map 9 | - observable 10 | - immutable 11 | - copy_clone 12 | - dot_access 13 | - local 14 | - global 15 | - patch_map 16 | - has_api_type 17 | -------------------------------------------------------------------------------- /lib/base_fields.mixologist.yaml: -------------------------------------------------------------------------------- 1 | output: base_fields.dart 2 | library: streamy.base 3 | class: Entity 4 | paths: 5 | - mixins 6 | mixins: 7 | - base_podo -------------------------------------------------------------------------------- /lib/generator/emitter_util.dart: -------------------------------------------------------------------------------- 1 | library streamy.generator.emitter_util; 2 | 3 | import 'package:mustache/mustache.dart' as mustache; 4 | import 'package:streamy/generator/dart.dart'; 5 | import 'package:streamy/generator/ir.dart'; 6 | import 'package:streamy/generator/util.dart'; 7 | 8 | const BASE_PREFIX = '_streamy_base_'; 9 | 10 | abstract class EmitterBase { 11 | 12 | Map get templates; 13 | String get objectPrefix; 14 | 15 | DartBody stringListBody(Iterable strings, {bool getter: false}) => 16 | new DartTemplateBody(templates['string_list'], { 17 | 'list': strings.map((i) => {'value': i}).toList(growable: false), 18 | 'getter': getter 19 | }); 20 | 21 | DartBody mapBody(Map map) { 22 | var data = []; 23 | map.forEach((key, value) { 24 | data.add({'key': key, 'value': value}); 25 | }); 26 | return new DartTemplateBody(templates['string_map'], {'map': data}); 27 | } 28 | 29 | Map invertMap(Map input) { 30 | Map output = {}; 31 | input.forEach((key, value) { 32 | output[value] = key; 33 | }); 34 | return output; 35 | } 36 | 37 | DartType streamyImport(String clazz, {params: const []}) => 38 | new DartType(clazz, 'streamy', params); 39 | 40 | DartType toDartType(TypeRef ref, {bool withPrefix: true}) { 41 | if (ref is ListTypeRef) { 42 | return new DartType.list(toDartType(ref.subType, withPrefix: withPrefix)); 43 | } else if (ref is SchemaTypeRef) { 44 | final prefix = withPrefix ? objectPrefix : null; 45 | return new DartType(makeClassName(ref.schemaClass), prefix, const []); 46 | } else { 47 | switch (ref.base) { 48 | case 'int64': 49 | return new DartType('Int64', 'fixnum', const []); 50 | case 'integer': 51 | return const DartType.integer(); 52 | case 'string': 53 | return const DartType.string(); 54 | case 'any': 55 | return const DartType.dynamic(); 56 | case 'double': 57 | return const DartType.double(); 58 | case 'boolean': 59 | return const DartType.boolean(); 60 | case 'number': 61 | return const DartType.double(); 62 | case 'external': 63 | ExternalTypeRef externalTypeRef = ref; 64 | return new DartType(externalTypeRef.type, 65 | externalTypeRef.importedFrom, const []); 66 | case 'dependency': 67 | return new DartType(makeClassName(ref.type), ref.importedFrom, 68 | const []); 69 | default: 70 | throw new Exception('Unhandled API type: $ref'); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/generator/generator.dart: -------------------------------------------------------------------------------- 1 | library streamy.generator.generator; 2 | 3 | import 'package:mustache/mustache.dart' as mustache; 4 | import 'package:streamy/generator/config.dart'; 5 | import 'package:streamy/generator/dart.dart'; 6 | import 'package:streamy/generator/ir.dart'; 7 | 8 | /// Carries the state of a single run of the emitter. 9 | abstract class EmitterContext { 10 | Config get config; 11 | Map get templates; 12 | Api get api; 13 | String get libPrefix; 14 | 15 | DartLibrary get rootFile; 16 | DartFile get resourceFile; 17 | DartFile get requestFile; 18 | DartFile get objectFile; 19 | DartFile get dispatchFile; 20 | 21 | String get rootPrefix; 22 | String get resourcePrefix; 23 | String get requestPrefix; 24 | String get objectPrefix; 25 | String get dispatchPrefix; 26 | } 27 | 28 | /// Emits a specific implementation of a marshaller into a given 29 | /// [EmitterContext]. 30 | abstract class MarshallerEmitter { 31 | /// Emits a marshaller class. 32 | void emit(); 33 | 34 | /// Adds marshalling code to a [requestClass]. 35 | void decorateRequestClass(Method method, DartClass requestClass); 36 | } 37 | -------------------------------------------------------------------------------- /lib/generator/protobuf/service.dart: -------------------------------------------------------------------------------- 1 | part of streamy.generator.protobuf; 2 | 3 | Api parseServices(List paths) { 4 | var api = new Api('example', marshalling: false); 5 | var i = 1; 6 | paths.forEach((path) => _parseServiceFile(api, path, analyzer.parseDartFile(path), i++)); 7 | var pc = new PathConfig.prefixed('lib/', 'package:api/'); 8 | var hc = new HierarchyConfig.fixed(new DartType('Entity', 'base', const [])); 9 | var c = new Config(knownProperties: false); 10 | var emitter = new Emitter(SPLIT_LEVEL_NONE, pc, hc, c, 11 | new TemplateLoader.fromDirectory('templates')); 12 | var client = emitter.process(api); 13 | print(client.root.render()); 14 | } 15 | 16 | void _parseServiceFile(Api api, String importPath, analyzer.CompilationUnit cu, int index) { 17 | var classes = cu 18 | .declarations 19 | .where((d) => d is analyzer.ClassDeclaration); 20 | 21 | api.imports[importPath] = 'service_$index'; 22 | classes 23 | .where(_isSchemaClass) 24 | .map((clazz) => clazz.name.name) 25 | .forEach((name) { 26 | var type = new Schema(name); 27 | type.mixins.add(new TypeRef.external(name, 'service_$index')); 28 | api.types[name] = type; 29 | }); 30 | 31 | classes 32 | .where(_isServiceClass) 33 | .forEach((clazz) => _parseService(api, importPath, clazz)); 34 | } 35 | 36 | void _parseService(Api api, String importPath, analyzer.ClassDeclaration clazz) { 37 | var res = new Resource(clazz.name.name); 38 | api.resources[res.name] = res; 39 | clazz 40 | .members 41 | .where((d) => d is analyzer.MethodDeclaration) 42 | .where((m) => m.returnType != null) 43 | .where((m) => m.returnType.name.name == 'Future') 44 | .forEach((m) { 45 | var rt = m.returnType.typeArguments.arguments[0].name.name; 46 | var ref = new TypeRef.external(rt, importPath); 47 | var method = new Method(m.name.name, new Path('/'), '', null, ref); 48 | res.methods[method.name] = method; 49 | m 50 | .parameters 51 | .parameters 52 | .where((p) => p is analyzer.SimpleFormalParameter) 53 | .forEach((p) { 54 | var name = p.identifier.name; 55 | method.parameters[name] = new Field(name, '', const TypeRef.string(), ''); 56 | }); 57 | }); 58 | } 59 | 60 | bool _isSchemaClass(analyzer.ClassDeclaration clazz) => clazz 61 | .members 62 | .where((d) => d is analyzer.ConstructorDeclaration) 63 | .isEmpty; 64 | 65 | bool _isServiceClass(analyzer.ClassDeclaration clazz) { 66 | var methods = clazz 67 | .members 68 | .where((d) => d is analyzer.MethodDeclaration); 69 | return methods.isNotEmpty && methods 70 | .map((m) { 71 | if (m.returnType == null) { 72 | return ""; 73 | } 74 | return m.returnType.name.name; 75 | }) 76 | .every((type) => type == 'Future'); 77 | } 78 | 79 | -------------------------------------------------------------------------------- /lib/generator/template_loader.dart: -------------------------------------------------------------------------------- 1 | library streamy.generator.template_loader; 2 | 3 | import 'dart:async'; 4 | import 'dart:io' as io; 5 | import 'package:mustache/mustache.dart' as mustache; 6 | 7 | /// The location of templates bundled with Streamy. It assumes Streamy is run 8 | /// from the root of the project. This value is used by default if no specific 9 | /// value is provided. 10 | const String DEFAULT_TEMPLATE_DIR = 'lib/templates'; 11 | 12 | /// Reads template source from files named {templateName}.mustache. 13 | class DefaultTemplateLoader implements TemplateLoader { 14 | final String templateDir; 15 | 16 | DefaultTemplateLoader(this.templateDir); 17 | 18 | factory DefaultTemplateLoader.defaultInstance() { 19 | return new DefaultTemplateLoader(DEFAULT_TEMPLATE_DIR); 20 | } 21 | 22 | @override 23 | Future load(String templateName) { 24 | var templateFile = 25 | new io.File('${templateDir}/${templateName}.mustache'); 26 | return templateFile.readAsString() 27 | .then((source) => new mustache.Template(source, htmlEscapeValues: false)); 28 | } 29 | } 30 | 31 | abstract class TemplateLoader { 32 | 33 | factory TemplateLoader.fromDirectory(String path) { 34 | return new FileTemplateLoader(path); 35 | } 36 | 37 | Future load(String name); 38 | } 39 | 40 | class FileTemplateLoader implements TemplateLoader { 41 | final io.Directory path; 42 | 43 | FileTemplateLoader(String path) : path = new io.Directory(path).absolute; 44 | 45 | Future load(String name) { 46 | var f = new io.File("${path.path}/$name.mustache"); 47 | if (!f.existsSync()) { 48 | return null; 49 | } 50 | return f.readAsString() 51 | .then((source) => new mustache.Template(source, htmlEscapeValues: false)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/google/protobuf/compiler/plugin.pb.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | /// 4 | library google.protobuf.compiler; 5 | 6 | import 'package:fixnum/fixnum.dart'; 7 | import 'package:protobuf/protobuf.dart'; 8 | import '../descriptor.pb.dart' as google$protobuf; 9 | 10 | class CodeGeneratorRequest extends GeneratedMessage { 11 | static final BuilderInfo _i = new BuilderInfo('CodeGeneratorRequest') 12 | ..p(1, 'fileToGenerate', GeneratedMessage.PS) 13 | ..a(2, 'parameter', GeneratedMessage.OS) 14 | ..m(15, 'protoFile', () => new google$protobuf.FileDescriptorProto(), () => new PbList()) 15 | ; 16 | 17 | CodeGeneratorRequest() : super(); 18 | CodeGeneratorRequest.fromBuffer(List i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromBuffer(i, r); 19 | CodeGeneratorRequest.fromJson(String i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromJson(i, r); 20 | CodeGeneratorRequest clone() => new CodeGeneratorRequest()..mergeFromMessage(this); 21 | BuilderInfo get info_ => _i; 22 | 23 | List get fileToGenerate => getField(1); 24 | 25 | String get parameter => getField(2); 26 | void set parameter(String v) { setField(2, v); } 27 | bool hasParameter() => hasField(2); 28 | void clearParameter() => clearField(2); 29 | 30 | List get protoFile => getField(15); 31 | } 32 | 33 | class CodeGeneratorResponse_File extends GeneratedMessage { 34 | static final BuilderInfo _i = new BuilderInfo('CodeGeneratorResponse_File') 35 | ..a(1, 'name', GeneratedMessage.OS) 36 | ..a(2, 'insertionPoint', GeneratedMessage.OS) 37 | ..a(15, 'content', GeneratedMessage.OS) 38 | ..hasRequiredFields = false 39 | ; 40 | 41 | CodeGeneratorResponse_File() : super(); 42 | CodeGeneratorResponse_File.fromBuffer(List i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromBuffer(i, r); 43 | CodeGeneratorResponse_File.fromJson(String i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromJson(i, r); 44 | CodeGeneratorResponse_File clone() => new CodeGeneratorResponse_File()..mergeFromMessage(this); 45 | BuilderInfo get info_ => _i; 46 | 47 | String get name => getField(1); 48 | void set name(String v) { setField(1, v); } 49 | bool hasName() => hasField(1); 50 | void clearName() => clearField(1); 51 | 52 | String get insertionPoint => getField(2); 53 | void set insertionPoint(String v) { setField(2, v); } 54 | bool hasInsertionPoint() => hasField(2); 55 | void clearInsertionPoint() => clearField(2); 56 | 57 | String get content => getField(15); 58 | void set content(String v) { setField(15, v); } 59 | bool hasContent() => hasField(15); 60 | void clearContent() => clearField(15); 61 | } 62 | 63 | class CodeGeneratorResponse extends GeneratedMessage { 64 | static final BuilderInfo _i = new BuilderInfo('CodeGeneratorResponse') 65 | ..a(1, 'error', GeneratedMessage.OS) 66 | ..m(15, 'file', () => new CodeGeneratorResponse_File(), () => new PbList()) 67 | ..hasRequiredFields = false 68 | ; 69 | 70 | CodeGeneratorResponse() : super(); 71 | CodeGeneratorResponse.fromBuffer(List i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromBuffer(i, r); 72 | CodeGeneratorResponse.fromJson(String i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromJson(i, r); 73 | CodeGeneratorResponse clone() => new CodeGeneratorResponse()..mergeFromMessage(this); 74 | BuilderInfo get info_ => _i; 75 | 76 | String get error => getField(1); 77 | void set error(String v) { setField(1, v); } 78 | bool hasError() => hasField(1); 79 | void clearError() => clearField(1); 80 | 81 | List get file => getField(15); 82 | } 83 | 84 | -------------------------------------------------------------------------------- /lib/impl.dart: -------------------------------------------------------------------------------- 1 | // Provides implementation logic that's shared by both in-browser and 2 | // out-of-browser code. 3 | library streamy.impl; 4 | 5 | import 'dart:async'; 6 | import 'dart:convert' show JSON; 7 | import 'package:streamy/streamy.dart'; 8 | 9 | /// A rudimentary [RequestHandler] that serializes [Request] objects to JSON 10 | /// and sends them to the API servers. By default it sends requests to Google 11 | /// API servers, but you can override it by providing your own server address 12 | /// in the contstructor. 13 | class SimpleRequestHandler extends RequestHandler { 14 | final StreamyHttpService _http; 15 | final String _apiServerAddress; 16 | 17 | SimpleRequestHandler(this._http, 18 | {String apiServerAddress}) : 19 | this._apiServerAddress = apiServerAddress != null 20 | ? apiServerAddress 21 | : 'https://content.googleapis.com'; 22 | 23 | Stream handle(HttpRequest request, Trace trace) { 24 | var cancelCompleter = new Completer(); 25 | var ctrl = new StreamController( 26 | sync: true, 27 | onCancel: () { 28 | cancelCompleter.complete(); 29 | }); 30 | 31 | var url = '${_apiServerAddress}/${request.root.servicePath}${request.path}'; 32 | 33 | var req = new StreamyHttpRequest(url, request.httpMethod, { 34 | 'Content-Type': 'application/json' 35 | }, {}, 36 | cancelCompleter.future, 37 | payload: request.hasPayload ? JSON.encode(request.marshalPayload()) : null); 38 | _http.send(req).then((StreamyHttpResponse resp) { 39 | if (resp.statusCode >= 200 && resp.statusCode < 300) { 40 | var responseJson = jsonParse(resp.body, trace); 41 | trace.record(new DeserializationStartEvent(resp.body.length)); 42 | var responsePayload = request.unmarshalResponse(responseJson); 43 | trace.record(new DeserializationEndEvent()); 44 | ctrl.add(new Response(responsePayload, Source.RPC, 45 | new DateTime.now().millisecondsSinceEpoch)); 46 | } else { 47 | Map jsonError = null; 48 | try { 49 | jsonError = JSON.decode(resp.body); 50 | } catch(_) { 51 | // Apparently, body wan't JSON. The caller will have to make do. 52 | } 53 | ctrl.addError(new StreamyRpcException(resp.statusCode, request, 54 | jsonError)); 55 | } 56 | }, onError: (e) => ctrl.addError(new StreamyRpcException(0, request, 57 | {'error': {'errors': [{'message': e.message}]}}))); 58 | 59 | return ctrl.stream; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/impl_html.dart: -------------------------------------------------------------------------------- 1 | // Implementations based on in-browser [HttpRequest]. Classes defined here can 2 | // only be used in applications that run inside a web-browser. They don't work 3 | // outside the web-browser. For out-of-browser implementations, check out 4 | // impl_server.dart. 5 | library streamy.html_impl; 6 | 7 | import 'dart:html'; 8 | import 'dart:async'; 9 | 10 | import 'package:streamy/streamy.dart' hide HttpRequest; 11 | import 'impl.dart'; 12 | 13 | /** 14 | * A plain HTTP service that sends HTTP requests via [HttpRequest]. 15 | */ 16 | class HtmlHttpService implements StreamyHttpService { 17 | 18 | const HtmlHttpService(); 19 | 20 | Future send(StreamyHttpRequest request) { 21 | var req = new HttpRequest(); 22 | 23 | req.open(request.method, request.url, async: true); 24 | 25 | request.headers.forEach((k, v) { 26 | req.setRequestHeader(k, v); 27 | }); 28 | 29 | if (request.withCredentials) { 30 | req.withCredentials = true; 31 | } 32 | 33 | if (request.payload != null) { 34 | req.send(request.payload); 35 | } else { 36 | req.send(); 37 | } 38 | 39 | request.onCancel.then((_) { 40 | req.abort(); 41 | }); 42 | 43 | var c = new Completer(); 44 | req.onLoad.first.then((_) { 45 | var bodyType = null; 46 | var responseType = req.getResponseHeader('Content-Type'); 47 | if (responseType != null) { 48 | bodyType = responseType.split(';')[0]; 49 | } 50 | c.complete(new StreamyHttpResponse(req.status, req.responseHeaders, 51 | req.responseText)); 52 | }); 53 | req.onError.first.then((_) => c.completeError(new StreamyHttpError( 54 | // The passed ProgressEvent doesn't contain any useful information. 55 | 'An error occured communicating with the server'))); 56 | return c.future; 57 | } 58 | } 59 | 60 | /// A [SimpleRequestHandler] specialized for use in applications that run 61 | /// inside a web-browser. 62 | class HtmlRequestHandler extends SimpleRequestHandler { 63 | HtmlRequestHandler([String apiServerAddress]) : 64 | super(const HtmlHttpService(), apiServerAddress: apiServerAddress); 65 | } 66 | -------------------------------------------------------------------------------- /lib/impl_server.dart: -------------------------------------------------------------------------------- 1 | // Implementations for out-of-browser applications, such as command-line apps 2 | // and servers. For in-browser applications use impl_html.dart. 3 | library streamy.server_impl; 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | import 'dart:io'; 8 | 9 | import 'package:streamy/streamy.dart'; 10 | import 'impl.dart'; 11 | 12 | /** 13 | * A plain HTTP service that sends HTTP requests via [HttpRequest]. 14 | */ 15 | class ServerHttpService implements StreamyHttpService { 16 | 17 | final _httpClient = new HttpClient(); 18 | 19 | ServerHttpService(); 20 | 21 | // TODO(yjbanov): Not sure how to cancel HTTP requests 22 | Future send(StreamyHttpRequest request) => _httpClient 23 | .openUrl(request.method, Uri.parse(request.url)) 24 | .then((HttpClientRequest req) { 25 | if (request.payload != null) { 26 | request.headers.forEach((k, v) { 27 | req.headers.add(k, v); 28 | }); 29 | req.write(request.payload); 30 | } 31 | return req.close(); 32 | }) 33 | .then((HttpClientResponse resp) { 34 | var responseType = resp.headers[HttpHeaders.CONTENT_TYPE]; 35 | var responseHeaders = {}; 36 | resp.headers.forEach((String name, List values) { 37 | responseHeaders[name] = values[0]; 38 | }); 39 | 40 | return resp.transform(const Utf8Decoder()) 41 | .fold(new StringBuffer(), (buf, e) => buf..write(e)) 42 | .then((StringBuffer responseBody) => 43 | new StreamyHttpResponse(resp.statusCode, responseHeaders, 44 | responseBody.toString())); 45 | }); 46 | } 47 | 48 | /// A [SimpleRequestHandler] specialized for use in server-side or command-line 49 | /// applications. 50 | class ServerRequestHandler extends SimpleRequestHandler { 51 | ServerRequestHandler([String apiServerAddress]) : 52 | super(new ServerHttpService(), apiServerAddress: apiServerAddress); 53 | } 54 | -------------------------------------------------------------------------------- /lib/indexed_cache.dart: -------------------------------------------------------------------------------- 1 | library streamy.indexed_cache; 2 | 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | import 'dart:html'; 6 | import 'dart:indexed_db' as idb; 7 | import 'package:streamy/streamy.dart'; 8 | 9 | /// A [Cache] that persists data in IndexedDB. 10 | /// TODO(arick): Some kind of web test for this. 11 | class IndexedDbCache extends Cache { 12 | 13 | final idb.Database db; 14 | final Duration maxAge; 15 | var _inGc = false; 16 | final int gcPerCycleLimit; 17 | Timer _gcTimer; 18 | 19 | static Future open({ 20 | Duration gcCycle: const Duration(minutes: 5), 21 | Duration maxAge: const Duration(days: 7), 22 | int gcPerCycleLimit: 500 23 | }) => openNamed("streamy", gcCycle: gcCycle, maxAge: maxAge, gcPerCycleLimit: gcPerCycleLimit); 24 | 25 | static Future openNamed(name, { 26 | Duration gcCycle: const Duration(minutes: 5), 27 | Duration maxAge: const Duration(days: 7), 28 | int gcPerCycleLimit: 500 29 | }) { 30 | return window.indexedDB.open(name, version: 1, onUpgradeNeeded: _initDb).then((database) { 31 | return new IndexedDbCache._private(database, gcCycle, maxAge, gcPerCycleLimit); 32 | }); 33 | } 34 | 35 | IndexedDbCache._private(this.db, gcCycle, this.maxAge, this.gcPerCycleLimit) { 36 | if (gcCycle != null) { 37 | _gcTimer = new Timer.periodic(gcCycle, (_) => gc()); 38 | } 39 | } 40 | 41 | static void _initDb(idb.VersionChangeEvent e) { 42 | idb.Database db = (e.target as idb.Request).result; 43 | 44 | var store = db.createObjectStore("entityCache", keyPath: "request"); 45 | store.createIndex("ts", "ts"); 46 | } 47 | 48 | /// Get an entity from the cache. 49 | Future get(Request key) { 50 | var jsonKey = key.signature; 51 | var txn = db.transaction("entityCache", "readonly"); 52 | var store = txn.objectStore("entityCache"); 53 | return store.getObject(jsonKey).then((result) { 54 | if (result != null && result["entity"] != null) { 55 | return key.responseDeserializer(result["entity"]) 56 | ..streamy.ts = result["ts"]; 57 | } 58 | return null; 59 | }); 60 | } 61 | 62 | /// Set an entity in the cache. 63 | Future set(Request key, CachedEntity entity) { 64 | var cacheEntry = { 65 | "request": key.signature, 66 | "ts": entity.ts, 67 | "entity": JSON.encode(entity.entity) 68 | }; 69 | var txn = db.transaction("entityCache", "readwrite"); 70 | var store = txn.objectStore("entityCache"); 71 | return store.put(cacheEntry); 72 | } 73 | 74 | /// Invalidate an entity in the cache. 75 | Future invalidate(Request key) { 76 | var txn = db.transaction("entityCache", "readwrite"); 77 | var store = txn.objectStore("entityCache"); 78 | return store.delete(key.signature); 79 | } 80 | 81 | /// Run garbage collection to remove all entries older than the stated date. 82 | void gc() { 83 | if (_inGc) { 84 | return; 85 | } 86 | _inGc = true; 87 | var txn = db.transaction("entityCache", "readwrite"); 88 | var store = txn.objectStore("entityCache"); 89 | var max = new DateTime.now().millisecondsSinceEpoch - maxAge.inMilliseconds; 90 | List futures = []; 91 | store.index("ts").openCursor(range: idb.KeyRange.upperBound_(max), autoAdvance: true) 92 | .take(gcPerCycleLimit) 93 | .listen((cursor) { 94 | futures.add(cursor.delete()); 95 | }); 96 | Future.wait(futures).whenComplete(() { 97 | _inGc = false; 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/mixins/base_map.dart: -------------------------------------------------------------------------------- 1 | library streamy.trait.base.map; 2 | 3 | import 'package:streamy/streamy.dart' as streamy; 4 | 5 | class MapBase implements streamy.DynamicAccess { 6 | var _map; 7 | 8 | Iterable get keys => _map.keys; 9 | bool containsKey(Object key) => _map.containsKey(key); 10 | operator[](Object key) => _map[key]; 11 | operator[]=(String key, value) { 12 | _map[key] = value; 13 | } 14 | remove(Object key) => _map.remove(key); 15 | } 16 | 17 | Map getMap(MapBase entity) => entity._map; 18 | void setMap(MapBase entity, map) { 19 | entity._map = map; 20 | } 21 | -------------------------------------------------------------------------------- /lib/mixins/base_podo.dart: -------------------------------------------------------------------------------- 1 | library streamy.trait.base.podo; 2 | 3 | import 'dart:mirrors'; 4 | import 'package:streamy/streamy.dart' as streamy; 5 | 6 | class PodoBase implements streamy.Entity { 7 | var _instanceMirrorImpl; 8 | 9 | InstanceMirror get _mirror { 10 | if (_instanceMirrorImpl == null) { 11 | _instanceMirrorImpl = reflect(this); 12 | } 13 | return _instanceMirrorImpl; 14 | } 15 | Map get _members => _mirror.type.instanceMembers; 16 | 17 | 18 | containsKey(String key) { 19 | if (key.startsWith('_')) { 20 | return false; 21 | } 22 | s = new Symbol(key); 23 | if (_members.containsKey(s) && _members[s].isGetter) { 24 | return true; 25 | } 26 | } 27 | 28 | operator[](String key) => _mirror.getField(new Symbol(key)).reflectee; 29 | operator[]=(String key, value) { 30 | _mirror.setField(new Symbol(key), value); 31 | } 32 | 33 | remove(String key) { 34 | var value = this[key]; 35 | this[key] = null; 36 | return value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/mixins/copy_clone.dart: -------------------------------------------------------------------------------- 1 | library streamy.traits.clone.copy; 2 | 3 | import 'package:streamy/streamy.dart' as streamy; 4 | 5 | class CopyClone implements streamy.Cloneable { 6 | 7 | dynamic clone(); 8 | 9 | copyInto(streamy.DynamicAccess other) { 10 | for (var key in keys) { 11 | other[key] = _cloneHelper(super[key]); 12 | } 13 | return other; 14 | } 15 | 16 | _cloneHelper(value) { 17 | if (value is streamy.Cloneable) { 18 | return value.clone(); 19 | } else if (value is List) { 20 | return value.map(_cloneHelper).toList(); 21 | } else { 22 | return value; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/mixins/dot_access.dart: -------------------------------------------------------------------------------- 1 | library streamy.trait.dot_access; 2 | 3 | class DotAccess { 4 | 5 | _resolve(List pieces, start) { 6 | var cur = start; 7 | for (var i = 0; i < pieces.length; i++) { 8 | cur = cur[pieces[i]]; 9 | if (cur == null) { 10 | return null; 11 | } 12 | } 13 | return cur; 14 | } 15 | 16 | bool containsKey(Object key) { 17 | if (key is! String) return false; 18 | if (!key.contains('.')) { 19 | return super.containsKey(key); 20 | } 21 | var pieces = key.split('.'); 22 | var last = pieces.removeLast(); 23 | var target = _resolve(pieces, this); 24 | if (target == null) { 25 | return false; 26 | } 27 | return target.containsKey(last); 28 | } 29 | 30 | operator[](Object key) { 31 | if (!key.contains('.')) { 32 | return super[key]; 33 | } 34 | var pieces = key.split('.'); 35 | var last = pieces.removeLast(); 36 | var target = _resolve(pieces, this); 37 | if (target == null) { 38 | return null; 39 | } 40 | return target[last]; 41 | } 42 | 43 | operator[]=(String key, value) { 44 | if (!key.contains('.')) { 45 | super[key] = value; 46 | return; 47 | } 48 | var pieces = key.split('.'); 49 | var last = pieces.removeLast(); 50 | var target = _resolve(pieces, this); 51 | target[last] = value; 52 | } 53 | 54 | remove(Object key) { 55 | if (key is! String) return; 56 | if (!key.contains('.')) { 57 | return super.remove(key); 58 | } 59 | var pieces = key.split('.'); 60 | var last = pieces.removeLast(); 61 | var target = _resolve(pieces, this); 62 | return target.remove(last); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/mixins/global.dart: -------------------------------------------------------------------------------- 1 | library streamy.trait.global; 2 | 3 | import 'package:streamy/streamy.dart' as streamy; 4 | 5 | class Global implements streamy.HasGlobal { 6 | 7 | var _global; 8 | 9 | operator[](Object key) => key == 'global' ? global : super[key]; 10 | 11 | streamy.GlobalView get global { 12 | if (_global == null) { 13 | _global = new streamy.GlobalView(this); 14 | } 15 | return _global; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/mixins/has_api_type.dart: -------------------------------------------------------------------------------- 1 | library streamy.trait.base.map; 2 | 3 | class HasApiType { 4 | String get apiType => 'unknown'; 5 | } 6 | -------------------------------------------------------------------------------- /lib/mixins/immutable.dart: -------------------------------------------------------------------------------- 1 | library streamy.mixins.immutable; 2 | 3 | import 'dart:collection' as col; 4 | import 'package:fixnum/fixnum.dart' as fixnum; 5 | import 'package:observe/observe.dart' as observe; 6 | import 'package:streamy/streamy.dart' as streamy; 7 | 8 | class Immutability implements streamy.Freezeable { 9 | 10 | bool _isFrozen = false; 11 | 12 | operator[]=(String key, value) { 13 | if (_isFrozen) { 14 | throw new UnsupportedError("Frozen."); 15 | } 16 | super[key] = value; 17 | } 18 | 19 | remove(Object key) { 20 | if (_isFrozen) { 21 | throw new UnsupportedError("Frozen."); 22 | } 23 | return super.remove(key); 24 | } 25 | 26 | void freeze() { 27 | _freezeHelper(this); 28 | } 29 | 30 | bool get isFrozen => _isFrozen; 31 | } 32 | 33 | dynamic _freezeHelper(dynamic object) { 34 | if (object is streamy.Freezeable && object._isFrozen) { 35 | // Already frozen, noop 36 | return object; 37 | } 38 | 39 | if (object is streamy.DynamicAccess || object is Map) { 40 | for (String key in object.keys) { 41 | object[key] = _freezeValue(object[key]); 42 | } 43 | } else if (object is List) { 44 | final frozenElements = object is observe.ObservableList 45 | ? new observe.ObservableList(object.length) 46 | : new List(object.length); 47 | for (int i = 0; i < object.length; i++) { 48 | frozenElements[i] = _freezeValue(object[i]); 49 | } 50 | if (object is observe.ObservableList) { 51 | object = new streamy.ObservableImmutableListView(frozenElements); 52 | } else { 53 | object = new col.UnmodifiableListView(frozenElements); 54 | } 55 | } 56 | 57 | if (object is streamy.Freezeable) { 58 | object._isFrozen = true; 59 | } 60 | 61 | return object; 62 | } 63 | 64 | /** 65 | * Chooses between [_freezeHelper] and [freeze] to freeze a value. 66 | * 67 | * Freezeables need to use [freeze] because it can be overridden. 68 | */ 69 | dynamic _freezeValue(dynamic object) { 70 | if (object is streamy.Freezeable) { 71 | object.freeze(); 72 | return object; 73 | } else { 74 | return _freezeHelper(object); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/mixins/is_map.dart: -------------------------------------------------------------------------------- 1 | library streamy.trait.base.map; 2 | 3 | import 'package:streamy/streamy.dart' as streamy; 4 | 5 | class IsMap implements Map { 6 | 7 | // Methods provided by DynamicAccess 8 | 9 | Iterable get keys => super.keys; 10 | bool containsKey(Object key) => super.containsKey(key); 11 | operator[](Object key) => super[key]; 12 | operator[]=(String key, value) { 13 | super[key] = value; 14 | } 15 | remove(Object key) => super.remove(key); 16 | 17 | // Map methods implemented via DynamicAccess 18 | 19 | bool containsValue(Object value) { 20 | for (String key in keys) { 21 | if (this[key] == value) { 22 | return true; 23 | } 24 | } 25 | return false; 26 | } 27 | 28 | putIfAbsent(String key, ifAbsent()) { 29 | if (!containsKey(key)) { 30 | var newVal = ifAbsent(); 31 | this[key] = newVal; 32 | return newVal; 33 | } 34 | return this[key]; 35 | } 36 | 37 | void addAll(Map other) { 38 | other.forEach((k, v) { 39 | this[k] = v; 40 | }); 41 | } 42 | 43 | void clear() { 44 | for (String key in new List.from(keys)) { 45 | this.remove(key); 46 | } 47 | } 48 | 49 | void forEach(void f(String key, dynamic value)) { 50 | for (String key in keys) { 51 | f(key, this[key]); 52 | } 53 | } 54 | 55 | Iterable get values => keys.map((String key) => this[key]); 56 | 57 | int get length => keys.length; 58 | 59 | bool get isEmpty => keys.isEmpty; 60 | 61 | bool get isNotEmpty => keys.isNotEmpty; 62 | } 63 | -------------------------------------------------------------------------------- /lib/mixins/lazy.dart: -------------------------------------------------------------------------------- 1 | library streamy.trait.lazy; 2 | 3 | import 'package:streamy/streamy.dart' as streamy; 4 | 5 | class Lazy { 6 | 7 | operator[](String key) { 8 | var value = super[key]; 9 | if (value is streamy.Lazy) { 10 | value = value.resolve(); 11 | super[key] = value; 12 | } 13 | return value; 14 | } 15 | 16 | remove(String key) { 17 | var value = super.remove(key); 18 | if (value is streamy.Lazy) { 19 | value = value.resolve(); 20 | } 21 | return value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/mixins/local.dart: -------------------------------------------------------------------------------- 1 | library streamy.trait.local; 2 | 3 | import 'package:observe/observe.dart' as observe; 4 | 5 | class Local { 6 | observe.ObservableMap _local; 7 | 8 | operator[](Object key) => key == 'local' ? local : super[key]; 9 | operator[]=(String key, value) { 10 | if (key == 'local') { 11 | throw new ArgumentError('"local" field is reserved'); 12 | } 13 | super[key] = value; 14 | } 15 | 16 | observe.ObservableMap get local { 17 | if (_local == null) { 18 | _local = new observe.ObservableMap(); 19 | } 20 | return _local; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/mixins/nosuchmethod.dart: -------------------------------------------------------------------------------- 1 | library streamy.trait.nosuchmethod; 2 | 3 | import 'dart:mirrors'; 4 | 5 | class NoSuchMethod { 6 | 7 | noSuchMethod(Invocation inv) { 8 | if (!inv.isAccessor) { 9 | return super.noSuchMethod(inv); 10 | } 11 | if (inv.isGetter) { 12 | return this[MirrorSystem.getName(inv.memberName)]; 13 | } else if (inv.isSetter) { 14 | this[MirrorSystem.getName(inv.memberName)] = inv.positionalArguments[0]; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/mixins/observable.dart: -------------------------------------------------------------------------------- 1 | library streamy.trait.observable; 2 | 3 | import 'dart:async'; 4 | import 'package:observe/observe.dart' as observe; 5 | 6 | /// Adds observability to an entity. 7 | class Observability implements observe.Observable { 8 | 9 | var _pendingChanges; 10 | var _changesImpl; 11 | 12 | StreamController> get _changes { 13 | if (_changesImpl == null) { 14 | _changesImpl = new StreamController>.broadcast(sync: true); 15 | } 16 | return _changesImpl; 17 | } 18 | 19 | bool deliverChanges() { 20 | if (_pendingChanges == null || _changesImpl == null || _pendingChanges.isEmpty) { 21 | return false; 22 | } 23 | var copy = _pendingChanges.toList(growable: false); 24 | _pendingChanges.clear(); 25 | _changes.add(copy); 26 | } 27 | 28 | notifyChange(observe.ChangeRecord record) { 29 | if (_pendingChanges == null) { 30 | _pendingChanges = []; 31 | } 32 | if (_pendingChanges.isEmpty) { 33 | scheduleMicrotask(deliverChanges); 34 | } 35 | _pendingChanges.add(record); 36 | } 37 | 38 | notifyPropertyChange(Symbol field, Object oldValue, Object newValue) => newValue; 39 | 40 | Stream> get changes => _changes.stream; 41 | 42 | bool get hasObservers => _changesImpl != null && _changes.hasListener; 43 | 44 | operator[]=(String key, value) { 45 | if (hasObservers) { 46 | if (containsKey(key)) { 47 | notifyChange(new observe.MapChangeRecord(key, super[key], value)); 48 | } else { 49 | notifyChange(new observe.MapChangeRecord.insert(key, value)); 50 | } 51 | } 52 | if (value != null && value is List && value is! observe.ObservableList) { 53 | value = new observe.ObservableList.from(value); 54 | } 55 | super[key] = value; 56 | } 57 | 58 | remove(Object key) { 59 | if (hasObservers) { 60 | notifyChange(new observe.MapChangeRecord.remove(key, super[key])); 61 | } 62 | return super.remove(key); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/mixins/patch_map.dart: -------------------------------------------------------------------------------- 1 | library streamy.mixins.patch; 2 | 3 | import 'package:observe/observe.dart' as observe; 4 | import 'package:streamy/streamy.dart' as streamy; 5 | 6 | class Patch implements streamy.Patchable { 7 | 8 | Map _original = null; 9 | 10 | dynamic patch(); 11 | 12 | patchInto(streamy.DynamicAccess other) { 13 | if (_original == null) { 14 | _original = {}; 15 | } 16 | keys.forEach((key) { 17 | var vOld = _original[key]; 18 | var vNew = this[key]; 19 | if (!_patchCheckEqual(vOld, vNew)) { 20 | other[key] = vOld == null ? vNew : _patchHelper(vNew); 21 | } 22 | }); 23 | return other; 24 | } 25 | 26 | void _setOriginal() { 27 | _original = getMap(this); 28 | if (this is! streamy.Freezeable || !isFrozen) { 29 | // Need to clone original if not frozen. Hope it's cloneable. 30 | _original = _original.clone(); 31 | } 32 | } 33 | 34 | copyInto(streamy.DynamicAccess other) => 35 | super.copyInto(other) 36 | .._original = _original; 37 | 38 | void freeze() { 39 | if (this is streamy.Freezeable) { 40 | super.freeze(); 41 | } 42 | _setOriginal(); 43 | } 44 | 45 | 46 | _patchHelper(v) { 47 | if (v == null) { 48 | return null; 49 | } else if (v is streamy.Patchable) { 50 | return v.patch(); 51 | } else if (v is Map) { 52 | var c = new observe.ObservableMap(); 53 | v.forEach((k, v) { 54 | c[k] = _patchHelper(v); 55 | }); 56 | return c; 57 | } else if (v is List) { 58 | // PATCH semantics dictate that arrays are replaced and not merged. Hence, 59 | // the array contents need to be clones, not patches. 60 | return new observe.ObservableList.from(v.map((value) => _cloneHelper(value))); 61 | } else { 62 | return v; 63 | } 64 | } 65 | 66 | bool _patchCheckEqual(a, b) { 67 | if (identical(a, b)) { 68 | return true; 69 | } 70 | if (a == null) { 71 | return b == null; 72 | } else if (a is List) { 73 | if (b is! List || b.length != a.length) { 74 | return false; 75 | } 76 | for (var i = 0; i < a.length; i++) { 77 | if (!_patchCheckEqual(a[i], b[i])) { 78 | return false; 79 | } 80 | } 81 | return true; 82 | } else if (a is streamy.DynamicAccess) { 83 | return (b is streamy.DynamicAccess) && streamy.EntityUtils.deepEquals(a, b); 84 | } 85 | return a == b; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/mixologist.dart: -------------------------------------------------------------------------------- 1 | library streamy.mixologist; 2 | 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | import 'dart:io'; 6 | import 'package:quiver/async.dart'; 7 | 8 | import 'src/fs/fs.dart'; 9 | 10 | part 'mixologist/in.dart'; 11 | part 'mixologist/import.dart'; 12 | part 'mixologist/out.dart'; 13 | part 'mixologist/config.dart'; 14 | part 'mixologist/generator.dart'; 15 | -------------------------------------------------------------------------------- /lib/mixologist/config.dart: -------------------------------------------------------------------------------- 1 | part of streamy.mixologist; 2 | 3 | class Config { 4 | String output; 5 | String libraryName; 6 | String className; 7 | 8 | List paths = []; 9 | List mixins = []; 10 | } 11 | 12 | Config parseConfig(Map data) { 13 | var missing = ['output', 'library', 'class', 'paths', 'mixins'] 14 | .where((key) => !data.containsKey(key)); 15 | if (missing.isNotEmpty) { 16 | throw new Exception('Mixologist YAML configuration missing keys: $missing'); 17 | } 18 | 19 | return new Config() 20 | ..output = data['output'] 21 | ..libraryName = data['library'] 22 | ..className = data['class'] 23 | ..paths.addAll(data['paths']) 24 | ..mixins.addAll(data['mixins']); 25 | } 26 | -------------------------------------------------------------------------------- /lib/mixologist/generator.dart: -------------------------------------------------------------------------------- 1 | part of streamy.mixologist; 2 | 3 | /// Generated mixed in code according to the provided [config]. Uses 4 | /// [fs] to access the file system. 5 | Future mix(Config config, FileSystem fs) { 6 | Map mixins = {}; 7 | // Walk paths and locate mixins 8 | return forEachAsync(config.paths, (String path) => 9 | forEachAsync( 10 | config.mixins.where((name) => !mixins.containsKey(name)), 11 | (String name) { 12 | var mixinPath = '${path}/${name}.dart'; 13 | return fs.exists(mixinPath).then((bool exists) { 14 | if (exists) { 15 | return fs.read(mixinPath).pipe(new MixinReader()) 16 | .then((Mixin mixin) { 17 | mixins[name] = mixin; 18 | }); 19 | } 20 | }); 21 | } 22 | ) 23 | ).then((_) { 24 | // Validate that every mixin needed has been loaded. 25 | var missing = config.mixins 26 | .where((mixin) => !mixins.containsKey(mixin)); 27 | if (missing.isNotEmpty) { 28 | throw new Exception('Could not find mixins: ${missing.join(", ")}'); 29 | } 30 | 31 | List mixinList = 32 | config.mixins.map((mixin) => mixins[mixin]).toList(); 33 | 34 | var codeLines = [ 35 | '// Generated by the Streamy Mixologist.', 36 | '// Mixins: ${config.mixins.join(",")}' 37 | '', 38 | 'library ${config.libraryName};', '' 39 | ] 40 | ..addAll(writeImports(unifyImports(mixinList))) 41 | ..add('') 42 | ..addAll(new LinearizedTarget( 43 | config.className, '', 'Object', mixinList).linearize()) 44 | ..add(''); 45 | return codeLines.join('\n'); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /lib/mixologist/import.dart: -------------------------------------------------------------------------------- 1 | part of streamy.mixologist; 2 | 3 | Map unifyImports(List mixins) { 4 | var out = {}; 5 | var pathToMixin = {}; 6 | mixins.forEach((mixin) { 7 | mixin.imports.forEach((path, alias) { 8 | if (out.containsKey(path) && out[path] != alias) { 9 | throw new ImportException( 10 | path, alias, mixin.name, out[path], pathToMixin[path]); 11 | } 12 | out[path] = alias; 13 | pathToMixin[path] = mixin.className; 14 | }); 15 | }); 16 | return out; 17 | } 18 | 19 | List writeImports(Map imports) { 20 | var out = []; 21 | imports.forEach((path, alias) { 22 | if (alias != null) { 23 | out.add("import '$path' as $alias;"); 24 | } else { 25 | out.add("import '$path';"); 26 | } 27 | }); 28 | return out; 29 | } 30 | 31 | class ImportException implements Exception { 32 | final String path; 33 | final String attemptedAlias; 34 | final String attemptedMixin; 35 | final String importAlias; 36 | final String importMixin; 37 | 38 | ImportException(this.path, this.attemptedAlias, this.attemptedMixin, 39 | this.importAlias, this.importMixin); 40 | 41 | String toString() => "Attempted import of '$path' as '$attemptedAlias' by " + 42 | "$attemptedMixin, but already imported as '$importAlias' by $importMixin"; 43 | } -------------------------------------------------------------------------------- /lib/mixologist/in.dart: -------------------------------------------------------------------------------- 1 | part of streamy.mixologist; 2 | 3 | class Mixin { 4 | Map imports = {}; 5 | String className; 6 | String baseClass; 7 | List interfaces = []; 8 | List classCodeLines = []; 9 | } 10 | 11 | const _STATE_IMPORTS = 1; 12 | const _STATE_CLASS = 2; 13 | 14 | var rImport = new RegExp('import\\s+[\'"](.*)[\'"](\\s+as\\s+(.*))?\\s*;'); 15 | 16 | class MixinReader implements StreamConsumer> { 17 | 18 | var _future; 19 | Future addStream(Stream> stream) { 20 | var state = _STATE_IMPORTS; 21 | _future = stream 22 | .transform(new Utf8Decoder()) 23 | .transform(new LineSplitter()) 24 | .fold(new Mixin(), (mixin, line) { 25 | var tline = line.trim(); 26 | if (tline.startsWith('import ')) { 27 | var m = rImport.matchAsPrefix(line); 28 | if (m != null) { 29 | mixin.imports[m[1]] = m[3]; 30 | } 31 | } else if (tline.startsWith('class')) { 32 | state = _STATE_CLASS; 33 | tline = tline.substring(5).trim(); 34 | var i = _indexOf(tline, [' ', '\t', '{']); 35 | mixin.className = tline.substring(0, i); 36 | tline = tline.substring(i).trim(); 37 | if (tline.startsWith('extends')) { 38 | tline = tline.substring(7).trim(); 39 | mixin.baseClass = _parseType(tline); 40 | tline = tline.substring(mixin.baseClass.length).trim(); 41 | } 42 | if (tline.startsWith('implements')) { 43 | tline = tline.substring(10).trim(); 44 | var iface = []; 45 | var readIfaces = true; 46 | while (readIfaces) { 47 | var type = _parseType(tline); 48 | mixin.interfaces.add(type); 49 | tline = tline.substring(type.length).trim(); 50 | if (tline.substring(0, 1) == ",") { 51 | tline = tline.substring(1).trim(); 52 | } else { 53 | readIfaces = false; 54 | } 55 | } 56 | } 57 | } else if (state == _STATE_CLASS) { 58 | mixin.classCodeLines.add(line); 59 | } 60 | return mixin; 61 | }); 62 | return new Future.value(); 63 | } 64 | 65 | Future close() => _future; 66 | } 67 | 68 | int _indexOf(String s, List ch) { 69 | var i = -1; 70 | for (var c in ch) { 71 | var i2 = s.indexOf(c); 72 | if (i2 != -1 && (i == -1 || i2 < i)) { 73 | i = i2; 74 | } 75 | } 76 | return i; 77 | } 78 | 79 | String _parseType(String l) { 80 | var i = _indexOf(l, ["<", ">", ",", " "]); 81 | var b = l.substring(0, i); 82 | var r = l.substring(i); 83 | if (r.substring(0, 1) == "<") { 84 | b += "<"; 85 | r = r.substring(1); 86 | while (r.substring(0, 1) == " ") { 87 | b += " "; 88 | r = r.substring(1); 89 | } 90 | var i = 0; 91 | while (i++ < 10 && r.substring(0, 1) != ">") { 92 | var t = _parseType(r); 93 | b += t; 94 | r = r.substring(t.length); 95 | if (r.substring(0, 1) == ",") { 96 | b += ","; 97 | r = r.substring(1); 98 | while (r.substring(0, 1) == " ") { 99 | b += " "; 100 | r = r.substring(1); 101 | } 102 | } 103 | } 104 | b += ">"; 105 | } 106 | return b; 107 | } 108 | -------------------------------------------------------------------------------- /lib/mixologist/out.dart: -------------------------------------------------------------------------------- 1 | part of streamy.mixologist; 2 | 3 | class LinearizedTarget { 4 | final String finalClassName; 5 | final String intermediatePrefix; 6 | final String baseClassName; 7 | 8 | final List mixins; 9 | 10 | LinearizedTarget(this.finalClassName, this.intermediatePrefix, this.baseClassName, this.mixins); 11 | 12 | List linearize() { 13 | var baseClass = baseClassName != null ? baseClassName : "Object"; 14 | var out = []; 15 | 16 | // Output each mixin. 17 | mixins.forEach((mixin) { 18 | var className = "$intermediatePrefix${mixin.className}"; 19 | var classDef = "abstract class $className extends $baseClass"; 20 | if (mixin.interfaces.isNotEmpty) { 21 | classDef += ' implements ' + mixin.interfaces.join(', '); 22 | } 23 | classDef += ' {'; 24 | out.add(classDef); 25 | out.addAll(mixin.classCodeLines); 26 | out.add(''); 27 | baseClass = className; 28 | }); 29 | 30 | // Output the final class. 31 | out.add("class $finalClassName extends $baseClass {}"); 32 | return out; 33 | } 34 | } -------------------------------------------------------------------------------- /lib/raw.api.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamy.raw_entity", 3 | "schemas": { 4 | "RawEntity": { 5 | "id": "RawEntity", 6 | "type": "object" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/raw.streamy.yaml: -------------------------------------------------------------------------------- 1 | # Generates an entity that has no predefined properties, but only 2 | # map-based access to data. 3 | discovery: raw.api.json 4 | output: 5 | files: single 6 | prefix: raw_entity 7 | generateApi: false 8 | generateMarshallers: false 9 | base: 10 | class: Entity 11 | import: package:streamy/base.dart 12 | backing: map 13 | options: 14 | clone: true 15 | patch: true 16 | removers: false 17 | known: false 18 | global: false 19 | -------------------------------------------------------------------------------- /lib/runtime/api.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | abstract class DynamicAccess { 4 | Iterable get keys; 5 | bool containsKey(String key); 6 | operator[](String key); 7 | void operator[]=(String key, value); 8 | remove(String key); 9 | } 10 | 11 | abstract class Cloneable { 12 | dynamic clone(); 13 | } 14 | 15 | abstract class Patchable { 16 | dynamic patch(); 17 | } 18 | 19 | abstract class Freezeable { 20 | bool get isFrozen; 21 | void freeze(); 22 | } 23 | 24 | abstract class HasGlobal { 25 | Type get streamyType; 26 | GlobalView get global; 27 | } 28 | -------------------------------------------------------------------------------- /lib/runtime/batching.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | /** 4 | * Accepts HTTP requests and decides how to batch them and when to send 5 | * batches. 6 | */ 7 | abstract class BatchingStrategy { 8 | /// Accepts a HTTP request 9 | void add(StreamyHttpRequest request); 10 | 11 | /** 12 | * Produces requests and batches of requests to be sent to the server. 13 | * 14 | * To send a single standalone HTTP request the stream must produce a value 15 | * of type [StreamyHttpRequest]. 16 | * 17 | * To send a batch of HTTP requests, the stream must produce a value of type 18 | * [Batch]. 19 | */ 20 | Stream get batches; 21 | } 22 | 23 | /** 24 | * Represents a batch of HTTP requests to be sent as a single multipart/mixed 25 | * payload. 26 | */ 27 | abstract class Batch { 28 | final List requests; 29 | final Future onCancel; 30 | 31 | Batch(this.requests, this.onCancel); 32 | 33 | /** 34 | * Is called by [BatchingHttpService] to notify the [BatchingStrategy] that 35 | * a batch request has completed. 36 | */ 37 | void done(StreamyHttpResponse batchResponse); 38 | } 39 | 40 | /** 41 | * Batches requests and sends them to a delegate HTTP service as 42 | * multipart/mixed POST requests. 43 | */ 44 | class BatchingHttpService implements StreamyHttpService { 45 | 46 | static final _responseCompleter = 47 | new Expando>( 48 | 'BatchingHttpService.responseCompleter'); 49 | 50 | final String _batchUrl; 51 | final String _method; 52 | final Map _headers; 53 | final BatchingStrategy _batchingStrategy; 54 | final StreamyHttpService _delegate; 55 | final Random _random; 56 | 57 | BatchingHttpService(this._batchUrl, this._method, this._headers, 58 | this._batchingStrategy, this._delegate, {Function onBatchStrategyError, 59 | Random random}) : 60 | _random = random { 61 | _batchingStrategy.batches.listen(_internalSend, 62 | onError: onBatchStrategyError); 63 | } 64 | 65 | Future send(StreamyHttpRequest request) { 66 | var completer = new Completer(); 67 | _responseCompleter[request] = completer; 68 | _batchingStrategy.add(request); 69 | return completer.future; 70 | } 71 | 72 | void _internalSend(dynamic whatToSend) { 73 | if (whatToSend is! StreamyHttpRequest && 74 | whatToSend is! Batch) { 75 | throw new ArgumentError('Unsupported type ${whatToSend.runtimeType}'); 76 | } 77 | if (whatToSend is StreamyHttpRequest) { 78 | _sendSingleRequest(whatToSend); 79 | } 80 | if (whatToSend is Batch) { 81 | _sendBatch(whatToSend); 82 | } 83 | } 84 | 85 | void _sendSingleRequest(StreamyHttpRequest request) { 86 | var completer = _responseCompleter[request]; 87 | _delegate.send(request) 88 | .then(completer.complete, onError: completer.completeError); 89 | } 90 | 91 | void _sendBatch(Batch batch) { 92 | var multipartReq = new StreamyHttpRequest.multipart(_batchUrl, _method, 93 | _headers, batch.onCancel, batch.requests, random: _random); 94 | _delegate.send(multipartReq) 95 | .then((StreamyHttpResponse multipartResp) { 96 | var parts = multipartResp.splitMultipart(); 97 | for (List pair in zip([batch.requests, parts])) { 98 | StreamyHttpRequest req = pair[0]; 99 | StreamyHttpResponse resp = pair[1]; 100 | _responseCompleter[req].complete(resp); 101 | } 102 | batch.done(multipartResp); 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/runtime/equality.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | class EntityUtils { 4 | 5 | static int deepHashCode(DynamicAccess entity) { 6 | // Running total, kept under MAX_HASHCODE. 7 | var running = 0; 8 | var fieldNames = new List.from(entity.keys)..sort(); 9 | var len = fieldNames.length; 10 | for (var i = 0; i < len; i++) { 11 | running = ((17 * running) + fieldNames[i].hashCode) % MAX_HASHCODE; 12 | var value = entity[fieldNames[i]]; 13 | if (value is DynamicAccess) { 14 | running = ((17 * running) + deepHashCode(value)) % MAX_HASHCODE; 15 | } else if (value is List) { 16 | for (var listValue in value) { 17 | if (listValue is DynamicAccess) { 18 | running = ((17 * running) + deepHashCode(listValue)) % MAX_HASHCODE; 19 | } else { 20 | running = ((17 * running) + listValue.hashCode) % MAX_HASHCODE; 21 | } 22 | } 23 | } else { 24 | running = ((17 * running) + value.hashCode) % MAX_HASHCODE; 25 | } 26 | } 27 | return running; 28 | } 29 | 30 | static bool deepEquals(DynamicAccess first, DynamicAccess second) { 31 | if (identical(first, second)) { 32 | return true; 33 | } 34 | if (first == null || second == null) { 35 | return (first == second); 36 | } 37 | 38 | // Loop through each field, checking equality of the values. 39 | var fieldNames = first.keys.toList(growable: false); 40 | var len = fieldNames.length; 41 | if (len != second.keys.length) { 42 | return false; 43 | } 44 | for (var i = 0; i < len; i++) { 45 | if (!second.containsKey(fieldNames[i])) { 46 | return false; 47 | } 48 | var firstValue = first[fieldNames[i]]; 49 | var secondValue = second[fieldNames[i]]; 50 | if (firstValue is DynamicAccess && secondValue is DynamicAccess) { 51 | if (!deepEquals(firstValue, secondValue)) { 52 | return false; 53 | } 54 | } else if (firstValue is List && secondValue is List) { 55 | if (firstValue.length != secondValue.length) { 56 | return false; 57 | } 58 | for (var j = 0; j < firstValue.length; j++) { 59 | if (firstValue[j] is DynamicAccess && secondValue[j] is DynamicAccess) { 60 | if (!deepEquals(firstValue[j], secondValue[j])) { 61 | return false; 62 | } 63 | } else if (firstValue[j] != secondValue[j]) { 64 | return false; 65 | } 66 | } 67 | } else if (firstValue != secondValue) { 68 | return false; 69 | } 70 | } 71 | return true; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/runtime/json.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | final _observableJsonCodec = new JsonCodec.withReviver(_observableReviver); 4 | 5 | /// Parses JSON into [Observable] lists and maps. 6 | dynamic jsonParse(String json, [Trace trace = const NoopTrace()]) { 7 | trace.record(new JsonParseStartEvent()); 8 | var result = _observableJsonCodec.decode(json); 9 | trace.record(new JsonParseEndEvent()); 10 | return result; 11 | } 12 | 13 | _observableReviver(dynamic key, dynamic value) { 14 | if (value is List) { 15 | return new ObservableList.from(value); 16 | } else { 17 | return value; 18 | } 19 | } 20 | 21 | /// Marshals a given [object] to a value that can be fed to a [JsonEncoder]. 22 | /// Turns entities into plain maps where keys are entity property names and 23 | /// values are recursively marshalled via this method. Encodes [Int64] and 24 | /// `double` as `String` to preserve precision. Leaves other types intact. 25 | // TODO(yjbanov): consider using JsonEncoder with toEncodable instead 26 | jsonMarshal(dynamic object) { 27 | if (object is List) { 28 | var len = object.length; 29 | var list = new List(len); 30 | for (int i = 0; i < len; i++) { 31 | list[i] = jsonMarshal(object[i]); 32 | } 33 | return list; 34 | } else if (object is Map) { 35 | final ret = {}; 36 | object.forEach((k, v) { 37 | ret[k] = jsonMarshal(v); 38 | }); 39 | return ret; 40 | } else if (object is Int64) { 41 | return object.toString(); 42 | } else if (object == null || object is num || object is bool || 43 | object is String) { 44 | return object; 45 | } else { 46 | throw new ArgumentError('Unable to marshal type ${object.runtimeType}'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/runtime/lazy.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | typedef T LazyDeserializerFn(rawValue); 4 | 5 | /// A [Lazy] represents a value of type T with a high cost of deserialization. 6 | class Lazy { 7 | 8 | var rawValue; 9 | LazyDeserializerFn deserializer; 10 | 11 | /// Construct a lazily deserialized value from a raw value and a closure 12 | /// to deserialize it. 13 | Lazy(this.rawValue, this.deserializer); 14 | 15 | T resolve() => deserializer(rawValue); 16 | 17 | static toLazy(LazyDeserializerFn deserializer) => 18 | ((v) => new Lazy(v, deserializer)); 19 | } 20 | 21 | // An [ObservableList] that understands [Lazy] values and silently resolves them. 22 | class LazyList extends ListBase implements ObservableList { 23 | 24 | // Not ObservableList since this can contain Lazy elements which aren't of 25 | // type E. 26 | ObservableList delegate; 27 | bool _changesDisabled = false; 28 | StreamController> _changes; 29 | 30 | LazyList(this.delegate); 31 | 32 | int get length => delegate.length; 33 | void set length(int newLength) => delegate.length = newLength; 34 | 35 | E operator[](int index) { 36 | var value = delegate[index]; 37 | if (value is Lazy) { 38 | value = value.resolve(); 39 | 40 | // Fire any batched changes before disabling them to update the value. 41 | deliverChanges(); 42 | _changesDisabled = true; 43 | 44 | // Replace the resolved value in the list. 45 | delegate[index] = value; 46 | 47 | // Make sure the replacement change has been discarded before enabling 48 | // changes again. 49 | deliverChanges(); 50 | _changesDisabled = false; 51 | } 52 | 53 | return value; 54 | } 55 | 56 | void operator[]=(int index, E value) { 57 | delegate[index] = value; 58 | } 59 | 60 | // Manually delegating [add] and [addAll] as per [ListBase] performance 61 | // recommendations. 62 | void add(E value) => delegate.add(value); 63 | void addAll(Iterable iterable) => delegate.addAll(iterable); 64 | 65 | // Observable interface. 66 | bool deliverChanges() => delegate.deliverChanges(); 67 | bool deliverListChanges() => delegate.deliverListChanges(); 68 | void discardListChages() => delegate.discardListChages(); 69 | void discardListChanges() => delegate.discardListChages(); 70 | 71 | void notifyChange(ChangeRecord record) => delegate.notifyChange(record); 72 | notifyPropertyChange(Symbol field, Object oldValue, Object newValue) => 73 | delegate.notifyPropertyChange(oldValue, newValue); 74 | void observed() => delegate.observed(); 75 | void unobserved() => delegate.unobserved(); 76 | 77 | Stream> get changes { 78 | if (_changes == null) { 79 | var sub; 80 | _changes = new StreamController>.broadcast(onListen: () { 81 | sub = delegate.changes.listen((changeList) { 82 | if (!_changesDisabled) { 83 | _changes.add(changeList); 84 | } 85 | }); 86 | }, onCancel: () => sub.cancel()); 87 | } 88 | return _changes.stream; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/runtime/marshal.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | void marshalToString(List fields, Map data) { 4 | fields 5 | .where(data.containsKey) 6 | .forEach((key) { 7 | data[key] = marshalDataToString(data[key]); 8 | }); 9 | } 10 | 11 | marshalDataToString(data) { 12 | if (data == null) { 13 | return null; 14 | } else if (data is List) { 15 | return new ObservableList.from(data.map(marshalDataToString)); 16 | } else { 17 | return data.toString(); 18 | } 19 | } 20 | 21 | void unmarshalInt64s(List fields, Map data, {bool lazy: false}) { 22 | fields 23 | .where(data.containsKey) 24 | .forEach((key) { 25 | data[key] = unmarshalInt64Data(data[key], lazy); 26 | }); 27 | } 28 | 29 | unmarshalInt64Data(data, bool lazy) { 30 | if (data == null) { 31 | return null; 32 | } else if (data is List) { 33 | if (lazy) { 34 | return new LazyList(new ObservableList.from(data.map(Lazy.toLazy(unmarshalInt64DataLazy)))); 35 | } else { 36 | return new ObservableList.from(data.map(unmarshalInt64DataNonLazy)); 37 | } 38 | } else if (data is String) { 39 | return Int64.parseInt(data); 40 | } else { 41 | return new Int64(data); 42 | } 43 | } 44 | 45 | unmarshalInt64DataLazy(data) => unmarshalDoubleData(data, true); 46 | unmarshalInt64DataNonLazy(data) => unmarshalDoubleData(data, false); 47 | 48 | void unmarshalDoubles(List fields, Map data, {bool lazy: false}) { 49 | fields 50 | .where(data.containsKey) 51 | .forEach((key) { 52 | data[key] = unmarshalDoubleData(data[key], lazy); 53 | }); 54 | } 55 | 56 | unmarshalDoubleData(data, bool lazy) { 57 | if (data == null) { 58 | return null; 59 | } else if (data is List) { 60 | if (lazy) { 61 | return new LazyList(new ObservableList.from(data.map(Lazy.toLazy(unmarshalDoubleDataLazy)))); 62 | } else { 63 | return new ObservableList.from(data.map(unmarshalDoubleDataNonLazy)); 64 | } 65 | } else if (data is String) { 66 | return double.parse(data); 67 | } else { 68 | return data; 69 | } 70 | } 71 | 72 | unmarshalDoubleDataLazy(data) => unmarshalDoubleData(data, true); 73 | unmarshalDoubleDataNonLazy(data) => unmarshalDoubleData(data, false); 74 | 75 | void handleEntities(Map handlers, Map data, bool marshal, {bool lazy: false}) { 76 | handlers 77 | .keys 78 | .where(data.containsKey) 79 | .forEach((key) { 80 | data[key] = handleEntityData(data[key], handlers[key], marshal, lazy); 81 | }); 82 | } 83 | 84 | handleEntityData(data, handler, bool marshal, bool lazy) { 85 | if (data == null) { 86 | return null; 87 | } else if (data is List) { 88 | var unwrapper = (v) => handleEntityData(v, handler, marshal, lazy); 89 | if (!lazy) { 90 | return new ObservableList.from(data.map(unwrapper)); 91 | } else { 92 | return new LazyList(new ObservableList.from(data.map(Lazy.toLazy(unwrapper)))); 93 | } 94 | } else { 95 | return handler(data, marshal, lazy: lazy); 96 | } 97 | } 98 | 99 | void unmarshalEntities(Map marshalledProperties, Map data, {bool lazy: false}) { 100 | marshalledProperties.keys.where(data.containsKey).forEach((key) { 101 | data[key] = unmarshalEntityData(marshalledProperties[key], data[key], lazy: lazy); 102 | }); 103 | } 104 | 105 | unmarshalEntityData(unmarshaller(dynamic, {bool lazy}), data, {bool lazy: false}) { 106 | if (data == null) { 107 | return null; 108 | } else if (data is List) { 109 | var unwrapper = (v) => unmarshalEntityData(unmarshaller, v, lazy: lazy); 110 | if (!lazy) { 111 | return new ObservableList.from(data.map(unwrapper)); 112 | } else { 113 | return new LazyList(new ObservableList.from(data.map(Lazy.toLazy(unwrapper)))); 114 | } 115 | } else { 116 | return unmarshaller(data, lazy: lazy); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/runtime/multiplexer.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | class ActiveRequest { 4 | final StreamController sink; 5 | final trace; 6 | bool seenPrimary = false; 7 | 8 | ActiveRequest({onCancel}) 9 | : sink = new StreamController(onCancel: onCancel); 10 | 11 | Stream get stream => sink.stream; 12 | 13 | void addPrimary(Response response) { 14 | sink.add(response); 15 | if (response.authority == Authority.PRIMARY) { 16 | seenPrimary = true; 17 | } 18 | } 19 | 20 | /// Secondary responses come from other requests. Should one be of primary 21 | /// authority, it is degraded to secondary if this request has not gotten its 22 | /// primary response yet. 23 | void addSecondary(Response response) { 24 | var authority = response.authority; 25 | if (!seenPrimary && authority == Authority.PRIMARY) { 26 | authority = Authority.SECONDARY; 27 | } 28 | sink.add(new Response(response.entity, response.source, response.ts, 29 | authority: authority)); 30 | } 31 | } 32 | 33 | /// Holds all cachable [Stream]s open after the delegated request completes. 34 | /// When a future [Request] is made which matches an open request, the 35 | /// [Response] is sent on that stream as well. Thus, clients that hold [Stream]s 36 | /// open after receiving the initial [Response] can be informed of future 37 | /// updates or changes. 38 | /// 39 | /// It is possible that [Response]s from other [Request]s can arrive prior to 40 | /// the primary [Response] for the original [Request]. If this happens, any 41 | /// responses with PRIMARY authority delivered to other active [Stream]s will be 42 | /// downgraded to SECONDARY authority, until the primary response is received. 43 | class MultiplexingRequestHandler extends RequestHandler { 44 | 45 | final RequestHandler delegate; 46 | final map = new SetMultimap(); 47 | 48 | MultiplexingRequestHandler(this.delegate); 49 | 50 | Stream handle(HttpRequest request, Trace trace) { 51 | if (!request.isCachable) { 52 | return delegate.handle(request, trace); 53 | } 54 | var key = request.cacheKey(); 55 | var sub; 56 | var active; 57 | 58 | active = new ActiveRequest(onCancel: () { 59 | if (sub != null) { 60 | sub.cancel(); 61 | } 62 | map.remove(key, active); 63 | }); 64 | sub = delegate.handle(request, trace).listen((resp) { 65 | active.addPrimary(resp); 66 | map[key] 67 | .where((a) => a != active) 68 | .forEach((a) => a.addSecondary(resp)); 69 | })..onError(active.sink.addError)..onDone(() { 70 | sub = null; 71 | }); 72 | 73 | map.add(key, active); 74 | return active.stream; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/runtime/proxy.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | const _CONTENT_TYPE = 'content-type'; 4 | 5 | /// A [RequestHandler] that proxies through a frontend server. 6 | class ProxyClient extends RequestHandler { 7 | 8 | /// The base url of the proxy. 9 | final String proxyUrl; 10 | final StreamyHttpService httpHandler; 11 | 12 | ProxyClient(this.proxyUrl, this.httpHandler); 13 | 14 | Stream handle(Request originalReq, Trace trace) { 15 | if (originalReq is! HttpRequest) { 16 | throw new ProxyClientException('ProxyClient only works with HttpRequests'); 17 | } 18 | HttpRequest req = originalReq; 19 | HttpRoot root = req.root; 20 | var url = '$proxyUrl/${root.servicePath}${req.path}'; 21 | var payload = null; 22 | var headers = {}; 23 | if (req.hasPayload) { 24 | payload = JSON.encode(req.marshalPayload()); 25 | headers[_CONTENT_TYPE] = 'application/json; charset=utf-8'; 26 | } 27 | var cancelCompleter = new Completer(); 28 | var httpReq = new StreamyHttpRequest(url, req.httpMethod, headers, 29 | req.local, cancelCompleter.future, payload: payload); 30 | var waitForHttpResponse = httpHandler.send(httpReq); 31 | 32 | var c; 33 | c = new StreamController(onCancel: () { 34 | // Only cancel requests if they haven't already completed. 35 | if (!c.isClosed) { 36 | cancelCompleter.complete(null); 37 | } 38 | }); 39 | 40 | waitForHttpResponse.then((StreamyHttpResponse resp) { 41 | if (resp.statusCode < 200 || resp.statusCode >= 300) { 42 | Map jsonError = null; 43 | // If the bodyType is not available, optimistically try parsing it as 44 | // JSON. 45 | if (!resp.headers.containsKey(_CONTENT_TYPE) || 46 | resp.headers[_CONTENT_TYPE].startsWith('application/json')) { 47 | try { 48 | jsonError = JSON.decode(resp.body); 49 | } catch(_) { 50 | // Apparently, body wan't JSON. The caller will have to make do. 51 | } 52 | } 53 | throw new StreamyRpcException(resp.statusCode, req, jsonError); 54 | } 55 | Freezeable responsePayload = null; 56 | if (resp.statusCode == 200 || resp.statusCode == 201) { 57 | var responseJson = jsonParse(resp.body, trace); 58 | trace.record(new DeserializationStartEvent(resp.body.length)); 59 | responsePayload = req.unmarshalResponse(responseJson); 60 | responsePayload.freeze(); 61 | trace.record(new DeserializationEndEvent()); 62 | } 63 | return new Response(responsePayload, Source.RPC, 64 | new DateTime.now().millisecondsSinceEpoch); 65 | }).then((value) { 66 | c.add(value); 67 | c.close(); 68 | }).catchError((error) { 69 | c.addError(error); 70 | c.close(); 71 | }); 72 | return c.stream; 73 | } 74 | } 75 | 76 | class ProxyRequestSent implements TraceEvent { 77 | factory ProxyRequestSent() => const ProxyRequestSent._private(); 78 | 79 | const ProxyRequestSent._private(); 80 | 81 | String toString() => 'streamy.proxy.sent'; 82 | } 83 | 84 | class ProxyClientException extends StreamyException { 85 | final String _msg; 86 | ProxyClientException(this._msg); 87 | String toString() => 'ProxyClientException: $_msg'; 88 | } 89 | -------------------------------------------------------------------------------- /lib/runtime/response.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | class Response { 4 | final T entity; 5 | final String source; 6 | final String authority; 7 | final int ts; 8 | 9 | const Response(this.entity, this.source, this.ts, {this.authority: Authority.PRIMARY}); 10 | 11 | toString() => 'streamy.Response(from $source with $authority at $ts: ${entity.runtimeType})'; 12 | } 13 | 14 | abstract class Source { 15 | static const RPC = 'RPC'; 16 | static const CACHE = 'CACHE'; 17 | static const ERROR = 'ERROR'; 18 | } 19 | 20 | abstract class Authority { 21 | static const PRIMARY = 'PRIMARY'; 22 | static const SECONDARY = 'SECONDARY'; 23 | } -------------------------------------------------------------------------------- /lib/runtime/root.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | /// The root object representing an entire API, which makes its resources 4 | /// available. 5 | abstract class Root { 6 | 7 | // Type name as defined in the API. 8 | String get apiType => 'Root'; 9 | 10 | /// Execute a [Request] and return a [Stream] of the results. 11 | Stream send(Request req); 12 | } 13 | 14 | // A [Root] with an Http path. 15 | abstract class HttpRoot implements Root { 16 | final String servicePath; 17 | 18 | HttpRoot(this.servicePath); 19 | 20 | dynamic get marshaller; 21 | } 22 | 23 | /// Substitute for a [Root] object that executes requests as part of the same 24 | /// transaction. The implementation of a transactional strategy is provided by 25 | /// a [TransactionStrategy] object. 26 | class TransactionRoot extends Root { 27 | 28 | final Transaction _tx; 29 | 30 | TransactionRoot(Transaction this._tx) : super(); 31 | 32 | Stream send(Request request) => _tx.send(request); 33 | Future commit() => _tx.commit(); 34 | } 35 | -------------------------------------------------------------------------------- /lib/runtime/tracing.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | typedef bool StreamTraceOverIndicator(TraceEvent event); 4 | 5 | /// An event that occurs during the processing of a particular request. Can be a const singleton 6 | /// instance or a subclass which contains more data about the event. 7 | abstract class TraceEvent { 8 | String toString(); 9 | } 10 | 11 | /// Indicates the start of JSON parsing. 12 | class JsonParseStartEvent extends TraceEvent { 13 | String toString() => "streamy.json.start"; 14 | } 15 | 16 | /// Indicates the end of JSON parsing. 17 | class JsonParseEndEvent extends TraceEvent { 18 | String toString() => "streamy.json.end"; 19 | } 20 | 21 | /// Indicates the start of deserialization. 22 | class DeserializationStartEvent extends TraceEvent { 23 | int size; 24 | 25 | DeserializationStartEvent(this.size); 26 | String toString() => "streamy.deserialization.start($size)"; 27 | } 28 | 29 | /// Indicates the end of deserialization. 30 | class DeserializationEndEvent extends TraceEvent { 31 | String toString() => "streamy.deserialization.end"; 32 | } 33 | 34 | /// A trace for a particular request. Essentially a sink for [TraceEvent]s. 35 | abstract class Trace { 36 | void record(TraceEvent event); 37 | } 38 | 39 | /// A tracing strategy that creates [Trace]s for [Request]s. Supplied by the user during the 40 | /// construction of [Root]s. 41 | abstract class Tracer { 42 | Trace trace(Request request); 43 | } 44 | 45 | class NoopTrace implements Trace { 46 | const NoopTrace(); 47 | 48 | void record(TraceEvent _) {} 49 | } 50 | 51 | /// Logs all traces into an in-memory list. 52 | class LoggingTrace implements Trace { 53 | final List log = []; 54 | 55 | void record(TraceEvent evt) { 56 | log.add(evt); 57 | } 58 | } 59 | 60 | /// A [Tracer] that drops [TraceEvent]s on the floor. 61 | class NoopTracer implements Tracer { 62 | const NoopTracer(); 63 | 64 | Trace trace(Request request) => const NoopTrace(); 65 | } 66 | 67 | /// A [Request] that's being traced, along with a [Stream] of events. 68 | class TracedRequest { 69 | final Request request; 70 | final Stream events; 71 | 72 | TracedRequest(this.request, this.events); 73 | } 74 | 75 | class _StreamTrace implements Trace { 76 | var _controller = new StreamController.broadcast(sync: true); 77 | StreamTraceOverIndicator traceOverPredicate; 78 | 79 | _StreamTrace(this.traceOverPredicate); 80 | 81 | void record(TraceEvent event) { 82 | if (_controller.isClosed) { 83 | return; 84 | } 85 | _controller.add(event); 86 | if (traceOverPredicate(event)) { 87 | _controller.close(); 88 | } 89 | } 90 | 91 | Stream get events => _controller.stream; 92 | } 93 | 94 | /// A [Tracer] which reports [TracedRequest]s on a [Stream], allowing subscription to their 95 | /// [TraceEvent]s. 96 | class StreamTracer implements Tracer { 97 | var _controller = new StreamController.broadcast(sync: true); 98 | StreamTraceOverIndicator traceOverPredicate; 99 | 100 | StreamTracer(this.traceOverPredicate); 101 | 102 | Trace trace(Request request) { 103 | var trace = new _StreamTrace(traceOverPredicate); 104 | _controller.add(new TracedRequest(request, trace.events)); 105 | return trace; 106 | } 107 | 108 | Stream get requests => _controller.stream; 109 | } 110 | -------------------------------------------------------------------------------- /lib/runtime/wire.dart: -------------------------------------------------------------------------------- 1 | part of streamy.runtime; 2 | 3 | abstract class Serializer { 4 | dynamic serialize(value); 5 | } 6 | 7 | class JsonSerializer implements Serializer { 8 | serialize(value) { 9 | if (value == null) { 10 | return null; 11 | } else if (value is DynamicAccess) { 12 | var map = {}; 13 | for (var key in value.keys) { 14 | map[key] = serialize(value[key]); 15 | } 16 | return map; 17 | } else if (value is Int64) { 18 | return value.toString(); 19 | } else if (value is List) { 20 | return value.map(serialize).toList(); 21 | } else { 22 | return value; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/fs/fs.dart: -------------------------------------------------------------------------------- 1 | library streamy.internal.filesystem; 2 | 3 | import 'dart:async'; 4 | 5 | /// A file-system abstraction that allows us to access 6 | /// local and transform assets in a unified way. 7 | abstract class FileSystem { 8 | Future exists(String path); 9 | Stream> read(String path); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/fs/local_fs.dart: -------------------------------------------------------------------------------- 1 | library streamy.internal.local_filesystem; 2 | 3 | import 'dart:async'; 4 | import 'dart:io' as io; 5 | import 'package:path/path.dart' as paths; 6 | import 'fs.dart'; 7 | 8 | class LocalFileSystem implements FileSystem { 9 | io.Directory _rootDir; 10 | 11 | LocalFileSystem(this._rootDir); 12 | 13 | Future exists(String path) => 14 | _toFullPath(path).exists(); 15 | 16 | Stream> read(String path) => 17 | _toFullPath(path).openRead(); 18 | 19 | io.File _toFullPath(String path) => 20 | new io.File(paths.join(_rootDir.path, path)); 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/fs/transform_fs.dart: -------------------------------------------------------------------------------- 1 | library streamy.internal.transform_filesystem; 2 | 3 | import 'dart:async'; 4 | import 'package:barback/barback.dart'; 5 | import 'fs.dart'; 6 | 7 | /// Adapts transformer asset space as a file system. Use it 8 | /// when your paths point to Dart assets. 9 | class TransformFileSystem implements FileSystem { 10 | 11 | final Transform _transform; 12 | 13 | TransformFileSystem(this._transform); 14 | 15 | Future exists(String path) => 16 | _transform.hasInput(_toAssetId(path)); 17 | 18 | Stream> read(String path) => 19 | _transform.readInput(_toAssetId(path)); 20 | 21 | AssetId _toAssetId(String path) { 22 | return new AssetId(_transform.primaryInput.id.package, 23 | '${prefixFrom(_transform.primaryInput.id)}$path'); 24 | } 25 | } 26 | 27 | /// Similar to UNIX dirname 28 | String prefixFrom(AssetId asset) => 29 | (asset.path.split('/')..removeLast()..add('')).join('/'); 30 | -------------------------------------------------------------------------------- /lib/streamy.dart: -------------------------------------------------------------------------------- 1 | /// Runtime library used by all generated APIs. 2 | library streamy.runtime; 3 | 4 | import 'dart:async'; 5 | import 'dart:collection'; 6 | import 'dart:convert'; 7 | import 'dart:math'; 8 | 9 | import 'package:fixnum/fixnum.dart'; 10 | import 'package:observe/observe.dart'; 11 | import 'package:quiver/collection.dart'; 12 | import 'package:quiver/iterables.dart'; 13 | import 'package:quiver/time.dart'; 14 | import 'package:smoke/smoke.dart' as smoke; 15 | 16 | part 'runtime/api.dart'; 17 | part 'runtime/batching.dart'; 18 | part 'runtime/cache.dart'; 19 | part 'runtime/dedup.dart'; 20 | part 'runtime/entity_util.dart'; 21 | part 'runtime/error.dart'; 22 | part 'runtime/equality.dart'; 23 | part 'runtime/global.dart'; 24 | part 'runtime/http.dart'; 25 | part 'runtime/json.dart'; 26 | part 'runtime/lazy.dart'; 27 | part 'runtime/marshal.dart'; 28 | part 'runtime/multiplexer.dart'; 29 | part 'runtime/proxy.dart'; 30 | part 'runtime/request.dart'; 31 | part 'runtime/response.dart'; 32 | part 'runtime/root.dart'; 33 | part 'runtime/tracing.dart'; 34 | part 'runtime/transforms.dart'; 35 | part 'runtime/util.dart'; 36 | -------------------------------------------------------------------------------- /lib/templates/lazy_resource_getter.mustache: -------------------------------------------------------------------------------- 1 | { 2 | if ({{field}} == null) { 3 | {{field}} = new {{resource}}({{root}}); 4 | } 5 | return {{field}}; 6 | } -------------------------------------------------------------------------------- /lib/templates/list.mustache: -------------------------------------------------------------------------------- 1 | {{#const}}const {{/const}}{{#getter}}=> {{/getter}}[ 2 | {{#values}} {{value}}{{^last}},{{/last}} 3 | {{/values}}]{{#getter}};{{/getter}} -------------------------------------------------------------------------------- /lib/templates/map.mustache: -------------------------------------------------------------------------------- 1 | {{#const}}const {{/const}}{{#getter}}=> {{/getter}}{ 2 | {{#pairs}} {{#string}}r'{{/string}}{{key}}{{#string}}'{{/string}}: {{value}}, 3 | {{/pairs}}}{{#getter}};{{/getter}} -------------------------------------------------------------------------------- /lib/templates/marshal.mustache: -------------------------------------------------------------------------------- 1 | { {{! TODO(Alex): #fromFields behavior when #hasFieldMapping is broken. }} 2 | var res = new Map(){{#fromFields}}{{#fields}} 3 | ..[r'{{key}}'] = entity.{{identifier}}{{/fields}}{{/fromFields}}{{^fromFields}} 4 | ..addAll({{basePrefix}}.getMap(entity)){{/fromFields}};{{#hasInt64s}} 5 | streamy.marshalToString(_int64s{{name}}, res);{{/hasInt64s}}{{#hasDoubles}} 6 | streamy.marshalToString(_doubles{{name}}, res);{{/hasDoubles}}{{#hasEntities}} 7 | streamy.handleEntities(_entities{{name}}, res, true);{{/hasEntities}}{{^fromFields}}{{#hasFieldMapping}} 8 | res 9 | .keys 10 | .toList() 11 | .forEach((key) { 12 | if (_fieldUnmapping{{name}}.containsKey(key)) { 13 | res[_fieldUnmapping{{name}}[key]] = res.remove(key); 14 | } 15 | });{{/hasFieldMapping}}{{/fromFields}} 16 | return res; 17 | } -------------------------------------------------------------------------------- /lib/templates/marshal_handle.mustache: -------------------------------------------------------------------------------- 1 | => marshal ? marshal{{type}}(data) : unmarshal{{type}}(data, lazy: lazy); -------------------------------------------------------------------------------- /lib/templates/marshal_mapbacked.mustache: -------------------------------------------------------------------------------- 1 | { 2 | var map = new Map()..addAll({{mapReader}}(entity); 3 | return map{{#fields}} 4 | ..[r'{{name}}'] = {{prefix}}map[r'{{name}}']{{suffix}}{{/fields}}; -------------------------------------------------------------------------------- /lib/templates/object_add_global.mustache: -------------------------------------------------------------------------------- 1 | => streamy.GlobalView.register({{type}}, name, 2 | new streamy.GlobalRegistration(computeFn, dependencies, memoize)); -------------------------------------------------------------------------------- /lib/templates/object_clone.mustache: -------------------------------------------------------------------------------- 1 | => copyInto(new {{type}}()); -------------------------------------------------------------------------------- /lib/templates/object_ctor.mustache: -------------------------------------------------------------------------------- 1 | {{#mapBacked}}{ 2 | {{basePrefix}}.setMap(this, {{#wrap}}map{{/wrap}}{{^wrap}}{}{{/wrap}}); 3 | }{{/mapBacked}}{{^mapBacked}};{{/mapBacked}} -------------------------------------------------------------------------------- /lib/templates/object_getter.mustache: -------------------------------------------------------------------------------- 1 | => this[r'{{name}}']; -------------------------------------------------------------------------------- /lib/templates/object_patch.mustache: -------------------------------------------------------------------------------- 1 | => patchInto(new {{type}}()); -------------------------------------------------------------------------------- /lib/templates/object_remove.mustache: -------------------------------------------------------------------------------- 1 | => this.remove(r'{{name}}'); -------------------------------------------------------------------------------- /lib/templates/object_setter.mustache: -------------------------------------------------------------------------------- 1 | { 2 | this[r'{{name}}'] = value; 3 | } -------------------------------------------------------------------------------- /lib/templates/proto_marshaller_ctor.mustache: -------------------------------------------------------------------------------- 1 | : this.withMarshallers({{#marshallers}}const {{import}}.Marshaller(){{^last}}, {{/last}}{{/marshallers}}); -------------------------------------------------------------------------------- /lib/templates/pubspec.mustache: -------------------------------------------------------------------------------- 1 | name: {{package_name}} 2 | version: {{version}} 3 | description: > 4 | API client library for {{api_name}} for use with Streamy RPC framework. 5 | authors: 6 | - Streamy 7 | homepage: {{homepage}} 8 | environment: 9 | sdk: '>=1.6.0' 10 | dependencies: 11 | browser: any 12 | args: ">=0.12.0 <0.13.0" 13 | fixnum: ">=0.9.0 <0.10.0" 14 | observe: ">=0.12.0 <0.13.0" 15 | streamy: {{streamy_version}} 16 | quiver: ">=0.18.0 <0.20.0" 17 | -------------------------------------------------------------------------------- /lib/templates/request_clone.mustache: -------------------------------------------------------------------------------- 1 | => streamy.internalCloneFrom(new {{type}}(root{{#hasPayload}}, payload.clone(){{/hasPayload}}), this); -------------------------------------------------------------------------------- /lib/templates/request_ctor.mustache: -------------------------------------------------------------------------------- 1 | : super{{superConstructor}}( 2 | root, 3 | '{{httpMethod}}', 4 | r'{{pathFormat}}', 5 | API_TYPE, 6 | const[ 7 | {{#pathParameters}}r'{{name}}',{{/pathParameters}} 8 | ], 9 | const[ 10 | {{#queryParameters}}r'{{name}}',{{/queryParameters}} 11 | ] 12 | {{#hasPayload}}, payload{{/hasPayload}}){{#hasListParams}} { 13 | parameters{{#listParams}} 14 | ..[r'{{name}}'] = <{{type}}>[]{{/listParams}}; 15 | }{{/hasListParams}}{{^hasListParams}};{{/hasListParams}} 16 | -------------------------------------------------------------------------------- /lib/templates/request_marshal_payload.mustache: -------------------------------------------------------------------------------- 1 | => root.marshaller.marshal{{name}}(payload); -------------------------------------------------------------------------------- /lib/templates/request_method.mustache: -------------------------------------------------------------------------------- 1 | => new {{requestType}}(_root{{#hasPayload}}, payload{{/hasPayload}}){{#parameters}} 2 | ..parameters[r'{{name}}'] = {{name}}{{/parameters}}; -------------------------------------------------------------------------------- /lib/templates/request_param_getter.mustache: -------------------------------------------------------------------------------- 1 | => parameters[r'{{name}}']; -------------------------------------------------------------------------------- /lib/templates/request_param_setter.mustache: -------------------------------------------------------------------------------- 1 | { 2 | parameters[r'{{name}}'] = value; 3 | } -------------------------------------------------------------------------------- /lib/templates/request_remove.mustache: -------------------------------------------------------------------------------- 1 | => parameters.remove(r'{{name}}'); -------------------------------------------------------------------------------- /lib/templates/request_send.mustache: -------------------------------------------------------------------------------- 1 | { 2 | {{#sendParams}} local[r'{{name}}'] = {{name}}; 3 | {{/sendParams}} return _sendDirect(){{^raw}} 4 | .map((response) => response.entity){{/raw}}{{#listen}} 5 | .listen(onData){{/listen}}; 6 | } -------------------------------------------------------------------------------- /lib/templates/request_send_direct.mustache: -------------------------------------------------------------------------------- 1 | => root.send(this); -------------------------------------------------------------------------------- /lib/templates/request_unmarshal_response.mustache: -------------------------------------------------------------------------------- 1 | => root.marshaller.unmarshal{{name}}(data); -------------------------------------------------------------------------------- /lib/templates/root_begin_transaction.mustache: -------------------------------------------------------------------------------- 1 | => new {{txClassName}}(txStrategy.beginTransaction(), servicePath, marshaller); -------------------------------------------------------------------------------- /lib/templates/root_constructor.mustache: -------------------------------------------------------------------------------- 1 | : super({{#http}}servicePath{{/http}}); -------------------------------------------------------------------------------- /lib/templates/root_send.mustache: -------------------------------------------------------------------------------- 1 | => requestHandler.handle(request, tracer.trace(request)); -------------------------------------------------------------------------------- /lib/templates/root_transaction_constructor.mustache: -------------------------------------------------------------------------------- 1 | : super(txn, servicePath); -------------------------------------------------------------------------------- /lib/templates/string_list.mustache: -------------------------------------------------------------------------------- 1 | {{#getter}}=> {{/getter}}const [ 2 | {{#list}} r'{{value}}', 3 | {{/list}}]{{#getter}};{{/getter}} -------------------------------------------------------------------------------- /lib/templates/string_map.mustache: -------------------------------------------------------------------------------- 1 | { 2 | {{#map}} r'{{key}}': r'{{value}}', 3 | {{/map}} 4 | } -------------------------------------------------------------------------------- /lib/templates/unmarshal.mustache: -------------------------------------------------------------------------------- 1 | { {{#hasFieldMapping}} 2 | _fieldMapping{{name}}.forEach((key, name) { 3 | if (data.containsKey(key)) { 4 | data[name] = data.remove(key); 5 | } 6 | });{{/hasFieldMapping}}{{#hasInt64s}} 7 | streamy.unmarshalInt64s(_int64s{{name}}, data, lazy: lazy); {{/hasInt64s}}{{#hasDoubles}} 8 | streamy.unmarshalDoubles(_doubles{{name}}, data, lazy: lazy); {{/hasDoubles}}{{#hasEntities}} 9 | streamy.handleEntities(_entities{{name}}, data, false, lazy: lazy); {{/hasEntities}} 10 | return new {{entity}}{{#fromFields}}(){{#fields}} 11 | ..{{identifier}} = data[r'{{key}}']{{/fields}}{{/fromFields}}{{^fromFields}}.wrap(data){{/fromFields}}; 12 | } 13 | -------------------------------------------------------------------------------- /lib/templates/unmarshal_json.mustache: -------------------------------------------------------------------------------- 1 | { 2 | {{#hasInt64s}} 3 | streamy.unmarshalInt64s(_int64s{{name}}, data, lazy: lazy); 4 | {{/hasInt64s}} 5 | {{#hasDoubles}} 6 | streamy.unmarshalDoubles(_doubles{{name}}, data, lazy: lazy); 7 | {{/hasDoubles}} 8 | {{#hasEntities}} 9 | streamy.unmarshalEntities(_entities{{name}}, data, lazy: lazy); 10 | {{/hasEntities}} 11 | return new {{entity}}.wrap(data); 12 | } 13 | -------------------------------------------------------------------------------- /lib/testing/dynamic_entity.dart: -------------------------------------------------------------------------------- 1 | part of streamy.testing; 2 | 3 | @proxy 4 | class DynamicEntity { 5 | 6 | DynamicEntity() : super(); 7 | 8 | DynamicEntity.fromMap(Map data) { 9 | data.forEach((key, value) { 10 | this[key] = toObservable(value); 11 | }); 12 | } 13 | 14 | noSuchMethod(Invocation invocation) { 15 | var memberName = MirrorSystem.getName(invocation.memberName); 16 | if (invocation.isGetter) { 17 | return this[memberName]; 18 | } else if (invocation.isSetter) { 19 | // Setter member names have an '=' at the end, strip it. 20 | var key = memberName.substring(0, memberName.length - 1); 21 | this[key] = invocation.positionalArguments[0]; 22 | } else { 23 | // Perhaps the called was trying to call a nonexistent method, 24 | // or perhaps the caller was trying to invoke a data member as a 25 | // completion. Throw an appropriate error. 26 | if (containsKey(memberName)) { 27 | throw new ClosureInvocationException(memberName); 28 | } else { 29 | throw new NoSuchMethodError(this, memberName, 30 | invocation.positionalArguments, invocation.namedArguments); 31 | } 32 | } 33 | } 34 | 35 | Type get streamyType => DynamicEntity; 36 | } 37 | 38 | class ClosureInvocationException extends StreamyException { 39 | 40 | final String memberName; 41 | 42 | ClosureInvocationException(this.memberName); 43 | 44 | String toString() => "Fields of DynamicEntity objects can't be invoked, as " + 45 | 'they cannot contain closures. Field: $memberName'; 46 | } 47 | -------------------------------------------------------------------------------- /presubmit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run this script before sending code for review. 4 | 5 | set -x 6 | set -e 7 | 8 | # Build everything 9 | pub get 10 | pub build test --mode=debug 11 | 12 | # Check for compiler warnings 13 | ANALYZE_CMD="dartanalyzer build/test/all_tests.dart" 14 | WARNING_COUNT=`${ANALYZE_CMD} | grep -ci warning || echo ''` 15 | 16 | if [ ${WARNING_COUNT} -gt 0 ] 17 | then 18 | echo 'Code contains compiler warnings' 19 | ${ANALYZE_CMD} 20 | exit 1 21 | fi 22 | 23 | # Run unit tests 24 | dart -c --package-root=build/test/packages \ 25 | build/test/all_tests.dart 26 | 27 | # Run integration tests 28 | test/integration/run_tests.sh 29 | -------------------------------------------------------------------------------- /pub-server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "net/http" 7 | "path/filepath" 8 | "log" 9 | "io" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | func filter(path string) bool { 15 | parts := strings.Split(path, "/") 16 | if parts[0] == ".git" || parts[0] == "pubspec.lock" || parts[0] == "packages" { 17 | return false 18 | } 19 | if parts[len(parts) - 1] == "packages" { 20 | return false 21 | } 22 | return true 23 | } 24 | 25 | func TarHandler(resp http.ResponseWriter, req *http.Request) { 26 | resp.Header().Add("Content-Type", "application/octet-stream") 27 | gz := gzip.NewWriter(resp) 28 | defer gz.Close() 29 | tw := tar.NewWriter(gz) 30 | defer tw.Close() 31 | filepath.Walk(".", func(path string, info os.FileInfo, err error) error { 32 | if err != nil { 33 | log.Fatalf("Walk error: %s", err) 34 | } 35 | if info.Name()[0] == '.' { 36 | return nil 37 | } 38 | if !filter(path) { 39 | return nil 40 | } 41 | h, err := tar.FileInfoHeader(info, "") 42 | if err != nil { 43 | log.Fatalf("FileInfoHeader Error: %s", err) 44 | } 45 | h.Name = path 46 | err = tw.WriteHeader(h) 47 | if err != nil { 48 | log.Fatalf("Tar WriteHeader Error: %s", err); 49 | } 50 | if !info.Mode().IsRegular() { 51 | return nil 52 | } 53 | f, err := os.Open(path) 54 | if err != nil { 55 | log.Fatalf("OpenError: %s", err) 56 | } 57 | defer f.Close() 58 | _, err = io.Copy(tw, f) 59 | if err != nil { 60 | log.Fatalf("CopyError: %s", err) 61 | } 62 | return nil 63 | }) 64 | } 65 | 66 | func main() { 67 | http.HandleFunc("/streamy.tar.gz", TarHandler) 68 | err := http.ListenAndServe(":8080", nil) 69 | if err != nil { 70 | log.Fatalf("Failed to start server: %s", err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: streamy 2 | version: 0.2.2-dev.2 3 | description: > 4 | An experimental client API generator for services described using Google's API 5 | discovery format (https://developers.google.com/discovery/v1/using#discovery-doc). 6 | The generator comes with templates that generate streaming API via dart:async's 7 | Stream object. 8 | authors: 9 | - Alex Rickabaugh 10 | - Yegor Jbanov 11 | homepage: https://github.com/google/streamy-dart 12 | environment: 13 | sdk: '>=1.6.0' 14 | dependencies: 15 | barback: ">=0.15.0 <0.16.0" 16 | browser: ">=0.10.0 <0.11.0" 17 | mustache: ">=0.2.3 <0.3.0" 18 | args: ">=0.12.0 <0.13.0" 19 | fixnum: ">=0.9.0 <0.10.0" 20 | observe: ">=0.12.0 <0.13.0" 21 | quiver: ">=0.18.0 <0.20.0" 22 | smoke: ">=0.3.1 <0.4.0" 23 | yaml: ">=2.0.0 <3.0.0" 24 | dev_dependencies: 25 | analyzer: ">=0.22.0 <0.24.0" 26 | path: ">=1.3.0 <1.4.0" 27 | protobuf: ">=0.3.4 <0.4.0" 28 | unittest: ">=0.11.0 <0.12.0" 29 | benchmark_harness: ">=1.0.0 <2.0.0" 30 | intl: ">=0.11.0 <0.12.0" 31 | transformers: 32 | - streamy 33 | - $dart2js: 34 | $include: test/benchmark_html.dart 35 | - $dart2js: 36 | $include: test/test_in_browser.dart 37 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | packages -------------------------------------------------------------------------------- /test/all_tests.dart: -------------------------------------------------------------------------------- 1 | library streamy.test; 2 | 3 | import 'base_test.dart' as base_test; 4 | 5 | import 'generated/addendum_test.dart' as generated_addendum_test; 6 | import 'generated/handler_test.dart' as generated_handler_test; 7 | import 'generated/identifier_name_test.dart' as generated_identifier_name_test; 8 | import 'generated/method_get_test.dart' as generated_method_get_test; 9 | import 'generated/nested_resources_test.dart' as generated_nested_resources_test; 10 | import 'generated/illegal_names_test.dart' as generated_illegal_names_test; 11 | import 'generated/method_post_test.dart' as generated_method_post_test; 12 | import 'generated/method_params_test.dart' as generated_method_params_test; 13 | import 'generated/proto_test.dart' as generated_proto_test; 14 | import 'generated/schema_object_test.dart' as generated_schema_object_test; 15 | import 'generated/reserved_expansion_path_param_test.dart' as generated_reserved_expansion_path_param_test; 16 | 17 | import 'mixins/dot_access_test.dart' as mixins_dot_access_test; 18 | import 'mixins/immutable_test.dart' as mixins_immutable_test; 19 | import 'mixins/is_map_test.dart' as mixins_is_map_test; 20 | import 'mixins/patch_map_test.dart' as mixins_patch_map_test; 21 | 22 | import 'runtime/batching_test.dart' as runtime_batching_test; 23 | import 'runtime/branching_test.dart' as runtime_branching_test; 24 | import 'runtime/cache_test.dart' as runtime_cache_test; 25 | import 'runtime/dedup_test.dart' as runtime_dedup_test; 26 | import 'runtime/error_test.dart' as runtime_error_test; 27 | import 'runtime/json_test.dart' as runtime_json_test; 28 | import 'runtime/http_test.dart' as runtime_http_test; 29 | import 'runtime/multiplexer_test.dart' as runtime_multiplexer_test; 30 | import 'runtime/proxy_test.dart' as runtime_proxy_test; 31 | import 'runtime/request_test.dart' as runtime_request_test; 32 | import 'runtime/transforms_test.dart' as runtime_transforms_test; 33 | import 'runtime/transaction_test.dart' as runtime_transaction_test; 34 | 35 | import 'streamy_test.dart' as streamy_test; 36 | 37 | /* 38 | import 'generator/emitter_test.dart' as generator_emitter_test; 39 | */ 40 | 41 | main(List args) { 42 | ensureCheckedMode(); 43 | 44 | base_test.main(); 45 | generated_addendum_test.main(); 46 | generated_handler_test.main(); 47 | generated_identifier_name_test.main(); 48 | generated_illegal_names_test.main(); 49 | generated_method_get_test.main(); 50 | generated_method_post_test.main(); 51 | generated_method_params_test.main(); 52 | generated_nested_resources_test.main(); 53 | generated_proto_test.main(); 54 | generated_schema_object_test.main(); 55 | generated_reserved_expansion_path_param_test.main(); 56 | 57 | mixins_dot_access_test.main(); 58 | mixins_immutable_test.main(); 59 | mixins_is_map_test.main(); 60 | mixins_patch_map_test.main(); 61 | 62 | runtime_batching_test.main(); 63 | runtime_branching_test.main(); 64 | runtime_cache_test.main(); 65 | runtime_dedup_test.main(); 66 | runtime_error_test.main(); 67 | runtime_json_test.main(); 68 | runtime_http_test.main(); 69 | runtime_multiplexer_test.main(); 70 | runtime_proxy_test.main(); 71 | runtime_request_test.main(); 72 | runtime_transforms_test.main(); 73 | runtime_transaction_test.main(); 74 | streamy_test.main(); 75 | /* 76 | generator_emitter_test.main(args); 77 | */ 78 | } 79 | 80 | void ensureCheckedMode() { 81 | try { 82 | Object a = "Testing checked mode..."; 83 | int b = a; 84 | print(b); // ensures that the code is not tree-shaken off 85 | throw new StateError("Checked mode is disabled. Use option -c."); 86 | } on TypeError { 87 | // expected 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/base_test.dart: -------------------------------------------------------------------------------- 1 | library base_test; 2 | 3 | import 'dart:convert'; 4 | import 'package:streamy/streamy.dart'; 5 | import 'package:unittest/unittest.dart'; 6 | import 'package:observe/observe.dart'; 7 | import 'generated/bank_api_client_objects.dart'; 8 | 9 | main() { 10 | group('base.Entity', () { 11 | Branch entity; 12 | setUp(() { 13 | entity = new Branch(); 14 | }); 15 | test('.wrapMap constructor does not copy data', () { 16 | var map = toObservable({'list': [1, 2, 3]}); 17 | var e = new Branch.wrap(map); 18 | map['list'].add(4); 19 | expect(e['list'].length, equals(4)); 20 | expect(e['list'][3], equals(4)); 21 | }); 22 | test('does allow setting closures on .local', () { 23 | var e = new Branch(); 24 | e['local.foo'] = () => true; 25 | }); 26 | test('exists and is a Map', () { 27 | expect(entity.local, isNotNull); 28 | expect(entity.local, new isInstanceOf()); 29 | }); 30 | test('stores and retrieves data via operator[]', () { 31 | entity.local['foo'] = 'bar'; 32 | expect(entity.local['foo'], equals('bar')); 33 | }); 34 | test('does not survive cloning', () { 35 | entity.local['foo'] = 'this should not be cloned'; 36 | expect(entity.clone().local['foo'], isNull); 37 | }); 38 | test('local cannot be set', () { 39 | expect(() => entity['local'] = {}, 40 | throwsA(new isInstanceOf())); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/benchmark.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Streamy Benchmark 6 | 7 | 8 | 9 | 10 |
NOTE: UI will freeze while running the benchmarks
11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/benchmark_html.dart: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs Streamy benchmarks in the web browser. 3 | */ 4 | library streamy.benchmarks.html; 5 | 6 | import 'dart:html'; 7 | import 'benchmark.dart'; 8 | 9 | main() { 10 | ButtonElement runBtn = find('#runBtn'); 11 | runBtn.onClick.listen((_) { 12 | runBtn.style.display = 'none'; 13 | runWithConfig(new BenchmarkConfig(printReport)); 14 | }); 15 | } 16 | 17 | void printReport(StreamyBenchmarkReport r) { 18 | find('#log').appendText(r.toTsv() + '\n'); 19 | } 20 | 21 | Element find(String selector) => document.querySelector(selector); 22 | -------------------------------------------------------------------------------- /test/generated/.gitignore: -------------------------------------------------------------------------------- 1 | packages -------------------------------------------------------------------------------- /test/generated/README: -------------------------------------------------------------------------------- 1 | TODO(yjbanov): Do not require checking in generated test clients when 2 | https://code.google.com/p/dart/issues/detail?id=5187 is fixed. 3 | 4 | This package contains tests that test the correctness of the client API 5 | generator. Tests are organized into triplets of files like this: 6 | 7 | NAME_test.json 8 | Contains a sample JSON Google API discovery document. 9 | NAME_client.dart 10 | Generated API client from the corresponsing _test.json file. 11 | NAME_test.dart 12 | A unittest that tests the generated API client. 13 | 14 | To generate _client.dart files after you added/edited a _test.json file, run 15 | bin/generate_test_clients.dart. Depending on your source version control system 16 | you may need to make the existing files editable prior to generating test 17 | clients. 18 | -------------------------------------------------------------------------------- /test/generated/addendum_addendum.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AddendumApi" 3 | } 4 | -------------------------------------------------------------------------------- /test/generated/addendum_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.generated.addendum.test; 2 | 3 | import 'dart:async'; 4 | import 'package:unittest/unittest.dart'; 5 | import 'package:streamy/streamy.dart'; 6 | import 'addendum_client.dart'; 7 | import 'addendum_client_dispatch.dart'; 8 | import 'addendum_client_objects.dart'; 9 | 10 | main() { 11 | group('Addendum', () { 12 | test('Can send requests', () { 13 | var subject = new AddendumApi(new ImmediateRequestHandler(new Foo()..id = 1)); 14 | subject.foos.get(1).send(foo: 'baz').first.then((res) { 15 | expect(res.id, equals(1)); 16 | }); 17 | expect(subject.servicePath, equals('addendum/v1/')); 18 | }); 19 | test('listen() shortcut', () { 20 | var subject = new AddendumApi(new ImmediateRequestHandler(new Foo()..id = 1)); 21 | subject.foos.get(1).listen((res) { 22 | expect(res.id, equals(1)); 23 | }, foo: 'baz'); 24 | }); 25 | }); 26 | } 27 | 28 | class ImmediateRequestHandler extends RequestHandler { 29 | Stream stream; 30 | ImmediateRequestHandler(Foo value) { 31 | this.stream = new Stream.fromIterable([jsonMarshal(value)]); 32 | } 33 | Stream> handle(HttpRequest request, Trace trace) { 34 | expect(request.local['dedup'], equals(true)); 35 | expect(request.local['ttl'], equals(800)); 36 | expect(request.local['foo'], equals('baz')); 37 | return stream.map((data) => new Response(request.unmarshalResponse(data), Source.RPC, 0)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/generated/addendum_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AddendumTest", 3 | "description": "Test client for addendum documents.", 4 | "servicePath": "addendum/v1/", 5 | "schemas": { 6 | "Foo": { 7 | "id": "Foo", 8 | "type": "object", 9 | "properties": { 10 | "id": { 11 | "type": "integer", 12 | "description": "Primary key." 13 | }, 14 | "bar": { 15 | "type": "string", 16 | "description": "Foo's favorite bar." 17 | } 18 | } 19 | } 20 | }, 21 | "resources": { 22 | "foos": { 23 | "methods": { 24 | "get": { 25 | "id": "service.foos.get", 26 | "path": "foos/{fooId}", 27 | "name": "", 28 | "response": { 29 | "$ref": "Foo" 30 | }, 31 | "httpMethod": "GET", 32 | "description": "Gets a foo", 33 | "parameters": { 34 | "fooId": { 35 | "type": "integer", 36 | "description": "Primary key of foo", 37 | "required": true, 38 | "location": "path" 39 | } 40 | }, 41 | "parameterOrder": ["fooId"] 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/generated/addendum_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: addendum_test.json 2 | addendum: addendum_addendum.json 3 | output: 4 | files: split 5 | prefix: addendum_client 6 | base: 7 | class: Entity 8 | import: package:streamy/base.dart 9 | backing: map 10 | options: 11 | clone: true 12 | removers: true 13 | known: false 14 | request: 15 | sendParams: 16 | foo: 17 | type: string 18 | ttl: 19 | type: int 20 | default: 800 21 | dedup: 22 | type: boolean 23 | default: true -------------------------------------------------------------------------------- /test/generated/bank_api_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bank", 3 | "servicePath": "bank/v1/", 4 | "schemas": { 5 | "Branch": { 6 | "id": "Branch", 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "type": "string", 11 | "description": "Primary key.", 12 | "format": "int64" 13 | }, 14 | "name": { 15 | "type": "string", 16 | "description": "Branch name." 17 | }, 18 | "location": { 19 | "$ref": "Address" 20 | } 21 | } 22 | }, 23 | "Address": { 24 | "id": "Address", 25 | "type": "object", 26 | "address_line": { 27 | "type": "string", 28 | "description": "Street address." 29 | }, 30 | "city": { 31 | "type": "string", 32 | "description": "City." 33 | } 34 | }, 35 | "Account": { 36 | "id": "Account", 37 | "type": "object", 38 | "properties": { 39 | "account_number": { 40 | "type": "string", 41 | "description": "Account number.", 42 | "format": "int64" 43 | }, 44 | "branch_id": { 45 | "type": "integer", 46 | "description": "Branch managing the account.", 47 | "format": "int64" 48 | }, 49 | "account_type": { 50 | "type": "string", 51 | "description": "Account type: CHECKING or SAVINGS" 52 | }, 53 | "currency_type": { 54 | "type": "string", 55 | "description": "Currency code: USD or CDN" 56 | }, 57 | "balance": { 58 | "type": "string", 59 | "description": "Balance on the account.", 60 | "format": "int64" 61 | } 62 | } 63 | }, 64 | "Customer": { 65 | "id": "Customer", 66 | "type": "object", 67 | "properties": { 68 | "accounts": { 69 | "type": "array", 70 | "description": "Customer's account numbers.", 71 | "items": { 72 | "type": "string", 73 | "description": "Account number.", 74 | "format": "int64" 75 | } 76 | }, 77 | "name": { 78 | "type": "string", 79 | "description": "Customer's full name." 80 | } 81 | } 82 | } 83 | }, 84 | "resources": { 85 | "branches": { 86 | "methods": { 87 | "get": { 88 | "id": "bank.branches.get", 89 | "path": "branches/{branchId}", 90 | "response": { 91 | "$ref": "Branch" 92 | }, 93 | "httpMethod": "GET", 94 | "description": "Retrieves branch information", 95 | "parameters": { 96 | "branchId": { 97 | "type": "string", 98 | "description": "Primary key of a branch", 99 | "format": "int64", 100 | "required": true, 101 | "location": "path" 102 | } 103 | }, 104 | "parameterOrder": ["branchId"] 105 | }, 106 | "insert": { 107 | "id": "bank.branches.insert", 108 | "path": "branches", 109 | "request": { 110 | "$ref": "Branch" 111 | }, 112 | "httpMethod": "POST", 113 | "description": "Inserts a branch", 114 | "parameters": { 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/generated/bank_api_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: bank_api_test.json 2 | output: 3 | files: split 4 | prefix: bank_api_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: false -------------------------------------------------------------------------------- /test/generated/benchmark_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SchemaObjectTest", 3 | "servicePath": "schemaObjectTest/v1/", 4 | "schemas": { 5 | "Foo": { 6 | "id": "Foo", 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "type": "integer", 11 | "description": "Primary key." 12 | }, 13 | "bar": { 14 | "$ref": "Bar", 15 | "description": "Foo's favorite bar." 16 | }, 17 | "baz": { 18 | "type": "integer", 19 | "description": "It's spelled buzz." 20 | }, 21 | "cruft": { 22 | "type": "string" 23 | }, 24 | "qux": { 25 | "type": "string", 26 | "description": "Not what it seems.", 27 | "format": "int64" 28 | }, 29 | "quux": { 30 | "type": "array", 31 | "description": "The plural of qux", 32 | "items": { 33 | "type": "string", 34 | "format": "double" 35 | } 36 | }, 37 | "corge": { 38 | "type": "array", 39 | "description": "A double field that's serialized as a number.", 40 | "items": { 41 | "type": "integer" 42 | } 43 | } 44 | } 45 | }, 46 | "Bar": { 47 | "id": "Bar", 48 | "type": "object", 49 | "properties": { 50 | "foos": { 51 | "type": "array", 52 | "description": "A bunch of foos.", 53 | "items": { 54 | "$ref": "Foo" 55 | } 56 | }, 57 | "foo": { 58 | "$ref": "Foo" 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/generated/benchmark_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: benchmark_test.json 2 | output: 3 | files: split 4 | prefix: benchmark_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: true 13 | global: true 14 | -------------------------------------------------------------------------------- /test/generated/handler_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HandlerTest", 3 | "servicePath": "handlerTest/v1/", 4 | "schemas": { 5 | "Foo": { 6 | "id": "Foo", 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "type": "integer", 11 | "description": "Primary key." 12 | }, 13 | "bar": { 14 | "type": "string", 15 | "description": "Foo's favorite bar." 16 | } 17 | } 18 | } 19 | }, 20 | "resources": { 21 | "foos": { 22 | "methods": { 23 | "get": { 24 | "id": "service.foos.get", 25 | "path": "foos/{id}", 26 | "name": "", 27 | "response": { 28 | "$ref": "Foo" 29 | }, 30 | "httpMethod": "GET", 31 | "description": "Gets a foo", 32 | "parameters": { 33 | "id": { 34 | "type": "integer", 35 | "description": "Primary key of foo", 36 | "required": true, 37 | "location": "path" 38 | } 39 | }, 40 | "parameterOrder": ["id"] 41 | }, 42 | "update": { 43 | "id": "service.foos.update", 44 | "path": "foos/{id}", 45 | "name": "", 46 | "request": { 47 | "$ref": "Foo" 48 | }, 49 | "response": { 50 | "$ref": "Foo" 51 | }, 52 | "httpMethod": "PUT", 53 | "description": "Updates a foo", 54 | "parameters": { 55 | "id": { 56 | "type": "integer", 57 | "description": "Primary key of foo", 58 | "required": true, 59 | "location": "path" 60 | } 61 | }, 62 | "parameterOrder": ["id"] 63 | }, 64 | "delete": { 65 | "id": "service.foos.delete", 66 | "path": "foos/{id}", 67 | "name": "", 68 | "httpMethod": "DELETE", 69 | "description": "Deletes a foo", 70 | "parameters": { 71 | "id": { 72 | "type": "integer", 73 | "description": "Primary key of foo", 74 | "required": true, 75 | "location": "path" 76 | } 77 | }, 78 | "parameterOrder": ["id"] 79 | }, 80 | "cancel": { 81 | "id": "service.foos.cancel", 82 | "path": "foos/cancel/{id}", 83 | "name": "", 84 | "httpMethod": "GET", 85 | "description": "A method to test request cancellation", 86 | "parameters": { 87 | "id": { 88 | "type": "integer", 89 | "description": "Primary key of foo", 90 | "required": true, 91 | "location": "path" 92 | } 93 | }, 94 | "parameterOrder": ["id"] 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/generated/handler_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: handler_test.json 2 | output: 3 | files: split 4 | prefix: handler_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: false 13 | global: false -------------------------------------------------------------------------------- /test/generated/identifier_name_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.generated.identifier_name.test; 2 | 3 | import 'dart:async'; 4 | import 'package:unittest/unittest.dart'; 5 | import 'package:streamy/streamy.dart'; 6 | import 'identifier_name_client.dart'; 7 | import '../utils.dart'; 8 | 9 | main() { 10 | group('Identifier name', () { 11 | IdentifierNameTest root; 12 | 13 | setUp(() { 14 | root = new IdentifierNameTest(null); 15 | }); 16 | 17 | test('for root class', () { 18 | expectType(IdentifierNameTest); 19 | }); 20 | test('for resources mixin', () { 21 | expectType(IdentifierNameTestResourcesMixin); 22 | }); 23 | test('for transaction', () { 24 | expectType(IdentifierNameTestTransaction); 25 | }); 26 | test('for schema object', () { 27 | expectType(Foo); 28 | }); 29 | test('for property', () { 30 | var foo = new Foo()..bar = 3; 31 | expect(foo.bar, 3); 32 | }); 33 | test('for method get', () { 34 | root.foos.get(1); 35 | }); 36 | test('for request class for method get', () { 37 | expectType(FoosGetRequest); 38 | var req = root.foos.get(1); 39 | expect(req, new isAssignableTo()); 40 | }); 41 | test('for method list', () { 42 | root.foos.list(); 43 | }); 44 | test('for request class for method list', () { 45 | expectType(FoosListRequest); 46 | var req = root.foos.list(); 47 | expect(req, new isAssignableTo()); 48 | }); 49 | test('for query parameter', () { 50 | root.foos.list() 51 | ..bar = 234; 52 | }); 53 | }); 54 | } 55 | 56 | expectType(Type type) { 57 | expect(type, isType); 58 | } 59 | -------------------------------------------------------------------------------- /test/generated/identifier_name_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IdentifierNameTest", 3 | "servicePath": "identifierName/v1/", 4 | "schemas": { 5 | "Foo": { 6 | "id": "Foo", 7 | "type": "object", 8 | "properties": { 9 | "bar": { 10 | "type": "integer" 11 | } 12 | } 13 | }, 14 | "FooListResponse": { 15 | "id": "FooListResponse", 16 | "type": "object", 17 | "properties": { 18 | "items": { 19 | "type": "array", 20 | "$ref": "Foo" 21 | } 22 | } 23 | } 24 | }, 25 | "resources": { 26 | "foos": { 27 | "methods": { 28 | "get": { 29 | "id": "service.foos.get", 30 | "path": "foos/{fooId}", 31 | "response": { 32 | "$ref": "Foo" 33 | }, 34 | "httpMethod": "GET", 35 | "parameters": { 36 | "fooId": { 37 | "type": "integer", 38 | "required": true, 39 | "location": "path" 40 | } 41 | }, 42 | "parameterOrder": ["fooId"] 43 | }, 44 | "list": { 45 | "id": "service.foos.list", 46 | "httpMethod": "GET", 47 | "path": "foos", 48 | "response": { 49 | "$ref": "FooListResponse" 50 | }, 51 | "parameters": { 52 | "bar": { 53 | "type": "integer", 54 | "location": "query" 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/generated/identifier_name_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: identifier_name_test.json 2 | output: 3 | files: single 4 | prefix: identifier_name_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: true 13 | global: true 14 | patch: true 15 | -------------------------------------------------------------------------------- /test/generated/illegal_names_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.generated.illegal_names.test; 2 | 3 | import 'dart:async'; 4 | import 'package:unittest/unittest.dart'; 5 | import 'package:streamy/streamy.dart'; 6 | import 'illegal_names_client.dart'; 7 | import 'illegal_names_client_requests.dart'; 8 | import 'illegal_names_client_resources.dart'; 9 | import 'illegal_names_client_objects.dart'; 10 | import 'illegal_names_client_dispatch.dart'; 11 | 12 | main() { 13 | group('IllegalNamesTest', () { 14 | test('RequestResponseCycle as response', () { 15 | $Type testResponse = new $Type()..id = 1; 16 | var marshaller = new Marshaller(); 17 | var testResponsePayload = new Response( 18 | marshaller.unmarshalType(jsonMarshal(testResponse)), Source.RPC, 0); 19 | var testRequestHandler = new RequestHandler.fromFunction( 20 | (req) => new Stream.fromIterable([testResponsePayload])); 21 | var subject = new IllegalNamesTest(testRequestHandler); 22 | subject.types.get(1).send().listen(expectAsync(($Type v) { 23 | expect(jsonMarshal(v), equals(jsonMarshal(testResponse))); 24 | }, count: 1)); 25 | }); 26 | test('RequestResponseCycle as field', () { 27 | var type = new $Type()..id = 2; 28 | Foo testResponse = new Foo() 29 | ..id = 1 30 | ..fooType = type; 31 | var marshaller = new Marshaller(); 32 | var testRequestHandler = new RequestHandler.fromFunction( 33 | (req) => new Stream.fromIterable( 34 | [new Response(marshaller.unmarshalFoo(jsonMarshal(testResponse)), Source.RPC, 0)])); 35 | var subject = new IllegalNamesTest(testRequestHandler); 36 | subject.foos.get(1).send().listen(expectAsync((Foo v) { 37 | expect(jsonMarshal(v), equals(jsonMarshal(testResponse))); 38 | }, count: 1)); 39 | }); 40 | }); 41 | group('Illegally named property apiType', () { 42 | test('of Type', () { 43 | expect($Type.API_TYPE, '\$Type'); 44 | expect(new $Type().apiType, '\$Type'); 45 | }); 46 | test('of TypesResource', () { 47 | expect(TypesResource.API_TYPE, 'TypesResource'); 48 | expect(new IllegalNamesTest(null).types.apiType, 'TypesResource'); 49 | }); 50 | test('of TypesGetRequest', () { 51 | expect(TypesGetRequest.API_TYPE, 'TypesGetRequest'); 52 | expect(new IllegalNamesTest(null).types.get(1).apiType, 'TypesGetRequest'); 53 | }); 54 | }); 55 | group('Illegally named property serialization', () { 56 | test('to/from json as object', () { 57 | var f = new $Type()..id = 1; 58 | var m = new Marshaller(); 59 | var f2 = m.unmarshalType(jsonMarshal(f)); 60 | expect(f2.id, equals(1)); 61 | }); 62 | test('to/from json as field', () { 63 | var type = new $Type()..id = 2; 64 | var foo = new Foo() 65 | ..id = 1 66 | ..fooType = type; 67 | var m = new Marshaller(); 68 | var foo2 = m.unmarshalFoo(jsonMarshal(foo)); 69 | expect(foo2.id, equals(1)); 70 | expect(foo2.fooType.id, equals(2)); 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /test/generated/illegal_names_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IllegalNamesTest", 3 | "servicePath": "illegalNamesTest/v1/", 4 | "schemas": { 5 | "Type": { 6 | "id": "Type", 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "type": "integer", 11 | "description": "Primary key." 12 | } 13 | } 14 | }, 15 | "Foo": { 16 | "id": "Foo", 17 | "type": "object", 18 | "properties": { 19 | "id": { 20 | "type": "integer", 21 | "description": "Primary key." 22 | }, 23 | "fooType": { 24 | "$ref": "Type" 25 | } 26 | } 27 | } 28 | }, 29 | "resources": { 30 | "types": { 31 | "methods": { 32 | "get": { 33 | "id": "service.types.get", 34 | "path": "types/{typeId}", 35 | "name": "", 36 | "response": { 37 | "$ref": "Type" 38 | }, 39 | "httpMethod": "GET", 40 | "description": "Gets a type", 41 | "parameters": { 42 | "typeId": { 43 | "type": "integer", 44 | "description": "Primary key of type", 45 | "required": true, 46 | "location": "path" 47 | } 48 | }, 49 | "parameterOrder": ["typeId"] 50 | } 51 | } 52 | }, 53 | "foos": { 54 | "methods": { 55 | "get": { 56 | "id": "service.foos.get", 57 | "path": "foos/{fooId}", 58 | "name": "", 59 | "response": { 60 | "$ref": "Foo" 61 | }, 62 | "httpMethod": "GET", 63 | "description": "Gets a foo", 64 | "parameters": { 65 | "fooId": { 66 | "type": "integer", 67 | "description": "Primary key of foo", 68 | "required": true, 69 | "location": "path" 70 | } 71 | }, 72 | "parameterOrder": ["fooId"] 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/generated/illegal_names_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: illegal_names_test.json 2 | output: 3 | files: split 4 | prefix: illegal_names_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: false 13 | -------------------------------------------------------------------------------- /test/generated/import_test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package import_test.import_proto; 4 | 5 | message Baz { 6 | optional string id = 1; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /test/generated/import_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | proto: 2 | name: ImportProto 3 | source: 4 | file: $CWD/test/generated/import_test.proto 5 | root: $CWD/test/generated/ 6 | output: 7 | files: single 8 | prefix: import_client 9 | base: 10 | class: Entity 11 | import: package:streamy/base.dart 12 | backing: map 13 | options: 14 | clone: false 15 | removers: false 16 | known: false 17 | global: false 18 | patch: false 19 | -------------------------------------------------------------------------------- /test/generated/method_get_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.generated.method_get.test; 2 | 3 | import 'dart:async'; 4 | import 'package:unittest/unittest.dart'; 5 | import 'package:streamy/streamy.dart'; 6 | import 'method_get_client.dart'; 7 | import 'method_get_client_requests.dart'; 8 | import 'method_get_client_resources.dart'; 9 | import 'method_get_client_objects.dart'; 10 | import 'method_get_client_dispatch.dart'; 11 | 12 | main() { 13 | group('MethodGetTest', () { 14 | test('RequestHttpMethod', () { 15 | var subject = new MethodGetTest(null); 16 | expect(subject.foos.get(1).httpMethod, equals('GET')); 17 | }); 18 | test('RequestPayload', () { 19 | var subject = new MethodGetTest(null); 20 | expect(subject.foos.get(1).hasPayload, equals(false)); 21 | }); 22 | test('RequestResponseCycle', () { 23 | Foo testResponse = new Foo() 24 | ..id = 1 25 | ..bar = 'bar'; 26 | var testRequestHandler = new RequestHandler.fromFunction( 27 | (req) => new Stream.fromIterable( 28 | [new Response(req.unmarshalResponse(jsonMarshal(testResponse)), Source.RPC, 0)])); 29 | var subject = new MethodGetTest(testRequestHandler); 30 | subject.foos.get(1).send().listen(expectAsync((Foo v) { 31 | expect(jsonMarshal(v), equals(jsonMarshal(testResponse))); 32 | }, count: 1)); 33 | }); 34 | test('API root has proper service path', () { 35 | var subject = new MethodGetTest(null); 36 | expect(subject.servicePath, equals('getTest/v1/')); 37 | }); 38 | }); 39 | group('apiType', () { 40 | test('of MethodGetTest', () { 41 | expect(MethodGetTest.API_TYPE, 'MethodGetTest'); 42 | expect(new MethodGetTest(null).apiType, 'MethodGetTest'); 43 | }); 44 | test('of MethodGetTestTransaction', () { 45 | expect(MethodGetTestTransaction.API_TYPE, 'MethodGetTestTransaction'); 46 | expect(new MethodGetTestTransaction(null, null, null).apiType, 47 | 'MethodGetTestTransaction'); 48 | }); 49 | test('of Foo', () { 50 | expect(Foo.API_TYPE, 'Foo'); 51 | expect(new Foo().apiType, 'Foo'); 52 | }); 53 | test('of FoosResource', () { 54 | expect(FoosResource.API_TYPE, 'FoosResource'); 55 | expect(new MethodGetTest(null).foos.apiType, 'FoosResource'); 56 | }); 57 | test('of FoosGetRequest', () { 58 | expect(FoosGetRequest.API_TYPE, 'FoosGetRequest'); 59 | expect(new MethodGetTest(null).foos.get(1).apiType, 'FoosGetRequest'); 60 | }); 61 | }); 62 | group('Serialization', () { 63 | test('to/from json', () { 64 | var f = new Foo() 65 | ..id = 1; 66 | var m = new Marshaller(); 67 | var f2 = m.unmarshalFoo(jsonMarshal(f)); 68 | expect(f2.containsKey('bar'), isFalse); 69 | expect(f2.containsKey('baz'), isFalse); 70 | }); 71 | }); 72 | group('Root object', () { 73 | test('should allow specifying custom servicePath', () { 74 | var root = new MethodGetTest(null, servicePath: '/differentPath'); 75 | expect(root.servicePath, '/differentPath'); 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /test/generated/method_get_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MethodGetTest", 3 | "servicePath": "getTest/v1/", 4 | "schemas": { 5 | "Foo": { 6 | "id": "Foo", 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "type": "integer", 11 | "description": "Primary key." 12 | }, 13 | "bar": { 14 | "type": "string", 15 | "description": "Foo's favorite bar." 16 | }, 17 | "baz": { 18 | "type": "string", 19 | "format": "int64" 20 | } 21 | } 22 | } 23 | }, 24 | "resources": { 25 | "foos": { 26 | "methods": { 27 | "get": { 28 | "id": "service.foos.get", 29 | "path": "foos/{fooId}", 30 | "name": "", 31 | "response": { 32 | "$ref": "Foo" 33 | }, 34 | "httpMethod": "GET", 35 | "description": "Gets a foo", 36 | "parameters": { 37 | "fooId": { 38 | "type": "integer", 39 | "description": "Primary key of foo", 40 | "required": true, 41 | "location": "path" 42 | } 43 | }, 44 | "parameterOrder": ["fooId"] 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/generated/method_get_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: method_get_test.json 2 | output: 3 | files: split 4 | prefix: method_get_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: false -------------------------------------------------------------------------------- /test/generated/method_params_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.generated.method_params.test; 2 | 3 | import 'package:unittest/unittest.dart'; 4 | import 'method_params_client_requests.dart'; 5 | 6 | main() { 7 | group('MethodParamsTest', () { 8 | var req; 9 | setUp(() { 10 | req = new FoosGetRequest(null) 11 | ..barId = 'abc' 12 | ..fooId = 123; 13 | }); 14 | test('All non-repeated parameters populated', () { 15 | req 16 | ..param1 = true 17 | ..param2 = false; 18 | expect(req.path, equals('foos/abc/123?param1=true¶m2=false')); 19 | }); 20 | test('Missing query parameter', () { 21 | req.param1 = true; 22 | expect(req.path, equals('foos/abc/123?param1=true')); 23 | }); 24 | test('No query parameter', () { 25 | expect(req.path, equals('foos/abc/123')); 26 | }); 27 | test('Repeated parameter type is list', () { 28 | expect(req.param3, new isInstanceOf()); 29 | }); 30 | test('Repeated parameters', () { 31 | req 32 | ..param1 = true 33 | ..param3.addAll(['foo', 'bar']); 34 | expect(req.path, equals( 35 | 'foos/abc/123?param1=true¶m3=bar¶m3=foo')); 36 | }); 37 | }); 38 | group('Request object tests', () { 39 | var r1; 40 | var r2; 41 | setUp(() { 42 | r1 = new FoosGetRequest(null) 43 | ..param3.addAll(['foo', 'bar']); 44 | r2 = new FoosGetRequest(null) 45 | ..param3.addAll(['foo', 'bar']); 46 | }); 47 | test('Repeated parameters are comparable', () { 48 | expect(r1 == r2, isTrue); 49 | }); 50 | test('Cloned requests are equal', () { 51 | expect(r1.clone(), equals(r1)); 52 | }); 53 | test('Cloned repeated parameters are not identical', () { 54 | expect(identical(r1.param3, r1.clone().param3), isFalse); 55 | }); 56 | // TODO(yjbanov): disabling temporarily until named params are back. 57 | // test('Named parameters get copied into the request object', () { 58 | // var req = new MethodParamsTest(null) 59 | // .foos 60 | // .get("a", 2, param1: false, param2: true, param3: ["b", "c"]); 61 | // expect(req.barId, equals("a")); 62 | // expect(req.fooId, equals(2)); 63 | // expect(req.param1, isFalse); 64 | // expect(req.param2, isTrue); 65 | // expect(req.param3, equals(["b", "c"])); 66 | // }); 67 | // test('Not specifying a named parameter does not add null to the request object.', () { 68 | // var req = new MethodParamsTest(null) 69 | // .foos 70 | // .get("a", 2, param1: false); 71 | // expect(req.parameters.containsKey('param2'), isFalse); 72 | // expect(req.parameters.containsKey('param3'), isTrue); 73 | // }); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /test/generated/method_params_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MethodParamsTest", 3 | "servicePath": "paramsTest/v1/", 4 | "resources": { 5 | "foos": { 6 | "methods": { 7 | "get": { 8 | "id": "service.foos.get", 9 | "path": "foos/{barId}/{fooId}", 10 | "name": "", 11 | "httpMethod": "GET", 12 | "description": "Gets a foo", 13 | "parameters": { 14 | "barId": { 15 | "type": "string", 16 | "description": "Primary key of bar", 17 | "required": true, 18 | "location": "path" 19 | }, 20 | "fooId": { 21 | "type": "integer", 22 | "description": "Primary key of foo", 23 | "required": true, 24 | "location": "path" 25 | }, 26 | "param1": { 27 | "type": "boolean", 28 | "description": "A parameter", 29 | "required": false, 30 | "location": "query" 31 | }, 32 | "param2": { 33 | "type": "boolean", 34 | "description": "Another parameter", 35 | "required": false, 36 | "location": "query" 37 | }, 38 | "param3": { 39 | "type": "string", 40 | "description": "A repeated parameter", 41 | "required": false, 42 | "repeated": true, 43 | "location": "query" 44 | } 45 | }, 46 | "parameterOrder": ["barId", "fooId"] 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/generated/method_params_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: method_params_test.json 2 | output: 3 | files: split 4 | prefix: method_params_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: false 13 | -------------------------------------------------------------------------------- /test/generated/method_post_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.generated.method_post.test; 2 | 3 | import 'dart:async'; 4 | import 'package:streamy/streamy.dart'; 5 | import 'package:unittest/unittest.dart'; 6 | import 'method_post_client.dart'; 7 | import 'method_post_client_objects.dart'; 8 | 9 | main() { 10 | group('MethodPostTest', () { 11 | var foo; 12 | setUp(() { 13 | foo = new Foo() 14 | ..id = 1 15 | ..bar = 'bar'; 16 | }); 17 | test('RequestHttpMethod', () { 18 | var subject = new MethodPostTest(null); 19 | expect(subject.foos.update(foo).httpMethod, equals('POST')); 20 | }); 21 | test('RequestPayload', () { 22 | var subject = new MethodPostTest(null); 23 | expect(subject.foos.update(foo).hasPayload, equals(true)); 24 | }); 25 | test('Can clone request payload', () { 26 | var subject = new MethodPostTest(null); 27 | var update = subject.foos.update(foo); 28 | var clone = update.clone(); 29 | expect(clone, equals(update)); 30 | expect(clone, isNot(same(update))); 31 | expect(clone.payload, isNot(same(update.payload))); 32 | }); 33 | test('RequestResponseCycle', () { 34 | var subject = new MethodPostTest(new ImmediateRequestHander()); 35 | var testReq = subject.foos.update(foo) 36 | ..id = 123; 37 | testReq.send().single.then(expectAsync((e) { 38 | expect(e.bar, equals('test')); 39 | }, count: 1)); 40 | }); 41 | test('Prepopulates id from payload', () { 42 | var subject = new MethodPostTest(null); 43 | expect(subject.foos.update(foo).path, equals('foos/1')); 44 | }); 45 | }); 46 | } 47 | 48 | class ImmediateRequestHander extends RequestHandler { 49 | Stream handle(Request request, Trace trace) => new Stream.fromIterable([new Response(new Foo()..bar = 'test', Source.RPC, 0)]); 50 | } 51 | -------------------------------------------------------------------------------- /test/generated/method_post_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MethodPostTest", 3 | "servicePath": "postTest/v1/", 4 | "schemas": { 5 | "Foo": { 6 | "id": "Foo", 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "type": "integer", 11 | "description": "Primary key." 12 | }, 13 | "bar": { 14 | "type": "string", 15 | "description": "Foo's favorite bar." 16 | } 17 | } 18 | } 19 | }, 20 | "resources": { 21 | "foos": { 22 | "methods": { 23 | "update": { 24 | "id": "service.foos.get", 25 | "path": "foos/{id}", 26 | "name": "", 27 | "request": { 28 | "$ref": "Foo" 29 | }, 30 | "httpMethod": "POST", 31 | "description": "Updates a foo", 32 | "parameters": { 33 | "id": { 34 | "type": "integer", 35 | "description": "Primary key of foo", 36 | "required": true, 37 | "location": "path" 38 | } 39 | }, 40 | "parameterOrder": ["id"] 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/generated/method_post_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: method_post_test.json 2 | output: 3 | files: split 4 | prefix: method_post_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: false -------------------------------------------------------------------------------- /test/generated/nested_resources_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.generated.nested_resources.test; 2 | 3 | import 'dart:async'; 4 | import 'package:unittest/unittest.dart'; 5 | import 'package:streamy/streamy.dart'; 6 | import 'nested_resources_client.dart'; 7 | import 'nested_resources_client_requests.dart'; 8 | import 'nested_resources_client_resources.dart'; 9 | import 'nested_resources_client_objects.dart'; 10 | import 'nested_resources_client_dispatch.dart'; 11 | 12 | main() { 13 | group('NestedResourcesTest', () { 14 | test('Generates top-level resources', () { 15 | var subject = new NestedResourcesTest(null); 16 | expect(subject.foos.runtimeType, equals(FoosResource)); 17 | }); 18 | test('Generates second-level resources', () { 19 | var subject = new NestedResourcesTest(null); 20 | expect(subject.foos.bars.runtimeType, equals(FoosBarsResource)); 21 | }); 22 | test('Generates third-level resources', () { 23 | var subject = new NestedResourcesTest(null); 24 | expect(subject.foos.bars.bazes.runtimeType, equals(FoosBarsBazesResource)); 25 | }); 26 | test('Generates methods for top-level resources', () { 27 | var subject = new NestedResourcesTest(null); 28 | expect(subject.foos.get(1).httpMethod, equals('GET')); 29 | }); 30 | test('Generates methods for second-level resources', () { 31 | var subject = new NestedResourcesTest(null); 32 | expect(subject.foos.bars.get(1, 2).httpMethod, equals('GET')); 33 | }); 34 | test('Generates methods for third-level resources', () { 35 | var subject = new NestedResourcesTest(null); 36 | expect(subject.foos.bars.bazes.get(1, 2, 3).httpMethod, equals('GET')); 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /test/generated/nested_resources_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NestedResourcesTest", 3 | "servicePath": "nestedResourcesTest/v1/", 4 | "schemas": { 5 | "Foo": { 6 | "id": "Foo", 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "type": "integer", 11 | "description": "Primary key." 12 | } 13 | } 14 | }, 15 | "Bar": { 16 | "id": "Bar", 17 | "type": "object", 18 | "properties": { 19 | "id": { 20 | "type": "integer", 21 | "description": "Primary key." 22 | } 23 | } 24 | }, 25 | "Baz": { 26 | "id": "Baz", 27 | "type": "object", 28 | "properties": { 29 | "id": { 30 | "type": "integer", 31 | "description": "Primary key." 32 | } 33 | } 34 | } 35 | }, 36 | "resources": { 37 | "foos": { 38 | "methods": { 39 | "get": { 40 | "id": "service.foos.get", 41 | "path": "foos/{fooId}", 42 | "name": "", 43 | "response": { 44 | "$ref": "Foo" 45 | }, 46 | "httpMethod": "GET", 47 | "description": "Gets a foo", 48 | "parameters": { 49 | "fooId": { 50 | "type": "integer", 51 | "description": "Primary key of foo", 52 | "required": true, 53 | "location": "path" 54 | } 55 | }, 56 | "parameterOrder": ["fooId"] 57 | } 58 | }, 59 | "resources": { 60 | "bars": { 61 | "methods": { 62 | "get": { 63 | "id": "service.foos.bars.get", 64 | "path": "foos/{fooId}/bars/{barId}", 65 | "name": "", 66 | "response": { 67 | "$ref": "Bar" 68 | }, 69 | "httpMethod": "GET", 70 | "description": "Gets a bar", 71 | "parameters": { 72 | "fooId": { 73 | "type": "integer", 74 | "description": "Primary key of foo", 75 | "required": true, 76 | "location": "path" 77 | }, 78 | "barId": { 79 | "type": "integer", 80 | "description": "Primary key of bar", 81 | "required": true, 82 | "location": "path" 83 | } 84 | }, 85 | "parameterOrder": ["fooId", "barId"] 86 | } 87 | }, 88 | "resources": { 89 | "bazes": { 90 | "methods": { 91 | "get": { 92 | "id": "service.foos.bars.bazes.get", 93 | "path": "foos/{fooId}/bars/{barId}/bazesbaz/{bazId}", 94 | "name": "", 95 | "response": { 96 | "$ref": "Bar" 97 | }, 98 | "httpMethod": "GET", 99 | "description": "Gets a baz", 100 | "parameters": { 101 | "fooId": { 102 | "type": "integer", 103 | "description": "Primary key of foo", 104 | "required": true, 105 | "location": "path" 106 | }, 107 | "barId": { 108 | "type": "integer", 109 | "description": "Primary key of bar", 110 | "required": true, 111 | "location": "path" 112 | }, 113 | "bazId": { 114 | "type": "integer", 115 | "description": "Primary key of baz", 116 | "required": true, 117 | "location": "path" 118 | } 119 | }, 120 | "parameterOrder": ["fooId", "barId", "bazId"] 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/generated/nested_resources_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: nested_resources_test.json 2 | output: 3 | files: split 4 | prefix: nested_resources_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: false 13 | -------------------------------------------------------------------------------- /test/generated/proto_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.generated.proto.test; 2 | 3 | import 'dart:async'; 4 | import 'package:unittest/unittest.dart'; 5 | import 'package:streamy/streamy.dart'; 6 | import 'proto_client.dart'; 7 | 8 | main() { 9 | var f = new Foo() 10 | ..name = 'Foo Test Object' 11 | ..other = [ 12 | new Bar()..name = 'Bar #1', 13 | new Bar()..name = 'Bar #2' 14 | ]; 15 | var m = new Marshaller(); 16 | group('ProtoTest', () { 17 | test('Serializes with tag numbers', () { 18 | var fm = m.marshalFoo(f); 19 | expect(fm, containsPair('2', 'Foo Test Object')); 20 | expect(fm, contains('3')); 21 | var others = fm['3']; 22 | expect(others, isList); 23 | expect(others, hasLength(2)); 24 | expect(others[0], containsPair('2', 'Bar #1')); 25 | expect(others[1], containsPair('2', 'Bar #2')); 26 | }); 27 | test('Serializer pass-through works as intended', () { 28 | var m = new Marshaller(); 29 | var f2 = m.unmarshalFoo(m.marshalFoo(f)); 30 | expect(f2.name, 'Foo Test Object'); 31 | expect(f2.other, isList); 32 | expect(f2.other, hasLength(2)); 33 | expect(f2.other[0].name, 'Bar #1'); 34 | expect(f2.other[1].name, 'Bar #2'); 35 | }); 36 | test('Can make a Foo request.', () { 37 | var bar = new Bar() 38 | ..name = 'ResponseBar'; 39 | var api = new TestProto(new RequestHandler.fromFunction( 40 | (_) => new Stream.fromIterable([new Response(bar, Source.RPC, 0)]))); 41 | var res = api.Test.Get(new Foo()..name = 'TestRequest') 42 | .send().single.then((v) { 43 | expect(v.name, 'ResponseBar'); 44 | }); 45 | }); 46 | test('Url for test get is correct.', () { 47 | var api = new TestProto(null); 48 | var req = api.Test.Get(new Foo()..name = 'TestRequest'); 49 | expect(api.servicePath, 'test/service/'); 50 | expect(req.path, 'Test/Get'); 51 | }); 52 | test('Enum field accepts a value.', () { 53 | var bar = new Bar(); 54 | bar.ev = TestEnum.BETA; 55 | }); 56 | test('Enum field serializes.', () { 57 | var bar = new Bar() 58 | ..ev = TestEnum.GAMMA; 59 | var m = const Marshaller(); 60 | expect(m.marshalBar(bar)['4'], 3); 61 | }); 62 | test('Enum field deserializes', () { 63 | var map = {'4': 1}; 64 | var bar = const Marshaller().unmarshalBar(map); 65 | expect(bar.ev, TestEnum.ALPHA); 66 | }); 67 | }); 68 | group('Lazy deserialization for protos', () { 69 | test('works for a proto', () { 70 | var fm = m.marshalFoo(f); 71 | var res = m.unmarshalFoo(fm, lazy: true); 72 | expect(fm['other'], new isInstanceOf()); 73 | expect(fm['other'].delegate[0], new isInstanceOf()); 74 | expect(fm['other'].delegate[1], new isInstanceOf()); 75 | expect(res.other[0].name, 'Bar #1'); 76 | expect(res.other[1].name, 'Bar #2'); 77 | expect(fm['other'].delegate[0], new isInstanceOf()); 78 | expect(fm['other'].delegate[1], new isInstanceOf()); 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /test/generated/proto_test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package proto_test; 4 | 5 | import "import_test.proto"; 6 | 7 | message Foo { 8 | optional int64 id = 1; 9 | optional string name = 2; 10 | repeated Bar other = 3; 11 | } 12 | 13 | message Bar { 14 | optional int64 id = 1; 15 | optional string name = 2; 16 | repeated import_test.import_proto.Baz imported = 3; 17 | optional TestEnum ev = 4; 18 | } 19 | 20 | enum TestEnum { 21 | ALPHA = 1; 22 | BETA = 2; 23 | GAMMA = 3; 24 | } 25 | 26 | service Test { 27 | rpc Get(Foo) returns (Bar); 28 | rpc Set(Bar) returns (Foo); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /test/generated/proto_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | proto: 2 | name: TestProto 3 | source: 4 | file: $CWD/test/generated/proto_test.proto 5 | root: $CWD/test/generated/ 6 | dependencies: 7 | ip_test: 8 | package: import_test.import_proto 9 | import: import_client.dart 10 | servicePath: test/service/ 11 | output: 12 | files: single 13 | prefix: proto_client 14 | base: 15 | class: Entity 16 | import: package:streamy/base.dart 17 | backing: map 18 | options: 19 | clone: false 20 | removers: false 21 | known: false 22 | global: false 23 | patch: false 24 | -------------------------------------------------------------------------------- /test/generated/reserved_expansion_path_param_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.generated.reserved_expansion_path_param.test; 2 | 3 | import 'package:unittest/unittest.dart'; 4 | import 'reserved_expansion_path_param_client_requests.dart'; 5 | 6 | main() { 7 | group('ReservedExpansionPathParamTest', () { 8 | test('Reserved Expansion path parameter may contain a slash', () { 9 | var req = new FoosGetRequest(null) 10 | ..barId = 'abc' 11 | ..fooId = 'def/ghi'; 12 | expect(req.path, equals('foos/abc/def/ghi')); 13 | }); 14 | test('Reserved Expansion path parameter does require a slash', () { 15 | var req = new FoosGetRequest(null) 16 | ..barId = 'abc' 17 | ..fooId = 'def'; 18 | expect(req.path, equals('foos/abc/def')); 19 | }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /test/generated/reserved_expansion_path_param_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReservedExpansionPathParamTest", 3 | "servicePath": "reservedExpansionPathParamTest/v1/", 4 | "resources": { 5 | "foos": { 6 | "methods": { 7 | "get": { 8 | "id": "service.foos.get", 9 | "path": "foos/{barId}/{+fooId}", 10 | "name": "", 11 | "httpMethod": "GET", 12 | "description": "Gets a foo", 13 | "parameters": { 14 | "barId": { 15 | "type": "string", 16 | "description": "Primary key of bar", 17 | "required": true, 18 | "location": "path" 19 | }, 20 | "fooId": { 21 | "type": "string", 22 | "description": "Primary key of foo", 23 | "required": true, 24 | "location": "path" 25 | } 26 | }, 27 | "parameterOrder": ["barId", "fooId"] 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/generated/reserved_expansion_path_param_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: reserved_expansion_path_param_test.json 2 | output: 3 | files: split 4 | prefix: reserved_expansion_path_param_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: false 13 | -------------------------------------------------------------------------------- /test/generated/schema_object_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SchemaObjectTest", 3 | "servicePath": "schemaObjectTest/v1/", 4 | "schemas": { 5 | "Foo": { 6 | "id": "Foo", 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "type": "integer", 11 | "description": "Primary key." 12 | }, 13 | "bar": { 14 | "type": "string", 15 | "description": "Foo's favorite bar." 16 | }, 17 | "baz": { 18 | "type": "integer", 19 | "description": "It's spelled buzz." 20 | }, 21 | "qux": { 22 | "type": "string", 23 | "description": "Not what it seems.", 24 | "format": "int64" 25 | }, 26 | "quux": { 27 | "type": "array", 28 | "description": "The plural of qux", 29 | "items": { 30 | "type": "string", 31 | "format": "double" 32 | } 33 | }, 34 | "corge": { 35 | "type": "number", 36 | "description": "A double field that's serialized as a number.", 37 | "format": "double" 38 | } 39 | } 40 | }, 41 | "Bar": { 42 | "id": "Bar", 43 | "type": "object", 44 | "properties": { 45 | "primary": { 46 | "$ref": "Foo", 47 | "description": "The primary foo." 48 | }, 49 | "foos": { 50 | "type": "array", 51 | "description": "A bunch of foos.", 52 | "items": { 53 | "$ref": "Foo" 54 | } 55 | } 56 | } 57 | }, 58 | "Context": { 59 | "id": "Context", 60 | "type": "object", 61 | "properties": { 62 | "facets": { 63 | "type": "array", 64 | "items": { 65 | "type": "array", 66 | "items": { 67 | "type": "object", 68 | "properties": { 69 | "anchor": { 70 | "type": "string" 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | }, 78 | "-some-entity-": { 79 | "id": "-some-entity-", 80 | "type": "object", 81 | "properties": { 82 | "%badly#named property~!@#$%^&*()?": { 83 | "type": "string", 84 | "format": "int64" 85 | } 86 | } 87 | } 88 | }, 89 | "resources": { 90 | "-some-resource-": { 91 | "methods": { 92 | "-some-method-": { 93 | "id": "service.foos.get", 94 | "path": "foos/{fooId}", 95 | "response": { 96 | "$ref": "-some-entity-" 97 | }, 98 | "httpMethod": "GET", 99 | "parameters": { 100 | "-path param-": { 101 | "type": "integer", 102 | "location": "path" 103 | }, 104 | "-query param-": { 105 | "type": "integer", 106 | "location": "path" 107 | } 108 | }, 109 | "parameterOrder": ["-path param-"] 110 | } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/generated/schema_object_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: schema_object_test.json 2 | output: 3 | files: split 4 | prefix: schema_object_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: false 13 | global: true 14 | lazy: true -------------------------------------------------------------------------------- /test/generated/service_test.dart: -------------------------------------------------------------------------------- 1 | import 'service_client.dart'; 2 | 3 | main() { 4 | // TODO(Alex): Implement tests in the future. 5 | } -------------------------------------------------------------------------------- /test/generated/service_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | service: 2 | name: TestService 3 | source: 4 | - import: service_test_interface.dart 5 | file: service_test_interface.dart 6 | output: 7 | files: single 8 | prefix: service_client 9 | base: 10 | class: Entity 11 | import: package:streamy/base_fields.dart 12 | backing: fields 13 | options: 14 | clone: false 15 | removers: false 16 | known: false 17 | global: false 18 | patch: false -------------------------------------------------------------------------------- /test/generated/service_test_interface.dart: -------------------------------------------------------------------------------- 1 | library streamy.test.generated.service.interface; 2 | 3 | class Foo { 4 | int id; 5 | String name; 6 | } 7 | 8 | class Bar { 9 | int id; 10 | Foo foo; 11 | } 12 | 13 | class Service { 14 | Future barFor(Foo foo); 15 | } -------------------------------------------------------------------------------- /test/generator/emitter_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.generator.emitter.test; 2 | 3 | import 'package:streamy/generator.dart'; 4 | import 'package:unittest/unittest.dart'; 5 | import '../project.dart'; 6 | 7 | main(List args) { 8 | var discovery = new Discovery.fromJsonString( 9 | """ 10 | { 11 | "name": "DocsTest", 12 | "description": "API definitions.\\nWith documentation", 13 | "servicePath": "docsTest/v1/", 14 | "schemas": { 15 | "Foo": { 16 | "id": "Foo", 17 | "type": "object", 18 | "description": "This is a foo.\\nEnough said.", 19 | "properties": { 20 | "id": { 21 | "type": "integer", 22 | "description": "Primary key.\\nSometimes called ID." 23 | } 24 | } 25 | } 26 | }, 27 | "resources": { 28 | "foos": { 29 | "methods": { 30 | "get": { 31 | "id": "service.foos.get", 32 | "path": "foos/{fooId}", 33 | "name": "", 34 | "response": { 35 | "\$ref": "Foo" 36 | }, 37 | "httpMethod": "GET", 38 | "description": "Gets a foo.\\nReturns 404 on bad ID.", 39 | "parameters": { 40 | "fooId": { 41 | "type": "integer", 42 | "description": "Primary key of foo.\\nSecond line", 43 | "required": true, 44 | "location": "path" 45 | } 46 | }, 47 | "parameterOrder": ["fooId"] 48 | } 49 | } 50 | } 51 | } 52 | } 53 | """); 54 | 55 | var rootOut = new StringBuffer(); 56 | var resourceOut = new StringBuffer(); 57 | var requestOut = new StringBuffer(); 58 | var objectOut = new StringBuffer(); 59 | emitCode(new EmitterConfig( 60 | discovery, 61 | new DefaultTemplateProvider('${projectRootDir(args)}/templates'), 62 | rootOut, 63 | resourceOut, 64 | requestOut, 65 | objectOut, 66 | addendumData: const { 67 | 'lib_name': 'docstestapi', 68 | })); 69 | 70 | var rootCode = rootOut.toString(); 71 | var resourceCode = resourceOut.toString(); 72 | var requestCode = requestOut.toString(); 73 | var objectCode = objectOut.toString(); 74 | 75 | group('Emitter', () { 76 | test('should emit docs for root class', () { 77 | expectContains(rootCode, 78 | '/// API definitions.\n' 79 | '/// With documentation\n' 80 | 'class DocsTest\n'); 81 | }); 82 | test('should emit docs for resource methods', () { 83 | expectContains(resourceCode, 84 | ' /// Gets a foo.\n' 85 | ' /// Returns 404 on bad ID.\n' 86 | ' req.FoosGetRequest get('); 87 | }); 88 | test('should emit docs for request class', () { 89 | expectContains(requestCode, 90 | '/// Gets a foo.\n' 91 | '/// Returns 404 on bad ID.\n' 92 | 'class FoosGetRequest '); 93 | }); 94 | test('should emit docs for request parameter', () { 95 | expectContains(requestCode, 96 | ' /// Primary key of foo.\n' 97 | ' /// Second line\n' 98 | ' int get fooId =>'); 99 | }); 100 | test('should emit docs for schema class', () { 101 | expectContains(objectCode, 102 | '/// This is a foo.\n' 103 | '/// Enough said.\n' 104 | 'class Foo '); 105 | }); 106 | test('should emit docs for schema property', () { 107 | expectContains(objectCode, 108 | ' /// Primary key.\n' 109 | ' /// Sometimes called ID.\n' 110 | ' int get id => '); 111 | }); 112 | }); 113 | } 114 | 115 | void expectContains(String container, String containee) { 116 | expect(container.contains(containee), isTrue, 117 | reason: "'$container' does not contain '$containee'"); 118 | } 119 | -------------------------------------------------------------------------------- /test/integration/README.md: -------------------------------------------------------------------------------- 1 | # Streamy integration tests 2 | -------------------------------------------------------------------------------- /test/integration/apigen_test/bin/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:bankapi/bank_api_client_objects.dart'; 2 | 3 | main() { 4 | var branch = new Branch(); 5 | branch.name = 'San Francisco North'; 6 | print(branch.name); 7 | } 8 | -------------------------------------------------------------------------------- /test/integration/apigen_test/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: apigen_test 2 | version: 0.0.1 3 | description: > 4 | Tests apigen.dart utility. 5 | dependencies: 6 | streamy: 7 | path: ../../.. 8 | bankapi: 9 | path: ../bankapi 10 | -------------------------------------------------------------------------------- /test/integration/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | # Run apigen test 6 | dart -c bin/apigen.dart \ 7 | --config=test/generated/bank_api_test.streamy.yaml \ 8 | --output-dir=test/integration \ 9 | --package-name=bankapi \ 10 | --package-version=0.0.0 \ 11 | --local-streamy-location=../../.. 12 | 13 | # NOTE: The (cmd1 && cmd2 && ...) trick doesn't work because it 14 | # does not exist on error. Have to cd in and out explicitly. 15 | cd test/integration/apigen_test 16 | pub get 17 | pub build bin --mode=debug 18 | dart -c --package-root=build/bin/packages build/bin/main.dart 19 | cd ../../.. 20 | 21 | # Run transformer test 22 | cd test/integration/transformer_test 23 | pub get 24 | pub build bin --mode=debug 25 | dart -c --package-root=build/bin/packages build/bin/main.dart 26 | cd ../../.. 27 | 28 | echo 'SUCCESS' 29 | -------------------------------------------------------------------------------- /test/integration/transformer_test/bin/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:transformer_test/bank_api_client_objects.dart'; 2 | 3 | main() { 4 | var branch = new Branch(); 5 | branch.name = 'San Francisco North'; 6 | print(branch.name); 7 | } 8 | -------------------------------------------------------------------------------- /test/integration/transformer_test/lib/bank_api_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bank", 3 | "servicePath": "bank/v1/", 4 | "schemas": { 5 | "Branch": { 6 | "id": "Branch", 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "type": "string", 11 | "description": "Primary key.", 12 | "format": "int64" 13 | }, 14 | "name": { 15 | "type": "string", 16 | "description": "Branch name." 17 | }, 18 | "location": { 19 | "$ref": "Address" 20 | } 21 | } 22 | }, 23 | "Address": { 24 | "id": "Address", 25 | "type": "object", 26 | "address_line": { 27 | "type": "string", 28 | "description": "Street address." 29 | }, 30 | "city": { 31 | "type": "string", 32 | "description": "City." 33 | } 34 | }, 35 | "Account": { 36 | "id": "Account", 37 | "type": "object", 38 | "properties": { 39 | "account_number": { 40 | "type": "string", 41 | "description": "Account number.", 42 | "format": "int64" 43 | }, 44 | "branch_id": { 45 | "type": "integer", 46 | "description": "Branch managing the account.", 47 | "format": "int64" 48 | }, 49 | "account_type": { 50 | "type": "string", 51 | "description": "Account type: CHECKING or SAVINGS" 52 | }, 53 | "currency_type": { 54 | "type": "string", 55 | "description": "Currency code: USD or CDN" 56 | }, 57 | "balance": { 58 | "type": "string", 59 | "description": "Balance on the account.", 60 | "format": "int64" 61 | } 62 | } 63 | }, 64 | "Customer": { 65 | "id": "Customer", 66 | "type": "object", 67 | "properties": { 68 | "accounts": { 69 | "type": "array", 70 | "description": "Customer's account numbers.", 71 | "items": { 72 | "type": "string", 73 | "description": "Account number.", 74 | "format": "int64" 75 | } 76 | }, 77 | "name": { 78 | "type": "string", 79 | "description": "Customer's full name." 80 | } 81 | } 82 | } 83 | }, 84 | "resources": { 85 | "branches": { 86 | "methods": { 87 | "get": { 88 | "id": "bank.branches.get", 89 | "path": "branches/{branchId}", 90 | "response": { 91 | "$ref": "Branch" 92 | }, 93 | "httpMethod": "GET", 94 | "description": "Retrieves branch information", 95 | "parameters": { 96 | "branchId": { 97 | "type": "string", 98 | "description": "Primary key of a branch", 99 | "format": "int64", 100 | "required": true, 101 | "location": "path" 102 | } 103 | }, 104 | "parameterOrder": ["branchId"] 105 | }, 106 | "insert": { 107 | "id": "bank.branches.insert", 108 | "path": "branches", 109 | "request": { 110 | "$ref": "Branch" 111 | }, 112 | "httpMethod": "POST", 113 | "description": "Inserts a branch", 114 | "parameters": { 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/integration/transformer_test/lib/bank_api_test.streamy.yaml: -------------------------------------------------------------------------------- 1 | discovery: bank_api_test.json 2 | output: 3 | files: split 4 | prefix: bank_api_client 5 | base: 6 | class: Entity 7 | import: package:streamy/base.dart 8 | backing: map 9 | options: 10 | clone: true 11 | removers: true 12 | known: false -------------------------------------------------------------------------------- /test/integration/transformer_test/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: transformer_test 2 | version: 0.0.1 3 | description: > 4 | Tests Streamy transformers. 5 | dependencies: 6 | streamy: 7 | path: ../../.. 8 | transformers: 9 | - streamy 10 | -------------------------------------------------------------------------------- /test/mixins/dot_access_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.mixins.dot_access.test; 2 | 3 | import 'package:streamy/raw_entity.dart'; 4 | import 'package:unittest/unittest.dart'; 5 | 6 | main() { 7 | group('DotAccess', () { 8 | test('map can contain keys with dots', () { 9 | var e = raw({'foo': raw({ 10 | 'bar': 'baz', 11 | })}); 12 | expect(e.containsKey('foo.bar'), isTrue); 13 | expect(e['foo.bar'], equals('baz')); 14 | }); 15 | test('removes lowest-most element in the path', () { 16 | var e = raw({'foo': raw({ 17 | 'bar': 'baz', 18 | 'cruft': 1, 19 | })}); 20 | expect(e['foo'].keys, hasLength(2)); 21 | e.remove('foo.bar'); 22 | expect(e['foo'].keys, hasLength(1)); 23 | expect(e['foo.cruft'], 1); 24 | }); 25 | }); 26 | } 27 | 28 | RawEntity raw(Map map) => (new RawEntity.wrap(map)..freeze()).clone(); 29 | -------------------------------------------------------------------------------- /test/mixins/immutable_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.mixins.immutable.test; 2 | 3 | import 'package:streamy/raw_entity.dart'; 4 | import 'package:observe/observe.dart'; 5 | import 'package:fixnum/fixnum.dart'; 6 | import 'package:unittest/unittest.dart'; 7 | 8 | main() { 9 | group('Immutable mixin', () { 10 | test('should be unfrozen by default', () { 11 | var entity = raw({ 12 | 'foo': 1, 13 | 'bar': 'baz', 14 | 'qux': false, 15 | 'quux': null, 16 | 'garply': new Int64(123), 17 | 'waldo': 3.14, 18 | }); 19 | expect(entity.isFrozen, isFalse); 20 | entity['foo'] = 2; 21 | expect(entity['foo'], 2); 22 | }); 23 | test('should throw on mutations when frozen', () { 24 | var entity = raw({'foo': 1}); 25 | 26 | entity.freeze(); 27 | 28 | expect(entity.isFrozen, isTrue); 29 | expect(() { 30 | entity['foo'] = 2; 31 | }, throwsUnsupportedError); 32 | expect(entity['foo'], 1); 33 | }); 34 | test('should freeze nested entities', () { 35 | var foo = raw({'bar': 1}); 36 | var entity = raw({'foo': foo}); 37 | 38 | entity.freeze(); 39 | 40 | expect(entity.isFrozen, isTrue); 41 | expect(foo.isFrozen, isTrue); 42 | expect(() { 43 | foo['bar'] = 2; 44 | }, throwsUnsupportedError); 45 | }); 46 | test('should freeze nested lists of entities', () { 47 | var foo = raw({'bar': 1}); 48 | var entity = raw({'foos': olist([foo])}); 49 | entity.freeze(); 50 | 51 | expect(() { 52 | entity['foos'][0] = raw({'baz': 3}); 53 | }, throwsUnsupportedError, reason: 'the list must be frozen'); 54 | 55 | expect(() { 56 | foo['bar'] = 2; 57 | }, throwsUnsupportedError, reason: 'entities in the list must be frozen'); 58 | }); 59 | test('should freeze lists of lists', () { 60 | var entity = raw({'list': olist([olist([olist([1])])])}); 61 | entity.freeze(); 62 | 63 | expect(() { 64 | entity['list'][0] = []; 65 | }, throwsUnsupportedError, reason: 'the list must be frozen'); 66 | expect(() { 67 | entity['list'][0][0] = []; 68 | }, throwsUnsupportedError, reason: 'the list must be frozen'); 69 | expect(() { 70 | entity['list'][0][0][0] = []; 71 | }, throwsUnsupportedError, reason: 'the list must be frozen'); 72 | }); 73 | }); 74 | } 75 | 76 | RawEntity raw(Map map) => new RawEntity.wrap(map); 77 | ObservableList olist(List list) => new ObservableList.from(list); 78 | -------------------------------------------------------------------------------- /test/mixins/is_map_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.mixins.is_map.test; 2 | 3 | import 'package:streamy/raw_entity.dart'; 4 | import 'package:unittest/unittest.dart'; 5 | 6 | main() { 7 | group('IsMap', () { 8 | test('should implement DynamicAccess', () { 9 | var entity = raw({ 10 | 'a': 1, 11 | 'b': 'c', 12 | }); 13 | expect(entity.keys, ['a', 'b']); 14 | expect(entity.containsKey('a'), isTrue); 15 | expect(entity.containsKey('z'), isFalse); 16 | expect(entity['a'], 1); 17 | expect(entity['b'], 'c'); 18 | entity.remove('a'); 19 | expect(entity.containsKey('a'), isFalse); 20 | }); 21 | 22 | test('.containsValue should work', () { 23 | var entity = raw({'a': 1}); 24 | expect(entity.containsValue(1), isTrue); 25 | expect(entity.containsValue(2), isFalse); 26 | }); 27 | 28 | test('.putIfAbsent should NOT put if present', () { 29 | var entity = raw({'a': 1}); 30 | entity.putIfAbsent('a', () { 31 | fail('Should not have been called'); 32 | }); 33 | expect(entity['a'], 1); 34 | }); 35 | 36 | test('.putIfAbsent should return original value if present', () { 37 | var entity = raw({'a': 1}); 38 | expect(entity.putIfAbsent('a', () { 39 | fail('Should not have been called'); 40 | }), 1); 41 | }); 42 | 43 | test('.putIfAbsent should return new value if NOT present', () { 44 | var entity = raw({}); 45 | expect(entity.putIfAbsent('a', () => 1), 1); 46 | }); 47 | 48 | test('.putIfAbsent should put if absent', () { 49 | var entity = raw({}); 50 | entity.putIfAbsent('a', () => 1); 51 | expect(entity['a'], 1); 52 | }); 53 | 54 | test('.addAll should add all', () { 55 | var entity = raw({'a': 1, 'b': 2}); 56 | entity.addAll({'b': 3, 'c': 4}); 57 | expect(entity, hasLength(3)); 58 | expect(entity['a'], 1); 59 | expect(entity['b'], 3); 60 | expect(entity['c'], 4); 61 | }); 62 | 63 | test('.clear should clear', () { 64 | var entity = raw({'a': 1, 'b': 2}) 65 | ..clear(); 66 | expect(entity, hasLength(0)); 67 | }); 68 | 69 | test('.forEach should call back for each pair', () { 70 | var pairs = {}; 71 | raw({'a': 1, 'b': 2}).forEach((k, v) { 72 | pairs[k] = v; 73 | }); 74 | expect(pairs, {'a': 1, 'b': 2}); 75 | }); 76 | 77 | test('.values should return values', () { 78 | expect(raw({'a': 1, 'b': 2}).values, [1, 2]); 79 | }); 80 | 81 | test('.length should return length', () { 82 | expect(raw({'a': 1, 'b': 2}).length, 2); 83 | }); 84 | 85 | test('.isEmpty should work', () { 86 | expect(raw({}).isEmpty, isTrue); 87 | expect(raw({'a': 1}).isEmpty, isFalse); 88 | }); 89 | 90 | test('.isNotEmpty should work', () { 91 | expect(raw({}).isNotEmpty, isFalse); 92 | expect(raw({'a': 1}).isNotEmpty, isTrue); 93 | }); 94 | }); 95 | } 96 | 97 | RawEntity raw(Map map) => (new RawEntity.wrap(map)..freeze()).clone(); 98 | -------------------------------------------------------------------------------- /test/mixins/patch_map_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.mixins.patch.test; 2 | 3 | import 'dart:convert' show JSON; 4 | import 'package:streamy/raw_entity.dart'; 5 | import 'package:streamy/base.dart'; 6 | import 'package:streamy/streamy.dart'; 7 | import 'package:unittest/unittest.dart'; 8 | import '../utils.dart' show RawEntity; 9 | 10 | main() { 11 | group('Patch', () { 12 | 13 | test('contains only updated properties', () { 14 | // Create original 15 | var entity = raw({ 16 | 'foo': 0, 17 | 'bar': 2, 18 | }); 19 | 20 | // Make updates 21 | entity['foo'] = 1; // change 22 | entity['baz'] = 3; // new property 23 | 24 | // Patch and verify 25 | var patch = entity.patch(); 26 | expect(patch.keys, hasLength(2)); 27 | expect(patch.keys, contains('foo')); 28 | expect(patch.keys, contains('baz')); 29 | }); 30 | 31 | test('patches EXISTING nested entity', () { 32 | // Create original 33 | var entity = raw({ 34 | 'foo': raw({ 35 | 'bar': 1, 36 | 'baz': 2, 37 | }), 38 | }); 39 | 40 | // Make updates 41 | entity['foo']['bar'] = 2; 42 | 43 | // Patch and verify 44 | var patch = entity.patch(); 45 | var expected = raw({ 46 | 'foo': raw({ 47 | 'bar': 2, 48 | // 'baz' should disappear due to patch 49 | }), 50 | }); 51 | expect(EntityUtils.deepEquals(patch, expected), isTrue, 52 | reason: '${getMap(patch)} should be equal to ${getMap(expected)}'); 53 | }); 54 | 55 | test('should NOT patch NEW nested entity', () { 56 | // Create original 57 | var entity = raw({}); 58 | 59 | // Make updates 60 | entity['foo'] = raw({ 61 | 'bar': 1, 62 | }); 63 | 64 | // Patch and verify 65 | var patch = entity.patch(); 66 | var expected = raw({ 67 | 'foo': raw({ 68 | // 'bar' should remain because we're not patching 69 | 'bar': 1, 70 | }), 71 | }); 72 | expect(EntityUtils.deepEquals(patch, expected), isTrue, 73 | reason: '${getMap(patch)} should be ' 74 | 'equal to ${getMap(expected)}'); 75 | }); 76 | }); 77 | } 78 | 79 | RawEntity raw(Map map) => (new RawEntity.wrap(map)..freeze()).clone(); 80 | -------------------------------------------------------------------------------- /test/project.dart: -------------------------------------------------------------------------------- 1 | // Miscellaneous project-related utilities 2 | 3 | import 'package:args/args.dart'; 4 | 5 | const STREAMY_PROJECT_ROOT_OPTION = 'streamy_project_root'; 6 | final _TRAILING_SLASHES = new RegExp(r"/+$"); 7 | 8 | // Path to the root of the Streamy project files. 9 | String projectRootDir(List arguments) { 10 | var argp = new ArgParser() 11 | ..addOption( 12 | STREAMY_PROJECT_ROOT_OPTION, 13 | help: 'Path to the root of the Streamy project files.'); 14 | var args = argp.parse(arguments); 15 | if (args[STREAMY_PROJECT_ROOT_OPTION] != null) { 16 | return args[STREAMY_PROJECT_ROOT_OPTION].replaceAll(_TRAILING_SLASHES, ""); 17 | } else { 18 | return '.'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/proto_tests.dart: -------------------------------------------------------------------------------- 1 | library streamy.test; 2 | 3 | import 'generated/proto_test.dart' as generated_proto_test; 4 | import 'runtime/marshaller_test.dart' as runtime_marshaller_test; 5 | 6 | main(List args) { 7 | generated_proto_test.main(); 8 | runtime_marshaller_test.main(); 9 | } 10 | -------------------------------------------------------------------------------- /test/runtime/branching_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.runtime.branching.test; 2 | 3 | import 'package:streamy/streamy.dart'; 4 | import 'package:streamy/testing/testing.dart'; 5 | import 'package:unittest/unittest.dart'; 6 | import '../utils.dart'; 7 | 8 | main() { 9 | group('BranchingRequestHandler', () { 10 | var defaultHandler; 11 | var testHandler; 12 | var testHandler2; 13 | setUp(() { 14 | defaultHandler = ( 15 | testRequestHandler() 16 | ..value(new Response(makeEntity()..['value'] = 'hello', Source.RPC, 0)) 17 | ).build(); 18 | testHandler = ( 19 | testRequestHandler() 20 | ..value(new Response(makeEntity()..['value'] = 'world', Source.RPC, 0)) 21 | ).build(); 22 | testHandler2 = ( 23 | testRequestHandler() 24 | ..value(new Response(makeEntity()..['value'] = 'universe', Source.RPC, 0)) 25 | ).build(); 26 | }); 27 | test('Properly branches on one type of request.', () { 28 | var brancher = (new BranchingRequestHandlerBuilder() 29 | ..addBranch(TypedTestRequest, testHandler) 30 | ).build(defaultHandler); 31 | brancher.handle(TEST_GET_REQUEST, const NoopTrace()).single.then(expectAsync((response) { 32 | expect(response.entity['value'], equals('hello')); 33 | })); 34 | brancher.handle(new TypedTestRequest(true), const NoopTrace()).single.then(expectAsync((response) { 35 | expect(response.entity['value'], equals('world')); 36 | })); 37 | }); 38 | test('Predicate works to disable branch.', () { 39 | var brancher = (new BranchingRequestHandlerBuilder() 40 | ..addBranch(TypedTestRequest, testHandler, predicate: (_) => false) 41 | ).build(defaultHandler); 42 | brancher.handle(new TypedTestRequest(true), const NoopTrace()).single.then(expectAsync((response) { 43 | expect(response.entity['value'], equals('hello')); 44 | })); 45 | }); 46 | test('Multiple branches of same type, with different predicates.', () { 47 | var brancher = (new BranchingRequestHandlerBuilder() 48 | ..addBranch(TypedTestRequest, testHandler, predicate: (r) => r.option) 49 | ..addBranch(TypedTestRequest, testHandler2, predicate: (r) => !r.option) 50 | ).build(defaultHandler); 51 | brancher.handle(new TypedTestRequest(true), const NoopTrace()).single.then(expectAsync((response) { 52 | expect(response.entity['value'], equals('world')); 53 | })); 54 | brancher.handle(new TypedTestRequest(false), const NoopTrace()).single.then(expectAsync((response) { 55 | expect(response.entity['value'], equals('universe')); 56 | })); 57 | brancher.handle(TEST_GET_REQUEST, const NoopTrace()).single.then(expectAsync((response) { 58 | expect(response.entity['value'], equals('hello')); 59 | })); 60 | }); 61 | test('Multiple branches of different types.', () { 62 | var brancher = (new BranchingRequestHandlerBuilder() 63 | ..addBranch(TypedTestRequest, testHandler) 64 | ..addBranch(DifferentlyTypedTestRequest, testHandler2) 65 | ).build(defaultHandler); 66 | brancher.handle(new TypedTestRequest(true), const NoopTrace()).single.then(expectAsync((response) { 67 | expect(response.entity['value'], equals('world')); 68 | })); 69 | brancher.handle(new DifferentlyTypedTestRequest(), const NoopTrace()).single.then(expectAsync((response) { 70 | expect(response.entity['value'], equals('universe')); 71 | })); 72 | brancher.handle(TEST_GET_REQUEST, const NoopTrace()).single.then(expectAsync((response) { 73 | expect(response.entity['value'], equals('hello')); 74 | })); 75 | }); 76 | }); 77 | } 78 | 79 | class TypedTestRequest extends TestRequest { 80 | final bool option; 81 | 82 | TypedTestRequest(this.option) : super("GET"); 83 | } 84 | 85 | class DifferentlyTypedTestRequest extends TestRequest { 86 | DifferentlyTypedTestRequest() : super("GET"); 87 | } 88 | -------------------------------------------------------------------------------- /test/runtime/error_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.runtime.error.test; 2 | 3 | import 'dart:async'; 4 | import 'package:streamy/mixins/base_map.dart'; 5 | import 'package:streamy/streamy.dart'; 6 | import 'package:streamy/testing/testing.dart'; 7 | import 'package:unittest/unittest.dart'; 8 | import '../utils.dart'; 9 | 10 | main() { 11 | group('RetryingRequestHandler', () { 12 | test('retries immediately on single 503 error', () { 13 | var testHandler = ( 14 | testRequestHandler() 15 | ..rpcError(503) 16 | ..value(new Response(makeEntity(), Source.RPC, 0)) 17 | ).build(); 18 | var subject = new RetryingRequestHandler(testHandler); 19 | subject.handle(TEST_GET_REQUEST, const NoopTrace()).first.then(expectAsync((res) { 20 | expect(res.entity, new isInstanceOf()); 21 | })); 22 | }); 23 | test("doesn't retry on 404 error", () { 24 | var testHandler = ( 25 | testRequestHandler() 26 | ..rpcError(404) 27 | ).build(); 28 | // Expect the retry strategy to not be called. 29 | var subject = new RetryingRequestHandler(testHandler, strategy: expectAsync3((a, b, c) {}, count: 0)); 30 | subject.handle(TEST_GET_REQUEST, const NoopTrace()).first.catchError(expectAsync((e) { 31 | expect(e, new isInstanceOf()); 32 | })); 33 | }); 34 | test('retries the maximum number of times', () { 35 | var testHandler = ( 36 | testRequestHandler() 37 | ..rpcError(503, times: 3) 38 | ..value(new Response(makeEntity(), Source.RPC, 0)) 39 | ).build(); 40 | 41 | int retryCount = 0; 42 | Future testStrategy(Request request, int retryNum, e) { 43 | expect(e.httpStatus, equals(503)); 44 | expect(retryNum, equals(++retryCount)); 45 | return new Future.value(true); 46 | } 47 | 48 | var subject = new RetryingRequestHandler(testHandler, strategy: expectAsync3(testStrategy, count: 3, max: 3)); 49 | subject.handle(TEST_GET_REQUEST, const NoopTrace()).first.then(expectAsync((res) { 50 | expect(res.entity, new isInstanceOf()); 51 | })); 52 | }); 53 | test("doesn't retry past the maximum number of times", () { 54 | var testHandler = ( 55 | testRequestHandler() 56 | ..rpcError(503, times: 4) 57 | ..value(new Response(makeEntity(), Source.RPC, 0)) 58 | ).build(); 59 | 60 | int retryCount = 0; 61 | Future testStrategy(Request request, int retryNum, e) { 62 | expect(e.httpStatus, equals(503)); 63 | expect(retryNum, equals(++retryCount)); 64 | return new Future.value(true); 65 | } 66 | 67 | var subject = new RetryingRequestHandler(testHandler, maxRetries: 3, strategy: expectAsync3(testStrategy, count: 3, max: 3)); 68 | subject.handle(TEST_GET_REQUEST, const NoopTrace()).first.catchError(expectAsync((e) { 69 | expect(e, new isInstanceOf()); 70 | })); 71 | }); 72 | }); 73 | } 74 | 75 | expectAsync3(fn, {count: 1, max: 0}) { 76 | var tracker = expectAsync(() {}, count: count, max: max); 77 | return (a, b, c) { 78 | tracker(); 79 | return fn(a, b, c); 80 | }; 81 | } -------------------------------------------------------------------------------- /test/runtime/http_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.runtime.http.test; 2 | 3 | import 'package:streamy/streamy.dart'; 4 | import 'package:unittest/unittest.dart'; 5 | 6 | var SIMPLE_RESPONSE = [ 7 | 'Content-Type: application/http', 8 | '', 9 | 'HTTP/1.1 200 OK', 10 | 'Host: google.com', 11 | 'Content-Type: text/plain; charset=utf-8', 12 | 'Content-Length: 12', 13 | '', 14 | 'Hello World!', 15 | '' 16 | ].join('\r\n'); 17 | var SIMPLE_RESPONSE_2 = [ 18 | 'Content-Type: application/http', 19 | '', 20 | 'HTTP/1.1 200 OK', 21 | 'Host: api.google.com', 22 | 'Content-Type: text/html; charset=utf-8', 23 | 'Content-Length: 11', 24 | '', 25 | 'Hello Moon!', 26 | '' 27 | ].join('\r\n'); 28 | var SIMPLE_RESPONSE_3 = [ 29 | 'Content-Type: application/http', 30 | '', 31 | 'HTTP/1.1 201 Created', 32 | 'Host: client6.google.com', 33 | 'Content-Type: application/json; charset=utf-8', 34 | 'Content-Length: 10', 35 | '', 36 | 'Hello Sun!', 37 | '' 38 | ].join('\r\n'); 39 | var MULTIPART_RESPONSE = [ 40 | 'HTTP/1.1 200 OK', 41 | 'Host: google.com', 42 | 'Content-Type: multipart/mixed; boundary=ABCDEFG', 43 | '', 44 | '--ABCDEFG', 45 | SIMPLE_RESPONSE, 46 | '--ABCDEFG', 47 | SIMPLE_RESPONSE_2, 48 | '--ABCDEFG', 49 | SIMPLE_RESPONSE_3 50 | ].join('\r\n'); 51 | 52 | main() { 53 | group('Http', () { 54 | group('Request', () { 55 | test('Simple get', () { 56 | var req = new StreamyHttpRequest('/test/url', 'GET', 57 | {'Host': 'google.com', 'Accept-Encoding': 'utf-8'}, {}, null); 58 | expect(req.toString(), equals( 59 | '''GET /test/url HTTP/1.1 60 | host: google.com 61 | accept-encoding: utf-8 62 | 63 | '''.replaceAll('\n', '\r\n'))); 64 | }); 65 | test('Multipart request', () { 66 | var req1 = new StreamyHttpRequest('/test/url', 'GET', 67 | {'Host': 'google.com', 'Accept-Encoding': 'utf-8'}, {}, null); 68 | var req2 = new StreamyHttpRequest('/test/another/url', 'POST', 69 | { 70 | 'Host': 'api.google.com', 71 | 'Content-Type': 'text/plain; charset=utf-8', 72 | }, 73 | {}, null, payload: 'Hello world!'); 74 | var req3 = new StreamyHttpRequest('/a/third/url', 'POST', 75 | {'Host': 'google.com', 'Accept-Encoding': 'utf-8'}, 76 | {}, null, payload: 'Goodbye world!'); 77 | var mpReq = new StreamyHttpRequest.multipart('/multipart/url', 'PUT', 78 | {'Host': 'multipart.google.com'}, null, [req1, req2, req3]); 79 | var cType = mpReq.headers['content-type']; 80 | var boundary = cType.split('=')[1]; 81 | expect(cType.startsWith('multipart/mixed; boundary='), isTrue); 82 | expect(mpReq.headers['host'], equals('multipart.google.com')); 83 | expect(mpReq.payload, equals( 84 | '''--$boundary 85 | Content-Type: application/http 86 | Content-Transfer-Encoding: binary 87 | 88 | GET /test/url HTTP/1.1 89 | host: google.com 90 | accept-encoding: utf-8 91 | 92 | --$boundary 93 | Content-Type: application/http 94 | Content-Transfer-Encoding: binary 95 | 96 | POST /test/another/url HTTP/1.1 97 | host: api.google.com 98 | content-type: text/plain; charset=utf-8 99 | content-length: 12 100 | 101 | Hello world! 102 | --$boundary 103 | Content-Type: application/http 104 | Content-Transfer-Encoding: binary 105 | 106 | POST /a/third/url HTTP/1.1 107 | host: google.com 108 | accept-encoding: utf-8 109 | content-length: 14 110 | 111 | Goodbye world! 112 | --$boundary-- 113 | '''.replaceAll('\n', '\r\n'))); 114 | }); 115 | }); 116 | group('Response', () { 117 | test('Simple response', () { 118 | var hr = StreamyHttpResponse.parse(SIMPLE_RESPONSE); 119 | expect(hr.statusCode, equals(200)); 120 | expect(hr.headers.length, equals(3)); 121 | expect(hr.headers['host'], equals('google.com')); 122 | expect(hr.headers['content-type'], equals('text/plain; charset=utf-8')); 123 | expect(hr.headers['content-length'], equals('12')); 124 | expect(hr.body, equals('Hello World!')); 125 | }); 126 | test('Multipart response', () { 127 | var hr = StreamyHttpResponse.parse(MULTIPART_RESPONSE); 128 | var parts = hr.splitMultipart(); 129 | expect(parts[0].body, equals('Hello World!')); 130 | expect(parts[1].body, equals('Hello Moon!')); 131 | expect(parts[2].body, equals('Hello Sun!')); 132 | }); 133 | }); 134 | }); 135 | } 136 | -------------------------------------------------------------------------------- /test/runtime/json_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.runtime.json.test; 2 | 3 | import 'package:fixnum/fixnum.dart'; 4 | import 'package:streamy/streamy.dart'; 5 | import 'package:unittest/unittest.dart'; 6 | 7 | main() { 8 | group('json', () { 9 | test('should send trace events', () { 10 | final trace = new LoggingTrace(); 11 | jsonParse('{}', trace); 12 | expect(trace.log, hasLength(2)); 13 | expect(trace.log[0].runtimeType, JsonParseStartEvent); 14 | expect(trace.log[1].runtimeType, JsonParseEndEvent); 15 | }); 16 | }); 17 | 18 | group('jsonMarshal', () { 19 | test('should toString Int64s', () { 20 | expect(jsonMarshal({'foo': new Int64(1)}), {'foo': '1'}); 21 | }); 22 | test('should not toString nums', () { 23 | expect(jsonMarshal({'i': 1, 'd': 1.0}), {'i': 1, 'd': 1.0}); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/runtime/marshaller_test.dart: -------------------------------------------------------------------------------- 1 | library runtime_marshaller_test; 2 | 3 | import 'dart:convert'; 4 | import 'package:unittest/unittest.dart'; 5 | import '../generated/proto_client.dart'; 6 | 7 | main() { 8 | group('Marshaller', () { 9 | final marshaller = new Marshaller(); 10 | Foo entity; 11 | setUp(() { 12 | entity = new Foo(); 13 | }); 14 | test('should serialize nulls as JSON nulls', () { 15 | entity.name = null; 16 | expect(JSON.encode(marshaller.marshalFoo(entity)), 17 | '{"2":null}'); 18 | }); 19 | test('.local does not affect serialization of the entity', () { 20 | var s1 = JSON.encode(marshaller.marshalFoo(entity)); 21 | entity.local['foo'] = 'not serialized'; 22 | var s2 = JSON.encode(marshaller.marshalFoo(entity)); 23 | expect(s2, equals(s1)); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/runtime/multiplexer_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.runtime.multiplexer.test; 2 | 3 | import 'dart:async'; 4 | import 'package:streamy/streamy.dart'; 5 | import 'package:streamy/testing/testing.dart'; 6 | import 'package:streamy/mixins/base_map.dart'; 7 | import 'package:unittest/unittest.dart'; 8 | import '../utils.dart'; 9 | 10 | main() { 11 | group('Multiplexer', () { 12 | test('does not throw on error but forward to error catchers', () { 13 | var testHandler = ( 14 | testRequestHandler() 15 | ..rpcError(404) 16 | ).build(); 17 | var subject = new MultiplexingRequestHandler(testHandler); 18 | subject 19 | .handle(TEST_GET_REQUEST, const NoopTrace()) 20 | .first 21 | .catchError(expectAsync((err) { 22 | expect(err, new isInstanceOf()); 23 | expect(err.httpStatus, 404); 24 | })); 25 | }); 26 | test('sends new value across request bounds', () { 27 | var r1 = makeEntity() 28 | ..['key'] = 'alpha'; 29 | var r2 = makeEntity() 30 | ..['key'] = 'beta'; 31 | var testHandler = ( 32 | testRequestHandler() 33 | ..value(new Response(r1, Source.RPC, 0)) 34 | ..value(new Response(r2, Source.RPC, 1)) 35 | ).build(); 36 | var subject = new MultiplexingRequestHandler(testHandler); 37 | var stream = subject 38 | .handle(TEST_GET_REQUEST, const NoopTrace()) 39 | .map((e) => e.entity) 40 | .asBroadcastStream(); 41 | stream.first.then(expectAsync((e) { 42 | expect(e['key'], 'alpha'); 43 | subject 44 | .handle(TEST_GET_REQUEST, const NoopTrace()) 45 | .map((e) => e.entity) 46 | .first 47 | .then(expectAsync((e) { 48 | expect(e['key'], 'beta'); 49 | })); 50 | }, count: 1)); 51 | stream.skip(1).first.then(expectAsync((e) { 52 | expect(e['key'], 'beta'); 53 | }, count: 1)); 54 | }); 55 | test('properly forwards a cancellation', () { 56 | // Expect onCancel to be called. 57 | var sink = new StreamController(onCancel: expectAsync(() {}, count: 1)); 58 | var testHandler = (testRequestHandler()..stream(sink.stream)).build(); 59 | var subject = new MultiplexingRequestHandler(testHandler); 60 | subject 61 | .handle(TEST_GET_REQUEST, const NoopTrace()) 62 | .listen(expectAsync((_) {}, count: 0)) 63 | .cancel(); 64 | }); 65 | test('demotes primary responses to secondary before first response', () { 66 | var s1 = new StreamController(); 67 | var s2 = new StreamController(); 68 | var testHandler = ( 69 | testRequestHandler() 70 | ..stream(s1.stream) 71 | ..stream(s2.stream) 72 | ).build(); 73 | var subject = new MultiplexingRequestHandler(testHandler); 74 | 75 | // First stream used to assert test conditions. 76 | var stream = subject 77 | .handle(TEST_GET_REQUEST, const NoopTrace()) 78 | .asBroadcastStream(); 79 | 80 | // Crossover response from [s2] considered SECONDARY. 81 | stream.first.then(expectAsync((r) { 82 | expect(r.authority, Authority.SECONDARY); 83 | expect(r.entity['key'], 'bar'); 84 | s1.add(new Response(makeEntity()..['key'] = 'foo', Source.RPC, 0)); 85 | })); 86 | 87 | // Primary response from [s1] considered PRIMARY. 88 | stream.skip(1).first.then(expectAsync((r) { 89 | expect(r.authority, Authority.PRIMARY); 90 | expect(r.entity['key'], 'foo'); 91 | s2.add(new Response(makeEntity()..['key'] = 'baz', Source.RPC, 0)); 92 | })); 93 | 94 | // Crossover response from [s2] now considered PRIMARY. 95 | stream.skip(2).first.then(expectAsync((r) { 96 | expect(r.authority, Authority.PRIMARY); 97 | expect(r.entity['key'], 'baz'); 98 | })); 99 | 100 | // Second request used to trigger test conditions (and verify that 101 | // the response is still considered PRIMARY. 102 | var second = subject 103 | .handle(TEST_GET_REQUEST, const NoopTrace()) 104 | .asBroadcastStream(); 105 | 106 | // Needed to ensure this listener stays active in the 107 | // [MultiplexingRequestHandler] so values sent to it will be echoed 108 | // in [stream]. 109 | second.drain(); 110 | second 111 | .first 112 | .then(expectAsync((r) { 113 | expect(r.authority, Authority.PRIMARY); 114 | expect(r.entity['key'], 'bar'); 115 | })); 116 | 117 | s2.add(new Response(makeEntity()..['key'] = 'bar', 118 | Source.RPC, 0, authority: Authority.PRIMARY)); 119 | }); 120 | }); 121 | } 122 | -------------------------------------------------------------------------------- /test/runtime/request_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.runtime.request.test; 2 | 3 | import 'package:unittest/unittest.dart'; 4 | import 'package:streamy/streamy.dart'; 5 | 6 | class RequestWithQueryParams extends HttpRequest { 7 | 8 | RequestWithQueryParams() : super(null); 9 | Request clone() => null; 10 | bool get hasPayload => false; 11 | String get httpMethod => null; 12 | get responseDeserializer => null; 13 | 14 | String get pathFormat => "/test"; 15 | List get pathParameters => []; 16 | List get queryParameters => ["foo"]; 17 | } 18 | 19 | class RequestWithPathParams extends HttpRequest { 20 | 21 | RequestWithPathParams() : super(null); 22 | Request clone() => null; 23 | bool get hasPayload => false; 24 | String get httpMethod => null; 25 | get responseDeserializer => null; 26 | 27 | String get pathFormat => "/test/{bar}"; 28 | List get pathParameters => ["bar"]; 29 | List get queryParameters => []; 30 | } 31 | 32 | class RequestWithReservedExpansionPathParam extends HttpRequest { 33 | 34 | RequestWithReservedExpansionPathParam() : super(null); 35 | Request clone() => null; 36 | bool get hasPayload => false; 37 | String get httpMethod => null; 38 | get responseDeserializer => null; 39 | 40 | String get pathFormat => "/test/{+bar}"; 41 | List get pathParameters => ["bar"]; 42 | List get queryParameters => []; 43 | } 44 | 45 | main() { 46 | group("Request", () { 47 | test("should escape query parameters", () { 48 | var req = new RequestWithQueryParams() 49 | ..parameters["foo"] = "a@b c& d"; 50 | expect(req.path, "/test?foo=a%40b+c%26+d"); // query component encoding 51 | }); 52 | test("should escape path parameters", () { 53 | var req = new RequestWithPathParams() 54 | ..parameters["bar"] = "a@b c& d"; 55 | expect(req.path, "/test/a%40b%20c%26%20d"); // component encoding 56 | }); 57 | test("adds local parameters properly", () { 58 | var req = new RequestWithQueryParams() 59 | ..parameters["foo"] = "test1" 60 | ..localParameters["baz"] = 'test2'; 61 | expect(req.path, "/test?foo=test1&baz=test2"); 62 | }); 63 | test("should allow slashes in Reserved Expansion path parameters", () { 64 | var req = new RequestWithReservedExpansionPathParam() 65 | ..parameters["bar"] = "a@b/c&d"; 66 | expect(req.path, "/test/a%40b/c%26d"); // slashes allowed 67 | }); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /test/runtime/transaction_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.runtime.transaction.test; 2 | 3 | import 'dart:async'; 4 | import 'package:fixnum/fixnum.dart'; 5 | import 'package:streamy/streamy.dart'; 6 | import 'package:streamy/testing/testing.dart'; 7 | import 'package:unittest/unittest.dart'; 8 | import '../generated/bank_api_client.dart'; 9 | import '../generated/bank_api_client_objects.dart'; 10 | 11 | main() { 12 | group('Transaction', () { 13 | TestTxnStrategy txStrategy; 14 | Bank root; 15 | 16 | setUp(() { 17 | txStrategy = new TestTxnStrategy(); 18 | root = new Bank(null, txStrategy: txStrategy); 19 | }); 20 | 21 | test('should be created by the root object', () { 22 | var tx = root.beginTransaction(); 23 | expect(tx, isNotNull); 24 | }); 25 | 26 | test('should receive requests from the transactional root', () { 27 | var tx = root.beginTransaction(); 28 | TestTxn tximpl = txStrategy.lastTransaction; 29 | expect(tximpl.requests, hasLength(0)); 30 | tx.send(TEST_GET_REQUEST); 31 | expect(tximpl.requests, hasLength(1)); 32 | expect(tximpl.requests[0], same(TEST_GET_REQUEST)); 33 | }); 34 | 35 | test('should commit when root commits', () { 36 | var tx = root.beginTransaction(); 37 | TestTxn tximpl = txStrategy.lastTransaction; 38 | tx.commit(); 39 | expect(tximpl.committed, isTrue); 40 | }); 41 | 42 | test('should pass end-to-end test', () { 43 | var tx = root.beginTransaction(); 44 | TestTxn tximpl = txStrategy.lastTransaction; 45 | expect(tximpl.requests, hasLength(0)); 46 | var req = tx.branches.insert(new Branch()..id = new Int64(123)); 47 | var resp = new Response(null, 'RPC', 0); 48 | fakeResponse[req] = resp; 49 | 50 | Response actual; 51 | req.sendRaw().listen((r) { actual = r; }); 52 | 53 | // Before we commit we expect requests to be accumulated 54 | expect(tximpl.requests, hasLength(1)); 55 | expect(tximpl.requests[0], same(req)); 56 | 57 | // After we commit the requests should be flushed and response received 58 | tx.commit(); 59 | expect(tximpl.requests, hasLength(0)); 60 | expect(actual, isNotNull); 61 | expect(actual, same(resp)); 62 | }); 63 | }); 64 | } 65 | 66 | class TestTxnStrategy implements TransactionStrategy { 67 | TestTxn lastTransaction; 68 | Transaction beginTransaction() { 69 | return lastTransaction = new TestTxn(); 70 | } 71 | } 72 | 73 | final fakeResponse = new Expando(); 74 | final committer = new Expando(); 75 | 76 | class TestTxn implements Transaction { 77 | bool committed = false; 78 | List requests = []; 79 | 80 | Future commit() { 81 | for (var request in requests) { 82 | committer[request](); 83 | } 84 | requests = []; 85 | committed = true; 86 | return null; 87 | } 88 | 89 | Stream send(Request request) { 90 | requests.add(request); 91 | var response = new StreamController(sync: true); 92 | committer[request] = () { 93 | response.add(fakeResponse[request]); 94 | }; 95 | return response.stream; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/runtime/transforms_test.dart: -------------------------------------------------------------------------------- 1 | library streamy.runtime.transforms.test; 2 | 3 | import 'package:unittest/unittest.dart'; 4 | import 'package:streamy/testing/testing.dart'; 5 | import 'package:streamy/raw_entity.dart'; 6 | import 'package:streamy/streamy.dart'; 7 | import '../utils.dart'; 8 | 9 | main() { 10 | group('RequestTrackingTransformer', () { 11 | test('Properly tracks a request', () { 12 | var bareHandler = (testRequestHandler() 13 | ..values([ 14 | new Response(new RawEntity()..['x'] = 'a', Source.RPC, 0), 15 | new Response(new RawEntity()..['x'] = 'b', Source.RPC, 0)])) 16 | .build(); 17 | var handler = bareHandler.transform(() => new UserCallbackTracingTransformer()); 18 | var tracer = new StreamTracer(UserCallbackTracingTransformer.traceDonePredicate); 19 | var root = new TestingRoot(handler, tracer); 20 | 21 | var x = ' '; 22 | var callCount = 0; 23 | tracer.requests.listen(expectAsync((traced) { 24 | expect(traced.request, equals(TEST_GET_REQUEST)); 25 | expect(x, equals(' ')); 26 | x = '_'; 27 | traced.events.where((event) => event is UserCallbackQueuedEvent).listen(expectAsync((_) { 28 | if (callCount == 0) { 29 | expect(x, equals('_')); 30 | } else { 31 | expect(x, equals('a')); 32 | } 33 | }, count: 2)); 34 | traced.events.where((event) => event is UserCallbackDoneEvent).listen(expectAsync((_) { 35 | if (callCount == 1) { 36 | expect(x, equals('a')); 37 | } else { 38 | expect(x, equals('b')); 39 | } 40 | }, count: 2)); 41 | traced.events.last.then(expectAsync((lastEvent) { 42 | expect(lastEvent.runtimeType, RequestOverEvent); 43 | })); 44 | })); 45 | root.send(TEST_GET_REQUEST).listen(expectAsync((response) { 46 | x = response.entity['x']; 47 | callCount++; 48 | }, count: 2)); 49 | }); 50 | test('Properly handles errors', () { 51 | var bareHandler = (testRequestHandler() 52 | ..error(new ArgumentError("test"))) 53 | .build(); 54 | var handler = bareHandler.transform(() => new UserCallbackTracingTransformer()); 55 | var tracer = new StreamTracer(UserCallbackTracingTransformer.traceDonePredicate); 56 | var root = new TestingRoot(handler, tracer); 57 | var sawErrorOnStream = false; 58 | 59 | tracer.requests.listen(expectAsync((traced) { 60 | expect(traced.request, equals(TEST_GET_REQUEST)); 61 | traced.events.where((event) => event is UserCallbackQueuedEvent).listen(expectAsync((_) { 62 | expect(sawErrorOnStream, isFalse); 63 | })); 64 | traced.events.where((event) => event is UserCallbackDoneEvent).listen(expectAsync((_) { 65 | expect(sawErrorOnStream, isTrue); 66 | })); 67 | traced.events.last.then(expectAsync((lastEvent) { 68 | expect(lastEvent.runtimeType, RequestOverEvent); 69 | })); 70 | })); 71 | root.send(TEST_GET_REQUEST).listen(expectAsync((_) {}, count: 0)) 72 | .onError(expectAsync((error) { 73 | expect(error, new isInstanceOf()); 74 | sawErrorOnStream = true; 75 | })); 76 | }); 77 | }); 78 | } -------------------------------------------------------------------------------- /test/test_in_browser.dart: -------------------------------------------------------------------------------- 1 | import 'package:unittest/html_config.dart'; 2 | import 'all_tests.dart' as streamy_tests; 3 | 4 | /// Runs unit tests in the browser. 5 | main() { 6 | useHtmlConfiguration(false); 7 | streamy_tests.main([]); 8 | } 9 | -------------------------------------------------------------------------------- /test/test_in_browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Streamy unit tests in browser 6 | 7 | 8 |

Starting tests...

9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/utils.dart: -------------------------------------------------------------------------------- 1 | /// Test utilities for Streamy itself 2 | library streamy.test.utils; 3 | 4 | import 'dart:async'; 5 | import 'package:unittest/unittest.dart'; 6 | import 'package:streamy/mixins/base_map.dart'; 7 | import 'package:streamy/streamy.dart'; 8 | 9 | final Matcher isType = new isAssignableTo(); 10 | 11 | /// A safer [isInstanceOf]. 12 | class isAssignableTo extends Matcher { 13 | 14 | String _name; 15 | final _delegate = new isInstanceOf(); 16 | 17 | isAssignableTo([name = 'specified type']) { 18 | _name = name; 19 | try { 20 | expect(new Object(), isNot(_delegate)); 21 | } on TestFailure catch(f) { 22 | throw new ArgumentError( 23 | 'Seems like an unsupported type was passed to ' 24 | 'isAssignableTo. Three known possibilities:\n' 25 | ' - You are trying to check Object/dynamic\n' 26 | ' - The type does not exist\n' 27 | ' - The type exists but you forgot to import it'); 28 | } 29 | } 30 | 31 | Description describe(Description description) => 32 | description.add('assignable to ${_name}'); 33 | 34 | bool matches(item, Map matchState) => 35 | _delegate.matches(item, matchState); 36 | } 37 | 38 | /** 39 | * Makes an empty map-backed entity. Useful for testing. 40 | */ 41 | DynamicAccess makeEntity() { 42 | var entity = new MapBase(); 43 | setMap(entity, {}); 44 | return entity; 45 | } 46 | 47 | List _asyncQueue = []; 48 | List _asyncErrors = []; 49 | bool _wrappedAsync = false; 50 | 51 | /** 52 | * Run the async callbacks in the queue. If [runUntilEmpty] is true, then 53 | * run through the whole queue and if new items were added to the queue as the 54 | * result of the callback execution the new callbacks will be automatically 55 | * executed as well. 56 | */ 57 | nextTurn([bool runUntilEmpty = false]) { 58 | if (!_wrappedAsync) { 59 | throw 'You must wrap your test with async(() { ... })'; 60 | } 61 | // copy the queue as it may change. 62 | do { 63 | var toRun = _asyncQueue; 64 | _asyncQueue = []; 65 | toRun.forEach((fn) => fn()); 66 | } while (runUntilEmpty && !_asyncQueue.isEmpty); 67 | } 68 | 69 | /** 70 | * An alias for nextTurn(true). 71 | */ 72 | fastForward() => nextTurn(true); 73 | 74 | /** 75 | * Makes sure there are no outstanding tasks in the queue 76 | */ 77 | expectAsyncQueueIsEmpty() { 78 | expect(_asyncQueue, isEmpty, 79 | reason: 'Async queue must be empty by the end of the test. ' 80 | 'Use nextTurn() or fastForward()'); 81 | } 82 | 83 | /** 84 | * Queues microtasks so they can be run synchronously using [nextTurn] or 85 | * [fastForward]. 86 | */ 87 | async(Function fn) => 88 | () { 89 | if (_wrappedAsync) { 90 | throw 'Cannot double-wrap with async.'; 91 | } 92 | _asyncQueue = []; 93 | _asyncErrors = []; 94 | 95 | _wrappedAsync = true; 96 | try { 97 | _asyncErrors = []; 98 | runZoned(fn, 99 | onError: (e, s) { 100 | _asyncErrors.add([e, s]); 101 | }, 102 | zoneSpecification: new ZoneSpecification( 103 | scheduleMicrotask: (_0, _1, _2, fn) { 104 | _asyncQueue.add(fn); 105 | }, 106 | handleUncaughtError: (_, __, ___, e, s) { 107 | _asyncErrors.add([e, s]); 108 | })); 109 | 110 | _asyncErrors.forEach((e) { 111 | if (e[0] is TestFailure) { 112 | print('Stacktrace: ${e[1]}'); 113 | throw e; 114 | } 115 | throw "During runZoned: $e. Stack:\n${e[1]}"; 116 | }); 117 | } finally { 118 | _wrappedAsync = false; 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /web/README: -------------------------------------------------------------------------------- 1 | This file must exist for pub serve to work. --------------------------------------------------------------------------------