SDK para Uber direct en C#

A continuación se presenta una propuesta de SDK en C# para la API de Uber Direct (Deliveries), basándose en la documentación pública disponible en https://developer.uber.com/docs/deliveries/overview. Este SDK está organizado de manera similar a otros SDKs modernos (por ejemplo, el de Square), separando capas, haciendo uso de modelos de datos (DTOs) con atributos de serialización, interfaces para los clientes de la API, manejo de autenticación, manejo de errores y un ejemplo básico de uso.

Notas importantes:

  • Este es un ejemplo de referencia. No es un SDK oficial ni probado extensamente.
  • La API de Uber Direct puede cambiar, por lo que deberás revisar los endpoints, parámetros, y modelos contra la documentación oficial.
  • Tendrás que agregar más endpoints y modelos según las necesidades; aquí se muestran algunos endpoints típicos (crear una entrega, obtener el estatus de la entrega, cancelar una entrega).
  • Se utiliza System.Text.Json para la serialización JSON.
  • Requiere .NET 6+ (para records, HttpClient moderno y init accessors).
  • Ajusta nombres de namespaces y proyectos según tu organización.

Estructura de la Solución

MyCompany.UberDirect.sln
|-- src
|   |-- MyCompany.UberDirect
|       |-- MyCompany.UberDirect.csproj
|       |-- UberDirectClient.cs
|       |-- Configuration
|       |   |-- UberDirectConfiguration.cs
|       |-- Authentication
|       |   |-- IAuthenticator.cs
|       |   |-- BearerTokenAuthenticator.cs
|       |-- Exceptions
|       |   |-- UberDirectApiException.cs
|       |-- Http
|       |   |-- AuthenticatedHttpClientHandler.cs
|       |-- Models
|       |   |-- CreateDeliveryRequest.cs
|       |   |-- CreateDeliveryResponse.cs
|       |   |-- DeliveryStatusResponse.cs
|       |   |-- CancelDeliveryResponse.cs
|       |   |-- ErrorResponse.cs
|       |-- Api
|           |-- IDeliveriesClient.cs
|           |-- DeliveriesClient.cs
|
|-- tests
|   |-- MyCompany.UberDirect.Tests
|       |-- MyCompany.UberDirect.Tests.csproj
|       |-- DeliveriesClientTests.cs (opcional)
|
|-- examples
|   |-- MyCompany.UberDirect.Examples
|       |-- MyCompany.UberDirect.Examples.csproj
|       |-- Program.cs

Configuración

UberDirectConfiguration.cs
Contiene la información necesaria para conectarse a la API (URL base, Token, etc.).

namespace MyCompany.UberDirect.Configuration
{
    public record UberDirectConfiguration
    {
        public string BaseUrl { get; init; } = "https://api.uber.com";
        public string BearerToken { get; init; } = string.Empty;
    }
}

Autenticación

IAuthenticator.cs

namespace MyCompany.UberDirect.Authentication
{
    public interface IAuthenticator
    {
        Task AuthenticateAsync(HttpRequestMessage request);
    }
}

BearerTokenAuthenticator.cs

using System.Net.Http.Headers;

namespace MyCompany.UberDirect.Authentication
{
    public class BearerTokenAuthenticator : IAuthenticator
    {
        private readonly string _token;

        public BearerTokenAuthenticator(string token)
        {
            _token = token;
        }

        public Task AuthenticateAsync(HttpRequestMessage request)
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
            return Task.CompletedTask;
        }
    }
}

Manejador HTTP Autenticado

AuthenticatedHttpClientHandler.cs
Un DelegatingHandler que añade la autenticación a cada request saliente.

namespace MyCompany.UberDirect.Http
{
    using MyCompany.UberDirect.Authentication;

    public class AuthenticatedHttpClientHandler : DelegatingHandler
    {
        private readonly IAuthenticator _authenticator;

        public AuthenticatedHttpClientHandler(IAuthenticator authenticator)
        {
            _authenticator = authenticator;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            await _authenticator.AuthenticateAsync(request);
            return await base.SendAsync(request, cancellationToken);
        }
    }
}

Excepciones

UberDirectApiException.cs

using System.Net;

namespace MyCompany.UberDirect.Exceptions
{
    public class UberDirectApiException : Exception
    {
        public HttpStatusCode StatusCode { get; }
        public string Content { get; }

        public UberDirectApiException(string message, string content, HttpStatusCode statusCode = 0)
            : base(message)
        {
            StatusCode = statusCode;
            Content = content;
        }
    }
}

Modelos

Basados en la documentación, adaptando un escenario de creación de una entrega:

CreateDeliveryRequest.cs
(Estos campos pueden variar según la documentación. Ajusta según la API real.)

using System.Text.Json.Serialization;

namespace MyCompany.UberDirect.Models
{
    public record CreateDeliveryRequest(
        [property: JsonPropertyName("external_store_id")] string ExternalStoreId,
        [property: JsonPropertyName("external_order_id")] string ExternalOrderId,
        [property: JsonPropertyName("pickup")] Location Pickup,
        [property: JsonPropertyName("dropoff")] Location Dropoff,
        [property: JsonPropertyName("items")] List<DeliveryItem> Items
    );

    public record Location(
        [property: JsonPropertyName("address")] string Address,
        [property: JsonPropertyName("latitude")] double Latitude,
        [property: JsonPropertyName("longitude")] double Longitude,
        [property: JsonPropertyName("contact")] Contact Contact
    );

    public record Contact(
        [property: JsonPropertyName("first_name")] string FirstName,
        [property: JsonPropertyName("last_name")] string LastName,
        [property: JsonPropertyName("phone_number")] string PhoneNumber
    );

    public record DeliveryItem(
        [property: JsonPropertyName("quantity")] int Quantity,
        [property: JsonPropertyName("title")] string Title
    );
}

CreateDeliveryResponse.cs

using System.Text.Json.Serialization;

namespace MyCompany.UberDirect.Models
{
    public record CreateDeliveryResponse(
        [property: JsonPropertyName("delivery_id")] string DeliveryId,
        [property: JsonPropertyName("status")] string Status,
        [property: JsonPropertyName("tracking_url")] string TrackingUrl
    );
}

DeliveryStatusResponse.cs

using System.Text.Json.Serialization;

namespace MyCompany.UberDirect.Models
{
    public record DeliveryStatusResponse(
        [property: JsonPropertyName("delivery_id")] string DeliveryId,
        [property: JsonPropertyName("status")] string Status,
        [property: JsonPropertyName("eta")] string? Eta
    );
}

CancelDeliveryResponse.cs

using System.Text.Json.Serialization;

namespace MyCompany.UberDirect.Models
{
    public record CancelDeliveryResponse(
        [property: JsonPropertyName("status")] string Status
    );
}

ErrorResponse.cs

using System.Text.Json.Serialization;

namespace MyCompany.UberDirect.Models
{
    public record ErrorResponse(
        [property: JsonPropertyName("message")] string Message,
        [property: JsonPropertyName("code")] string? Code
    );
}

Cliente de API (Deliveries)

IDeliveriesClient.cs

namespace MyCompany.UberDirect.Api
{
    using MyCompany.UberDirect.Models;

    public interface IDeliveriesClient
    {
        Task<CreateDeliveryResponse> CreateDeliveryAsync(CreateDeliveryRequest request, CancellationToken cancellationToken = default);
        Task<DeliveryStatusResponse> GetDeliveryStatusAsync(string deliveryId, CancellationToken cancellationToken = default);
        Task<CancelDeliveryResponse> CancelDeliveryAsync(string deliveryId, CancellationToken cancellationToken = default);
    }
}

DeliveriesClient.cs

using System.Net;
using System.Text;
using System.Text.Json;
using MyCompany.UberDirect.Exceptions;
using MyCompany.UberDirect.Models;

namespace MyCompany.UberDirect.Api
{
    public class DeliveriesClient : IDeliveriesClient
    {
        private readonly HttpClient _httpClient;
        private readonly JsonSerializerOptions _jsonOptions;

        public DeliveriesClient(HttpClient httpClient)
        {
            _httpClient = httpClient;
            _jsonOptions = new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            };
        }

        public async Task<CreateDeliveryResponse> CreateDeliveryAsync(CreateDeliveryRequest request, CancellationToken cancellationToken = default)
        {
            using var content = new StringContent(JsonSerializer.Serialize(request, _jsonOptions), Encoding.UTF8, "application/json");
            var response = await _httpClient.PostAsync("/v1/deliveries", content, cancellationToken);
            return await HandleResponse<CreateDeliveryResponse>(response);
        }

        public async Task<DeliveryStatusResponse> GetDeliveryStatusAsync(string deliveryId, CancellationToken cancellationToken = default)
        {
            var response = await _httpClient.GetAsync($"/v1/deliveries/{deliveryId}", cancellationToken);
            return await HandleResponse<DeliveryStatusResponse>(response);
        }

        public async Task<CancelDeliveryResponse> CancelDeliveryAsync(string deliveryId, CancellationToken cancellationToken = default)
        {
            var request = new HttpRequestMessage(HttpMethod.Delete, $"/v1/deliveries/{deliveryId}");
            var response = await _httpClient.SendAsync(request, cancellationToken);
            return await HandleResponse<CancelDeliveryResponse>(response);
        }

        private async Task<T> HandleResponse<T>(HttpResponseMessage response)
        {
            var body = await response.Content.ReadAsStringAsync();
            if (!response.IsSuccessStatusCode)
            {
                ErrorResponse? error = null;
                try
                {
                    error = JsonSerializer.Deserialize<ErrorResponse>(body, _jsonOptions);
                }
                catch { /* ignorar error en parseo */ }

                var message = error?.Message ?? "Error calling Uber Direct API";
                throw new UberDirectApiException(message, body, response.StatusCode);
            }

            var result = JsonSerializer.Deserialize<T>(body, _jsonOptions);
            if (result == null)
            {
                throw new UberDirectApiException("Failed to deserialize response", body, response.StatusCode);
            }
            return result;
        }
    }
}

Cliente principal

UberDirectClient.cs
Este agrupa la configuración y la creación de los clientes individuales. Aquí sólo tenemos DeliveriesClient, pero se podrían agregar más en el futuro.

using MyCompany.UberDirect.Api;
using MyCompany.UberDirect.Authentication;
using MyCompany.UberDirect.Configuration;
using MyCompany.UberDirect.Http;

namespace MyCompany.UberDirect
{
    public class UberDirectClient
    {
        public IDeliveriesClient Deliveries { get; }

        public UberDirectClient(UberDirectConfiguration config)
        {
            var authenticator = new BearerTokenAuthenticator(config.BearerToken);

            var httpClientHandler = new HttpClientHandler();
            var authHandler = new AuthenticatedHttpClientHandler(authenticator)
            {
                InnerHandler = httpClientHandler
            };

            var httpClient = new HttpClient(authHandler)
            {
                BaseAddress = new Uri(config.BaseUrl)
            };

            Deliveries = new DeliveriesClient(httpClient);
        }
    }
}

Ejemplo de Uso

Program.cs en el proyecto MyCompany.UberDirect.Examples:

using MyCompany.UberDirect;
using MyCompany.UberDirect.Configuration;
using MyCompany.UberDirect.Models;

class Program
{
    static async Task Main()
    {
        var config = new UberDirectConfiguration
        {
            BaseUrl = "https://api.uber.com", // verificar URL
            BearerToken = "YOUR_ACCESS_TOKEN" // colocar el token obtenido de Uber
        };

        var client = new UberDirectClient(config);

        var createRequest = new CreateDeliveryRequest(
            ExternalStoreId: "store_123",
            ExternalOrderId: "order_456",
            Pickup: new Location(
                "123 Main St, City, State",
                37.7749,
                -122.4194,
                new Contact("John", "Doe", "+11234567890")
            ),
            Dropoff: new Location(
                "456 Market St, City, State",
                37.7890,
                -122.4010,
                new Contact("Jane", "Smith", "+11234567890")
            ),
            Items: new List<DeliveryItem>
            {
                new DeliveryItem(1, "Pizza"),
                new DeliveryItem(2, "Soda")
            }
        );

        // Crear la entrega
        var createResponse = await client.Deliveries.CreateDeliveryAsync(createRequest);
        Console.WriteLine($"Delivery created with ID: {createResponse.DeliveryId}, Status: {createResponse.Status}");

        // Obtener estado
        var statusResponse = await client.Deliveries.GetDeliveryStatusAsync(createResponse.DeliveryId);
        Console.WriteLine($"Delivery status: {statusResponse.Status}, ETA: {statusResponse.Eta}");

        // Cancelar la entrega (si se permite)
        var cancelResponse = await client.Deliveries.CancelDeliveryAsync(createResponse.DeliveryId);
        Console.WriteLine($"Delivery cancelled: {cancelResponse.Status}");
    }
}

Conclusión

Este SDK:

  • Crea una arquitectura modular y escalable.
  • Se inspira en la arquitectura de otros SDKs, con clases separadas por responsabilidad.
  • Maneja autenticación vía un BearerTokenAuthenticator.
  • Ofrece excepciones personalizadas y modelos fuertemente tipados.
  • Permite extenderse fácilmente a más endpoints que la API de Uber Direct ofrezca.

Antes de usar en producción, se debe:

  • Ajustar los modelos a los campos exactos de la API (revisar su documentación).
  • Agregar pruebas unitarias.
  • Probar el SDK con la API real y manejar posibles errores o casos especiales.