nebula_api_studio 0.1.1 copy "nebula_api_studio: ^0.1.1" to clipboard
nebula_api_studio: ^0.1.1 copied to clipboard

A professional Dart/Flutter package combining a Compiler + Runtime SDK. Convert OpenAPI 3.x / Swagger 2.0 specs into fully type-safe Dart SDKs with smart caching, retry policies, a plugin system, offl [...]

example/main.dart

// ignore_for_file: avoid_print
/// Nebula API Studio — Complete Usage Example
///
/// This file demonstrates the full runtime API:
///   • MockPetStoreAdapter  — zero-network mock implementing [HttpAdapter]
///   • NebulaClient         — constructed with plugins, retry and cache
///   • Plugin chain         — JwtAuthPlugin + LoggingPlugin + AnalyticsPlugin
///   • PetsService          — hand-written service layer (mirrors generated code)
///   • Result<T> monad      — fold, map, when-style, getOrElse
///   • Cache statistics     — via AnalyticsPlugin.snapshot()
///   • Graceful shutdown    — client.dispose()
library;

import 'dart:async';
import 'dart:convert';

import 'package:nebula_api_studio/nebula_api_studio.dart';

// ─────────────────────────────────────────────────────────────────────────────
// Domain models  (normally emitted by `nebula generate`)
// ─────────────────────────────────────────────────────────────────────────────

/// Simulated Pet model — mirrors what the code-generator would produce.
class Pet {
  // ── Constructors ────────────────────────────────────────────────────────────
  const Pet({
    required this.id,
    required this.name,
    required this.category,
    required this.status,
    this.age,
    this.tags = const [],
  });

  factory Pet.fromJson(Map<String, dynamic> j) => Pet(
        id: j['id'] as String,
        name: j['name'] as String,
        category: j['category'] as String,
        status: j['status'] as String,
        age: j['age'] as int?,
        tags: (j['tags'] as List<dynamic>?)?.cast<String>() ?? [],
      );

  // ── Fields ──────────────────────────────────────────────────────────────────
  final String id;
  final String name;
  final String category;
  final String status;
  final int? age;
  final List<String> tags;

  // ── Methods ─────────────────────────────────────────────────────────────────
  Map<String, dynamic> toJson() => {
        'id': id,
        'name': name,
        'category': category,
        'status': status,
        if (age != null) 'age': age,
        'tags': tags,
      };

  @override
  String toString() =>
      'Pet(id: $id, name: $name, category: $category, '
      'status: $status, age: $age, tags: $tags)';
}

// ─────────────────────────────────────────────────────────────────────────────
// Mock HTTP adapter  — returns hard-coded responses, no real network traffic
// ─────────────────────────────────────────────────────────────────────────────

/// Implements [HttpAdapter] with a small in-memory "database" of pets.
/// Replace with [DefaultAdapter] in production.
class MockPetStoreAdapter extends HttpAdapter {
  final _pets = <String, Pet>{
    'pet-001': const Pet(
      id: 'pet-001',
      name: 'Buddy',
      category: 'dog',
      status: 'available',
      age: 3,
      tags: ['friendly', 'trained'],
    ),
    'pet-002': const Pet(
      id: 'pet-002',
      name: 'Whiskers',
      category: 'cat',
      status: 'adopted',
      age: 5,
      tags: ['indoor', 'calm'],
    ),
    'pet-003': const Pet(
      id: 'pet-003',
      name: 'Rio',
      category: 'bird',
      status: 'available',
      age: 2,
      tags: ['talking', 'colorful'],
    ),
  };

  int _requestCount = 0;

  int get requestCount => _requestCount;

  @override
  Future<ApiResponse> send(ApiRequest request) async {
    _requestCount++;
    // Simulate a tiny network latency.
    await Future<void>.delayed(const Duration(milliseconds: 30));

    final path = request.path;
    final method = request.method.toUpperCase();

    // ── GET /pets ─────────────────────────────────────────────────────────────
    if (_isPetsListPath(path) && method == 'GET') {
      final items = _pets.values.map((p) => p.toJson()).toList();
      return _jsonResponse(200, {
        'items': items,
        'total': items.length,
      });
    }

    // ── GET /pets/{id} ────────────────────────────────────────────────────────
    if (_isPetItemPath(path) && method == 'GET') {
      final id = _extractId(path);
      final pet = _pets[id];
      if (pet != null) return _jsonResponse(200, pet.toJson());
      return _jsonResponse(404, {
        'code': 'NOT_FOUND',
        'message': 'Pet $id not found',
      });
    }

    // ── POST /pets ────────────────────────────────────────────────────────────
    if (_isPetsListPath(path) && method == 'POST') {
      final body = request.body is Map<String, dynamic>
          ? request.body as Map<String, dynamic>
          : <String, dynamic>{};
      final newId = 'pet-${(_pets.length + 1).toString().padLeft(3, '0')}';
      final newPet = Pet(
        id: newId,
        name: body['name'] as String? ?? 'Unknown',
        category: body['category'] as String? ?? 'other',
        status: body['status'] as String? ?? 'available',
        age: body['age'] as int?,
        tags: (body['tags'] as List<dynamic>?)?.cast<String>() ?? [],
      );
      _pets[newPet.id] = newPet;
      return _jsonResponse(201, newPet.toJson());
    }

    // ── DELETE /pets/{id} ─────────────────────────────────────────────────────
    if (_isPetItemPath(path) && method == 'DELETE') {
      final id = _extractId(path);
      if (_pets.containsKey(id)) {
        _pets.remove(id);
        return const ApiResponse(
          statusCode: 204,
          body: '',
          headers: {},
        );
      }
      return _jsonResponse(404, {
        'code': 'NOT_FOUND',
        'message': 'Pet $id not found',
      });
    }

    // ── Fallback ──────────────────────────────────────────────────────────────
    return _jsonResponse(501, {
      'code': 'NOT_IMPLEMENTED',
      'message': 'Endpoint $method $path is not mocked',
    });
  }

  @override
  Future<void> close() async {}

  // ── Helpers ─────────────────────────────────────────────────────────────────

  /// Strips the base URL and any version prefix, returning only the
  /// resource portion of the path (e.g. "/pets" or "/pets/pet-001").
  ///
  /// Examples:
  ///   https://api.example.com/v1/pets       → /pets
  ///   https://api.example.com/v1/pets/p-1   → /pets/p-1
  ///   /v1/pets                              → /pets
  ///   /pets                                 → /pets
  static String _normalisePath(String raw) {
    // 1. Extract the URI path (strips scheme + host + query).
    String path;
    try {
      path = Uri.parse(raw).path;
    } catch (_) {
      path = raw;
    }
    // 2. Remove a leading version segment like /v1, /v2, /api/v3, etc.
    //    Pattern: optional /api then /v<digits> at the start of the path.
    path = path.replaceFirst(RegExp(r'^(?:/api)?/v\d+'), '');
    // 3. Ensure exactly one leading slash.
    if (!path.startsWith('/')) path = '/$path';
    return path;
  }

  static bool _isPetsListPath(String raw) {
    final p = _normalisePath(raw);
    return p == '/pets' || p == '/pets/';
  }

  static bool _isPetItemPath(String raw) {
    final p = _normalisePath(raw);
    final parts = p.split('/').where((s) => s.isNotEmpty).toList();
    return parts.length == 2 && parts[0] == 'pets';
  }

  static String _extractId(String raw) {
    final p = _normalisePath(raw);
    return p.split('/').where((s) => s.isNotEmpty).last;
  }

  static ApiResponse _jsonResponse(int status, Map<String, dynamic> body) =>
      ApiResponse(
        statusCode: status,
        body: jsonEncode(body),
        headers: const {'content-type': 'application/json'},
      );
}

// ─────────────────────────────────────────────────────────────────────────────
// Service layer  (mirrors what `nebula generate` would produce)
// ─────────────────────────────────────────────────────────────────────────────

/// Hand-written service that wraps [NebulaClient] — same pattern as generated.
class PetsService {
  PetsService(this._client);

  final NebulaClient _client;
  static const _base = '/pets';

  // ── List ──────────────────────────────────────────────────────────────────

  Future<Result<List<Pet>>> listPets({String? status, int limit = 20}) =>
      _client.request<List<Pet>>(
        ApiRequest(
          method: 'GET',
          path: _base,
          query: {
            if (status != null) 'status': status,
            'limit': limit,
          },
          useCache: true,
        ),
        (json) {
          final data = json as Map<String, dynamic>;
          return (data['items'] as List<dynamic>)
              .cast<Map<String, dynamic>>()
              .map(Pet.fromJson)
              .toList();
        },
      );

  // ── Get one ───────────────────────────────────────────────────────────────

  Future<Result<Pet>> getPet(String petId) => _client.request<Pet>(
        ApiRequest(method: 'GET', path: '$_base/$petId', useCache: true),
        (json) => Pet.fromJson(json as Map<String, dynamic>),
      );

  // ── Create ────────────────────────────────────────────────────────────────

  Future<Result<Pet>> createPet({
    required String name,
    required String category,
    required String status,
    int? age,
    List<String> tags = const [],
  }) =>
      _client.request<Pet>(
        ApiRequest(
          method: 'POST',
          path: _base,
          body: {
            'name': name,
            'category': category,
            'status': status,
            if (age != null) 'age': age,
            'tags': tags,
          },
        ),
        (json) => Pet.fromJson(json as Map<String, dynamic>),
      );

  // ── Delete ────────────────────────────────────────────────────────────────

  Future<Result<bool>> deletePet(String petId) => _client.request<bool>(
        ApiRequest(method: 'DELETE', path: '$_base/$petId'),
        (_) => true,
      );
}

// ─────────────────────────────────────────────────────────────────────────────
// Console helpers
// ─────────────────────────────────────────────────────────────────────────────

void _section(String title) {
  print('\n${'─' * 62}');
  print('  $title');
  print('─' * 62);
}

void _ok(String msg) => print('  ✓  $msg');
void _err(String msg) => print('  ✗  $msg');
void _info(String msg) => print('     $msg');

// ─────────────────────────────────────────────────────────────────────────────
// Shared fold helper — adapts Result.fold to the "when" style used below
// ─────────────────────────────────────────────────────────────────────────────

extension ResultWhen<T> on Result<T> {
  R when<R>({
    required R Function(T value) success,
    required R Function(ApiError error) failure,
  }) =>
      fold(onSuccess: success, onFailure: failure);
}

// ─────────────────────────────────────────────────────────────────────────────
// main()
// ─────────────────────────────────────────────────────────────────────────────

Future<void> main() async {
  print('╔══════════════════════════════════════════════════════════════╗');
  print('║           Nebula API Studio — Runtime Demo v0.1.0           ║');
  print('╚══════════════════════════════════════════════════════════════╝');

  // ── 1. Create mock adapter ────────────────────────────────────────────────
  _section('1. Adapter Setup');

  final adapter = MockPetStoreAdapter();
  _ok('MockPetStoreAdapter instantiated (no real network calls)');

  // ── 2. Build plugin chain ─────────────────────────────────────────────────
  _section('2. Plugin Chain');

  final authPlugin = JwtAuthPlugin(
    tokenProvider: () async => 'demo-jwt-token-xxxx',
  );

  final loggingPlugin = LoggingPlugin(level: LogLevel.basic);

  final analyticsPlugin = AnalyticsPlugin(
    onFlush: (snapshot) => _info(
      'Analytics flush — ${snapshot.totalRequests} req, '
      'success rate: '
      '${(snapshot.overallSuccessRate * 100).toStringAsFixed(0)}%',
    ),
  );

  _ok('JwtAuthPlugin      configured (Bearer token)');
  _ok('LoggingPlugin      configured (basic level)');
  _ok('AnalyticsPlugin    configured');

  // ── 3. Instantiate NebulaClient ───────────────────────────────────────────
  _section('3. NebulaClient Construction');

  final client = NebulaClient(
    baseUrl: 'https://api.petstore.example.com/v1',
    adapter: adapter,
    plugins: [authPlugin, loggingPlugin, analyticsPlugin],
    cache: MemoryCache(maxCapacity: 200),
    invalidation: InvalidationGraph.crud('/pets'),
    retry: ExponentialBackoff(
      maxAttempts: 3,
      baseDelay: const Duration(milliseconds: 100),
      maxDelay: const Duration(seconds: 5),
    ),
  );

  _ok('NebulaClient created — baseUrl: https://api.petstore.example.com/v1');

  final pets = PetsService(client);

  // ── 4. List all pets ──────────────────────────────────────────────────────
  _section('4. List All Pets  [GET /pets]');

  final listResult1 = await pets.listPets();
  listResult1.when(
    success: (list) {
      _ok('Received ${list.length} pet(s):');
      for (final p in list) {
        _info(p.toString());
      }
    },
    failure: (err) => _err('listPets failed: $err'),
  );

  // ── 5. Fetch by ID ────────────────────────────────────────────────────────
  _section('5. Fetch Pet by ID  [GET /pets/pet-001]');

  final getResult = await pets.getPet('pet-001');
  getResult.when(
    success: (p) => _ok('Found: $p'),
    failure: (err) => _err('getPet failed: $err'),
  );

  // ── 6. Cache hit — same request, no network call ──────────────────────────
  _section('6. Cache Hit  [GET /pets/pet-001 — served from memory cache]');

  final before = adapter.requestCount;
  final cachedResult = await pets.getPet('pet-001');
  final after = adapter.requestCount;
  cachedResult.when(
    success: (p) {
      final fromCache = after == before;
      _ok('Got: ${p.name} — ${fromCache ? "⚡ from cache (0 network calls)" : "network call"}');
    },
    failure: (err) => _err('getPet (cached) failed: $err'),
  );

  // ── 7. 404 — non-existent pet ─────────────────────────────────────────────
  _section('7. 404 Error Handling  [GET /pets/pet-999]');

  final notFound = await pets.getPet('pet-999');
  notFound.when(
    success: (p) => _err('Should NOT reach here: $p'),
    failure: (err) {
      _ok('Correctly received Failure —  '
          'type: ${err.type.name},  statusCode: ${err.statusCode}');
    },
  );

  // ── 8. Create a new pet ───────────────────────────────────────────────────
  _section('8. Create Pet  [POST /pets]');

  final createResult = await pets.createPet(
    name: 'Nemo',
    category: 'fish',
    status: 'available',
    age: 1,
    tags: ['saltwater', 'tropical'],
  );
  createResult.when(
    success: (p) => _ok('Created: $p'),
    failure: (err) => _err('createPet failed: $err'),
  );

  // ── 9. Delete a pet ───────────────────────────────────────────────────────
  _section('9. Delete Pet  [DELETE /pets/pet-002]');

  final deleteResult = await pets.deletePet('pet-002');
  deleteResult.when(
    success: (_) => _ok('pet-002 deleted successfully'),
    failure: (err) => _err('deletePet failed: $err'),
  );

  // ── 10. List after mutations (cache invalidated) ──────────────────────────
  _section('10. List After Mutations  [cache was invalidated by POST/DELETE]');

  final listResult2 = await pets.listPets();
  listResult2.when(
    success: (list) {
      _ok('Now ${list.length} pet(s) in store:');
      for (final p in list) {
        _info('  • ${p.name} (${p.category}) — ${p.status}');
      }
    },
    failure: (err) => _err('listPets failed: $err'),
  );

  // ── 11. Result.map() chaining ─────────────────────────────────────────────
  _section('11. Result.map()  — transform success value without unwrapping');

  final namesResult = (await pets.listPets())
      .map((list) => list.map((p) => p.name).join(', '));

  namesResult.when(
    success: (names) => _ok('Pet names: $names'),
    failure: (err) => _err('Failed: $err'),
  );

  // ── 12. Result.getOrElse() ────────────────────────────────────────────────
  _section('12. Result.getOrElse()  — fallback on failure');

  final names = (await pets.listPets())
      .map((list) => list.map((p) => p.name).toList())
      .getOrElse([]);

  _ok('getOrElse result: $names');

  // ── 13. Result.fold() — explicit exhaustive pattern ──────────────────────
  _section('13. Result.fold()  — exhaustive success / failure handling');

  final singleResult = await pets.getPet('pet-001');
  final message = singleResult.fold(
    onSuccess: (p) => 'Success → ${p.name} is ${p.status}',
    onFailure: (err) => 'Failure → ${err.type.name}: ${err.message}',
  );
  _ok(message);

  // ── 14. Analytics snapshot ────────────────────────────────────────────────
  _section('14. Analytics Snapshot');

  final snap = analyticsPlugin.snapshot();
  _info('Total requests dispatched : ${snap.totalRequests}');
  _info('Total errors              : ${snap.totalErrors}');
  _info('Total cache hits          : ${snap.totalCacheHits}');
  _info(
    'Overall success rate      : '
    '${(snap.overallSuccessRate * 100).toStringAsFixed(1)}%',
  );
  _info('HTTP adapter call count   : ${adapter.requestCount}');
  if (snap.endpoints.isNotEmpty) {
    _info('Per-endpoint breakdown:');
    for (final m in snap.endpoints.values) {
      _info(
        '  ${m.key.padRight(35)} '
        'reqs=${m.totalRequests}  '
        'ok=${m.successCount}  '
        'fail=${m.failureCount}  '
        'p50=${m.p50}ms',
      );
    }
  }

  // ── 15. Graceful shutdown ─────────────────────────────────────────────────
  _section('15. Graceful Shutdown');

  await client.dispose();
  _ok('NebulaClient disposed — adapter and plugins cleaned up');

  print(
    '\n╔══════════════════════════════════════════════════════════════╗',
  );
  print(
    '║              Demo completed successfully! 🎉                ║',
  );
  print(
    '╚══════════════════════════════════════════════════════════════╝\n',
  );
}
2
likes
130
points
137
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A professional Dart/Flutter package combining a Compiler + Runtime SDK. Convert OpenAPI 3.x / Swagger 2.0 specs into fully type-safe Dart SDKs with smart caching, retry policies, a plugin system, offline queue, and a powerful CLI with watch mode. Ready for pub.dev.

Topics

#networking #code-generation #openapi #swagger #http

License

MIT (license)

Dependencies

collection, http, meta, path, yaml

More

Packages that depend on nebula_api_studio