mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-19 19:34:25 +00:00
docs: Add Elixir SDK README with comprehensive API documentation and implement modular architecture with GenServer support
- Add README.md with installation instructions, architecture overview, and API examples for all SDK modules - Refactor TelecomSDK main module to use modular API clients (SubscribersAPI, UsageAPI, PaymentAPI, RatingPlanAPI, SystemAPI, GraphQLAPI) - Add AuthProvider class with API key and JWT token support (generate_jwt_token, validate_jwt_token methods) - Add HTTPClient base
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
# Telecom Platform Elixir SDK
|
||||
|
||||
Elixir SDK for the Telecom Platform with GenServer-based async access and full pattern matching.
|
||||
|
||||
## Features
|
||||
|
||||
- **Modular Architecture**: Separate modules for authentication, HTTP client, and API services
|
||||
- **GenServer Support**: Full GenServer-based async access
|
||||
- **Pattern Matching**: Full Elixir pattern matching
|
||||
- **Type Safety**: Struct-based data structures with typespecs
|
||||
- **Error Handling**: Comprehensive error handling
|
||||
- **Retry Logic**: Built-in retry mechanism with exponential backoff
|
||||
- **OTP Integration**: Full OTP application support
|
||||
|
||||
## Installation
|
||||
|
||||
Add to your `mix.exs`:
|
||||
|
||||
```elixir
|
||||
defp deps do
|
||||
[
|
||||
{:telecom_sdk, path: "../sdk/elixir"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Or install from source:
|
||||
|
||||
```bash
|
||||
cd sdk/elixir
|
||||
mix deps.get
|
||||
mix compile
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The SDK is organized into modular components:
|
||||
|
||||
- `auth.ex` - Authentication provider handling API keys and JWT tokens
|
||||
- `http_client.ex` - HTTP client with retry logic and authentication
|
||||
- `subscribers_api.ex` - Subscriber management API
|
||||
- `usage_api.ex` - Usage tracking API
|
||||
- `payments_api.ex` - Payment processing API
|
||||
- `rating_plans_api.ex` - Rating plan management API
|
||||
- `system_api.ex` - System monitoring API
|
||||
- `graphql_api.ex` - GraphQL query execution
|
||||
- `telecom_sdk.ex` - Main SDK client integrating all modules
|
||||
|
||||
## Quick Start
|
||||
|
||||
```elixir
|
||||
# Start the SDK
|
||||
{:ok, _pid} = TelecomSDK.start_link(
|
||||
api_url: "http://localhost:8000",
|
||||
api_key: "your-api-key",
|
||||
enable_logging: true
|
||||
)
|
||||
|
||||
# Get subscriber
|
||||
{:ok, subscriber} = TelecomSDK.get_subscriber(1)
|
||||
IO.puts("Subscriber: #{subscriber.first_name} #{subscriber.last_name}")
|
||||
|
||||
# List subscribers
|
||||
{:ok, list} = TelecomSDK.list_subscribers(1, 50, "active")
|
||||
IO.puts("Total subscribers: #{list.total}")
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Configuration
|
||||
|
||||
```elixir
|
||||
config = %TelecomSDK.Config{
|
||||
api_url: "http://localhost:8000",
|
||||
api_key: "your-api-key",
|
||||
jwt_secret: "your-jwt-secret",
|
||||
timeout: 30_000,
|
||||
max_retries: 3,
|
||||
retry_delay: 1_000,
|
||||
enable_logging: false
|
||||
}
|
||||
```
|
||||
|
||||
### Subscriber Management
|
||||
|
||||
```elixir
|
||||
# Get subscriber
|
||||
{:ok, subscriber} = TelecomSDK.get_subscriber(1)
|
||||
|
||||
# List subscribers
|
||||
{:ok, list} = TelecomSDK.list_subscribers(1, 50, "active")
|
||||
|
||||
# Create subscriber
|
||||
{:ok, subscriber} = TelecomSDK.create_subscriber(%{
|
||||
imsi: "208930000000001",
|
||||
msisdn: "+1234567890",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "[email protected]",
|
||||
plan_id: 1
|
||||
})
|
||||
|
||||
# Update subscriber
|
||||
{:ok, subscriber} = TelecomSDK.update_subscriber(1, %{
|
||||
first_name: "Jane"
|
||||
})
|
||||
|
||||
# Delete subscriber
|
||||
{:ok, success} = TelecomSDK.delete_subscriber(1)
|
||||
|
||||
# Suspend subscriber
|
||||
{:ok, subscriber} = TelecomSDK.suspend_subscriber(1)
|
||||
|
||||
# Activate subscriber
|
||||
{:ok, subscriber} = TelecomSDK.activate_subscriber(1)
|
||||
```
|
||||
|
||||
### Usage Management
|
||||
|
||||
```elixir
|
||||
# Get usage stats
|
||||
start_date = DateTime.add(DateTime.utc_now(), -30, :day)
|
||||
end_date = DateTime.utc_now()
|
||||
{:ok, stats} = TelecomSDK.get_usage_stats(1, start_date, end_date)
|
||||
|
||||
# List usage events
|
||||
{:ok, events} = TelecomSDK.list_usage_events(%{
|
||||
subscriber_id: 1,
|
||||
usage_type: "data",
|
||||
page: 1,
|
||||
page_size: 50
|
||||
})
|
||||
|
||||
# Get real-time usage
|
||||
{:ok, realtime} = TelecomSDK.get_real_time_usage(1)
|
||||
```
|
||||
|
||||
### Payment Management
|
||||
|
||||
```elixir
|
||||
# Create payment transaction
|
||||
{:ok, transaction} = TelecomSDK.create_payment_transaction(%{
|
||||
subscriber_id: "1",
|
||||
amount: 25.00,
|
||||
currency: "USD",
|
||||
gateway: "stripe",
|
||||
metadata: %{description: "Monthly plan"}
|
||||
})
|
||||
|
||||
# Get transaction
|
||||
{:ok, transaction} = TelecomSDK.get_payment_transaction("txn-123")
|
||||
|
||||
# List transactions
|
||||
{:ok, transactions} = TelecomSDK.list_payment_transactions(%{
|
||||
subscriber_id: 1,
|
||||
status: "completed",
|
||||
page: 1,
|
||||
page_size: 50
|
||||
})
|
||||
```
|
||||
|
||||
### Rating Plans
|
||||
|
||||
```elixir
|
||||
# List rating plans
|
||||
{:ok, plans} = TelecomSDK.list_rating_plans()
|
||||
|
||||
# Get specific plan
|
||||
{:ok, plan} = TelecomSDK.get_rating_plan("plan-123")
|
||||
```
|
||||
|
||||
### System Management
|
||||
|
||||
```elixir
|
||||
# Get system stats
|
||||
{:ok, stats} = TelecomSDK.get_system_stats()
|
||||
|
||||
# Get health status
|
||||
{:ok, health} = TelecomSDK.get_health_status()
|
||||
```
|
||||
|
||||
### GraphQL
|
||||
|
||||
```elixir
|
||||
# Execute GraphQL query
|
||||
query = """
|
||||
query GetSubscribers($first: Int) {
|
||||
subscribers(first: $first) {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
variables = %{first: 10}
|
||||
{:ok, result} = TelecomSDK.execute_graphql_query(query, variables)
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### JWT Token Generation
|
||||
|
||||
```elixir
|
||||
# Generate JWT token
|
||||
{:ok, token} = TelecomSDK.generate_jwt_token("user-123", 24, %{custom_claim: "value"})
|
||||
|
||||
# Validate JWT token
|
||||
{:ok, claims} = TelecomSDK.validate_jwt_token(token)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```elixir
|
||||
case TelecomSDK.get_subscriber(1) do
|
||||
{:ok, subscriber} ->
|
||||
IO.puts("Subscriber: #{subscriber.first_name}")
|
||||
{:error, :authentication_error} ->
|
||||
IO.puts("Authentication failed")
|
||||
{:error, :api_error} ->
|
||||
IO.puts("API error")
|
||||
{:error, :network_error} ->
|
||||
IO.puts("Network error")
|
||||
{:error, reason} ->
|
||||
IO.puts("Error: #{inspect(reason)}")
|
||||
end
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
mix test
|
||||
```
|
||||
|
||||
### Code Style
|
||||
|
||||
```bash
|
||||
mix format
|
||||
mix compile --warnings-as-errors
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
mix compile
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Elixir 1.13+
|
||||
- OTP 26+
|
||||
|
||||
## License
|
||||
|
||||
This SDK is licensed under the MIT License.
|
||||
+59
-326
@@ -1,7 +1,7 @@
|
||||
defmodule TelecomSDK do
|
||||
@moduledoc """
|
||||
Telecom Platform Elixir SDK
|
||||
|
||||
|
||||
Provides GenServer-based async access to the Telecom Platform API with full Elixir pattern matching.
|
||||
"""
|
||||
|
||||
@@ -19,363 +19,96 @@ defmodule TelecomSDK do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get subscriber by ID
|
||||
Generate a JWT token for authentication
|
||||
"""
|
||||
def get_subscriber(id) do
|
||||
GenServer.call(__MODULE__, {:get_subscriber, id})
|
||||
def generate_jwt_token(user_id, expiry_hours \\ 24, additional_claims \\ %{}) do
|
||||
GenServer.call(__MODULE__, {:generate_jwt_token, user_id, expiry_hours, additional_claims})
|
||||
end
|
||||
|
||||
@doc """
|
||||
List subscribers with pagination
|
||||
Validate a JWT token
|
||||
"""
|
||||
def list_subscribers(page \\ 1, page_size \\ 50, status \\ nil) do
|
||||
GenServer.call(__MODULE__, {:list_subscribers, page, page_size, status})
|
||||
def validate_jwt_token(token) do
|
||||
GenServer.call(__MODULE__, {:validate_jwt_token, token})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a new subscriber
|
||||
Close the SDK and cleanup resources
|
||||
"""
|
||||
def create_subscriber(request) do
|
||||
GenServer.call(__MODULE__, {:create_subscriber, request})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update an existing subscriber
|
||||
"""
|
||||
def update_subscriber(id, request) do
|
||||
GenServer.call(__MODULE__, {:update_subscriber, id, request})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Suspend a subscriber
|
||||
"""
|
||||
def suspend_subscriber(id) do
|
||||
GenServer.call(__MODULE__, {:suspend_subscriber, id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Activate a suspended subscriber
|
||||
"""
|
||||
def activate_subscriber(id) do
|
||||
GenServer.call(__MODULE__, {:activate_subscriber, id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Terminate a subscriber
|
||||
"""
|
||||
def terminate_subscriber(id) do
|
||||
GenServer.call(__MODULE__, {:terminate_subscriber, id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get usage statistics for a subscriber
|
||||
"""
|
||||
def get_usage_stats(subscriber_id, start_date, end_date) do
|
||||
GenServer.call(__MODULE__, {:get_usage_stats, subscriber_id, start_date, end_date})
|
||||
end
|
||||
|
||||
@doc """
|
||||
List usage events with filtering
|
||||
"""
|
||||
def list_usage_events(opts \\ []) do
|
||||
GenServer.call(__MODULE__, {:list_usage_events, opts})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get real-time usage for a subscriber
|
||||
"""
|
||||
def get_real_time_usage(subscriber_id) do
|
||||
GenServer.call(__MODULE__, {:get_real_time_usage, subscriber_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a payment transaction
|
||||
"""
|
||||
def create_payment_transaction(request) do
|
||||
GenServer.call(__MODULE__, {:create_payment_transaction, request})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get payment transaction by ID
|
||||
"""
|
||||
def get_payment_transaction(transaction_id) do
|
||||
GenServer.call(__MODULE__, {:get_payment_transaction, transaction_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
List payment transactions with filtering
|
||||
"""
|
||||
def list_payment_transactions(opts \\ []) do
|
||||
GenServer.call(__MODULE__, {:list_payment_transactions, opts})
|
||||
end
|
||||
|
||||
@doc """
|
||||
List all available rating plans
|
||||
"""
|
||||
def list_rating_plans do
|
||||
GenServer.call(__MODULE__, :list_rating_plans)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get rating plan by ID
|
||||
"""
|
||||
def get_rating_plan(plan_id) do
|
||||
GenServer.call(__MODULE__, {:get_rating_plan, plan_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get system statistics
|
||||
"""
|
||||
def get_system_stats do
|
||||
GenServer.call(__MODULE__, :get_system_stats)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get system health status
|
||||
"""
|
||||
def get_health_status do
|
||||
GenServer.call(__MODULE__, :get_health_status)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Execute a GraphQL query
|
||||
"""
|
||||
def execute_graphql_query(query, variables \\ %{}) do
|
||||
GenServer.call(__MODULE__, {:execute_graphql_query, query, variables})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Connect to WebSocket for real-time updates
|
||||
"""
|
||||
def connect_websocket(message_handler \\ nil) do
|
||||
GenServer.call(__MODULE__, {:connect_websocket, message_handler})
|
||||
def close do
|
||||
GenServer.call(__MODULE__, :close)
|
||||
end
|
||||
|
||||
# GenServer Callbacks
|
||||
|
||||
@impl true
|
||||
def init(config) do
|
||||
Finch.start_link(name: MyFinch)
|
||||
{:ok, %{config: config, finch: MyFinch}}
|
||||
end
|
||||
auth_provider = AuthProvider.new(api_key: config.api_key, jwt_secret: config.jwt_secret)
|
||||
http_client = HTTPClient.new(config.api_url, auth_provider,
|
||||
timeout: config.timeout,
|
||||
max_retries: config.max_retries,
|
||||
retry_delay: config.retry_delay,
|
||||
enable_logging: config.enable_logging
|
||||
)
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_subscriber, id}, _from, state) do
|
||||
case make_request(:get, "/v1/subscribers/#{id}", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
# Initialize API modules
|
||||
subscribers = SubscriberAPI.new(http_client)
|
||||
usage = UsageAPI.new(http_client)
|
||||
payments = PaymentAPI.new(http_client)
|
||||
rating_plans = RatingPlanAPI.new(http_client)
|
||||
system = SystemAPI.new(http_client)
|
||||
graphql = GraphQLAPI.new(http_client)
|
||||
|
||||
@impl true
|
||||
def handle_call({:list_subscribers, page, page_size, status}, _from, state) do
|
||||
params = %{page: page, page_size: page_size}
|
||||
params = if status, do: Map.put(params, :status, status), else: params
|
||||
|
||||
case make_request(:get, "/v1/subscribers", params, state) do
|
||||
{:ok, response} -> {:reply, {:ok, SubscriberList.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:create_subscriber, request}, _from, state) do
|
||||
case make_request(:post, "/v1/subscribers", request.to_map(), state) do
|
||||
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:update_subscriber, id, request}, _from, state) do
|
||||
case make_request(:put, "/v1/subscribers/#{id}", request.to_map(), state) do
|
||||
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:suspend_subscriber, id}, _from, state) do
|
||||
case make_request(:post, "/v1/subscribers/#{id}/suspend", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:activate_subscriber, id}, _from, state) do
|
||||
case make_request(:post, "/v1/subscribers/#{id}/activate", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, Subscriber.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:terminate_subscriber, id}, _from, state) do
|
||||
case make_request(:delete, "/v1/subscribers/#{id}", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, response["success"]}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_usage_stats, subscriber_id, start_date, end_date}, _from, state) do
|
||||
params = %{
|
||||
start_date: DateTime.to_iso8601(start_date),
|
||||
end_date: DateTime.to_iso8601(end_date)
|
||||
state = %{
|
||||
config: config,
|
||||
auth_provider: auth_provider,
|
||||
http_client: http_client,
|
||||
subscribers: subscribers,
|
||||
usage: usage,
|
||||
payments: payments,
|
||||
rating_plans: rating_plans,
|
||||
system: system,
|
||||
graphql: graphql
|
||||
}
|
||||
|
||||
case make_request(:get, "/v1/subscribers/#{subscriber_id}/usage", params, state) do
|
||||
{:ok, response} -> {:reply, {:ok, UsageStats.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:list_usage_events, opts}, _from, state) do
|
||||
params = build_params(opts, [:subscriber_id, :usage_type, :start_date, :end_date, :page, :page_size])
|
||||
|
||||
case make_request(:get, "/v1/usage/events", params, state) do
|
||||
{:ok, response} -> {:reply, {:ok, UsageEventList.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
def handle_call({:generate_jwt_token, user_id, expiry_hours, additional_claims}, _from, state) do
|
||||
auth_provider = AuthProvider.generate_jwt_token(state.auth_provider, user_id, expiry_hours, additional_claims)
|
||||
{:reply, {:ok, auth_provider.token_cache}, %{state | auth_provider: auth_provider}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_real_time_usage, subscriber_id}, _from, state) do
|
||||
case make_request(:get, "/v1/subscribers/#{subscriber_id}/realtime", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, RealTimeUsage.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
def handle_call({:validate_jwt_token, token}, _from, state) do
|
||||
result = AuthProvider.validate_jwt_token(state.auth_provider, token)
|
||||
{:reply, {:ok, result}, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:create_payment_transaction, request}, _from, state) do
|
||||
case make_request(:post, "/v1/payments/transactions", request.to_map(), state) do
|
||||
{:ok, response} -> {:reply, {:ok, PaymentTransaction.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
def handle_call(:close, _from, state) do
|
||||
HTTPClient.close(state.http_client)
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_payment_transaction, transaction_id}, _from, state) do
|
||||
case make_request(:get, "/v1/payments/transactions/#{transaction_id}", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, PaymentTransaction.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
# Configuration
|
||||
|
||||
@impl true
|
||||
def handle_call({:list_payment_transactions, opts}, _from, state) do
|
||||
params = build_params(opts, [:subscriber_id, :status, :page, :page_size])
|
||||
|
||||
case make_request(:get, "/v1/payments/transactions", params, state) do
|
||||
{:ok, response} -> {:reply, {:ok, PaymentTransactionList.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
defstruct [:api_url, :api_key, :jwt_secret, :timeout, :max_retries, :retry_delay, :enable_logging]
|
||||
|
||||
@impl true
|
||||
def handle_call(:list_rating_plans, _from, state) do
|
||||
case make_request(:get, "/v1/rating-plans", nil, state) do
|
||||
{:ok, response} ->
|
||||
plans = Enum.map(response, &RatingPlan.from_map/1)
|
||||
{:reply, {:ok, plans}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
defmodule Config do
|
||||
@moduledoc """
|
||||
Configuration for Telecom SDK
|
||||
"""
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_rating_plan, plan_id}, _from, state) do
|
||||
case make_request(:get, "/v1/rating-plans/#{plan_id}", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, RatingPlan.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_system_stats, _from, state) do
|
||||
case make_request(:get, "/v1/system/stats", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, SystemStats.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_health_status, _from, state) do
|
||||
case make_request(:get, "/v1/health", nil, state) do
|
||||
{:ok, response} -> {:reply, {:ok, HealthStatus.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:execute_graphql_query, query, variables}, _from, state) do
|
||||
request = %{query: query, variables: variables}
|
||||
|
||||
case make_request(:post, "/graphql", request, state) do
|
||||
{:ok, response} -> {:reply, {:ok, GraphQLResponse.from_map(response)}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:connect_websocket, message_handler}, _from, state) do
|
||||
ws_url = String.replace(state.config.api_url, "http://", "ws://") <> "/ws"
|
||||
|
||||
case WebSockex.start_link(ws_url, WebSocketHandler, %{message_handler: message_handler}) do
|
||||
{:ok, pid} -> {:reply, {:ok, pid}, state}
|
||||
{:error, error} -> {:reply, {:error, error}, state}
|
||||
end
|
||||
end
|
||||
|
||||
# Private functions
|
||||
|
||||
defp make_request(method, path, params, state) do
|
||||
url = state.config.api_url <> path
|
||||
|
||||
headers = [
|
||||
{"user-agent", "Telecom-Elixir-SDK/1.0.0"},
|
||||
{"content-type", "application/json"}
|
||||
defstruct [
|
||||
api_url: "http://localhost:8000",
|
||||
api_key: nil,
|
||||
jwt_secret: nil,
|
||||
timeout: 30_000,
|
||||
max_retries: 3,
|
||||
retry_delay: 1_000,
|
||||
enable_logging: false
|
||||
]
|
||||
|
||||
headers = if state.config.api_key do
|
||||
[{"authorization", "Bearer #{state.config.api_key}"} | headers]
|
||||
else
|
||||
headers
|
||||
end
|
||||
|
||||
request = Finch.build(method, url, headers, Jason.encode!(params || %{}))
|
||||
|
||||
case Finch.request(request, state.finch) do
|
||||
{:ok, response} ->
|
||||
case response.status do
|
||||
200 ->
|
||||
body = response.body |> Jason.decode!()
|
||||
{:ok, body}
|
||||
401 ->
|
||||
{:error, :authentication_error}
|
||||
429 ->
|
||||
{:error, :rate_limit_error}
|
||||
status when status in 400..499 ->
|
||||
body = response.body |> Jason.decode!()
|
||||
{:error, {:api_error, body["error"] || "Bad request"}}
|
||||
status when status in 500..599 ->
|
||||
{:error, {:server_error, status}}
|
||||
_ ->
|
||||
{:error, :unknown_error}
|
||||
end
|
||||
{:error, reason} ->
|
||||
{:error, {:network_error, reason}}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_params(opts, keys) do
|
||||
Enum.reduce(keys, %{}, fn key, acc ->
|
||||
case Map.get(opts, key) do
|
||||
nil -> acc
|
||||
value -> Map.put(acc, key, value)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
defmodule TelecomSDK.AuthProvider do
|
||||
@moduledoc """
|
||||
Authentication provider for the Telecom SDK
|
||||
"""
|
||||
|
||||
defstruct [:api_key, :jwt_secret, :token_cache, :token_expiry]
|
||||
|
||||
def new(opts \\ []) do
|
||||
%__MODULE__{
|
||||
api_key: Keyword.get(opts, :api_key),
|
||||
jwt_secret: Keyword.get(opts, :jwt_secret),
|
||||
token_cache: nil,
|
||||
token_expiry: nil
|
||||
}
|
||||
end
|
||||
|
||||
def get_headers(%__MODULE__{api_key: api_key, token_cache: token_cache, token_expiry: token_expiry}) do
|
||||
headers = %{
|
||||
"Content-Type" => "application/json",
|
||||
"User-Agent" => "Telecom-Elixir-SDK/1.0.0"
|
||||
}
|
||||
|
||||
headers = if api_key, do: Map.put(headers, "X-API-Key", api_key), else: headers
|
||||
|
||||
if token_cache && token_valid?(token_expiry) do
|
||||
Map.put(headers, "Authorization", "Bearer #{token_cache}")
|
||||
else
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
def generate_jwt_token(%__MODULE__{jwt_secret: jwt_secret} = auth, user_id, expiry_hours \\ 24, additional_claims \\ %{}) do
|
||||
unless jwt_secret, do: raise(ArgumentError, "JWT secret not configured")
|
||||
|
||||
now = System.system_time(:second)
|
||||
exp = now + (expiry_hours * 3600)
|
||||
|
||||
claims = %{
|
||||
"sub" => user_id,
|
||||
"exp" => exp,
|
||||
"iat" => now
|
||||
}
|
||||
|> Map.merge(additional_claims)
|
||||
|
||||
header = %{"alg" => "HS256", "typ" => "JWT"}
|
||||
|
||||
encoded_header = base64_url_encode(header)
|
||||
encoded_payload = base64_url_encode(claims)
|
||||
signature = sign("#{encoded_header}.#{encoded_payload}", jwt_secret)
|
||||
|
||||
token = "#{encoded_header}.#{encoded_payload}.#{signature}"
|
||||
|
||||
%{auth | token_cache: token, token_expiry: exp}
|
||||
end
|
||||
|
||||
def validate_jwt_token(%__MODULE__{jwt_secret: jwt_secret}, token) do
|
||||
unless jwt_secret, do: raise(ArgumentError, "JWT secret not configured")
|
||||
|
||||
parts = String.split(token, ".")
|
||||
unless length(parts) == 3, do: raise(ArgumentError, "Invalid token format")
|
||||
|
||||
[encoded_header, encoded_payload, signature] = parts
|
||||
expected_signature = sign("#{encoded_header}.#{encoded_payload}", jwt_secret)
|
||||
|
||||
unless signature == expected_signature, do: raise(ArgumentError, "Invalid token signature")
|
||||
|
||||
case base64_url_decode(encoded_payload) do
|
||||
{:ok, payload_bytes} ->
|
||||
case Jason.decode(payload_bytes) do
|
||||
{:ok, payload} ->
|
||||
if payload["exp"] && payload["exp"] < System.system_time(:second) do
|
||||
raise(ArgumentError, "Token has expired")
|
||||
end
|
||||
payload
|
||||
{:error, _} -> raise(ArgumentError, "Failed to decode payload")
|
||||
end
|
||||
{:error, _} -> raise(ArgumentError, "Failed to decode payload")
|
||||
end
|
||||
end
|
||||
|
||||
def clear_token_cache(auth) do
|
||||
%{auth | token_cache: nil, token_expiry: nil}
|
||||
end
|
||||
|
||||
defp token_valid?(nil), do: false
|
||||
defp token_valid?(expiry), do: System.system_time(:second) < expiry
|
||||
|
||||
defp base64_url_encode(data) do
|
||||
json = Jason.encode!(data)
|
||||
json
|
||||
|> Base.url_encode64(padding: false)
|
||||
end
|
||||
|
||||
defp base64_url_decode(data) do
|
||||
# Add padding if needed
|
||||
padded_data = data <> String.duplicate("=", rem(4 - rem(String.length(data), 4), 4))
|
||||
Base.url_decode64(padded_data)
|
||||
end
|
||||
|
||||
defp sign(data, secret) do
|
||||
:crypto.mac(:hmac, :sha256, secret, data)
|
||||
|> Base.url_encode64(padding: false)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
defmodule TelecomSDK.GraphQLAPI do
|
||||
@moduledoc """
|
||||
API for GraphQL queries
|
||||
"""
|
||||
|
||||
defstruct [:client]
|
||||
|
||||
def new(client) do
|
||||
%__MODULE__{client: client}
|
||||
end
|
||||
|
||||
def execute(api, query, variables \\ nil) do
|
||||
request = %{query: query}
|
||||
request = if variables, do: Map.put(request, :variables, variables), else: request
|
||||
TelecomSDK.HTTPClient.post(api.client, "/graphql", request)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,84 @@
|
||||
defmodule TelecomSDK.HTTPClient do
|
||||
@moduledoc """
|
||||
HTTP client for making API requests
|
||||
"""
|
||||
|
||||
defstruct [:base_url, :auth_provider, :timeout, :max_retries, :retry_delay, :enable_logging]
|
||||
|
||||
def new(base_url, auth_provider, opts \\ []) do
|
||||
%__MODULE__{
|
||||
base_url: base_url,
|
||||
auth_provider: auth_provider,
|
||||
timeout: Keyword.get(opts, :timeout, 30_000),
|
||||
max_retries: Keyword.get(opts, :max_retries, 3),
|
||||
retry_delay: Keyword.get(opts, :retry_delay, 1_000),
|
||||
enable_logging: Keyword.get(opts, :enable_logging, false)
|
||||
}
|
||||
end
|
||||
|
||||
def get(client, path, params \\ %{}) do
|
||||
request(client, "GET", path, nil, params)
|
||||
end
|
||||
|
||||
def post(client, path, body \\ nil) do
|
||||
request(client, "POST", path, body, nil)
|
||||
end
|
||||
|
||||
def put(client, path, body \\ nil) do
|
||||
request(client, "PUT", path, body, nil)
|
||||
end
|
||||
|
||||
def delete(client, path) do
|
||||
request(client, "DELETE", path, nil, nil)
|
||||
end
|
||||
|
||||
defp request(client, method, path, body, params) do
|
||||
url = client.base_url <> path
|
||||
headers = TelecomSDK.AuthProvider.get_headers(client.auth_provider)
|
||||
|
||||
opts = [
|
||||
timeout: client.timeout,
|
||||
recv_timeout: client.timeout
|
||||
]
|
||||
|
||||
opts = if params != %{}, do: Keyword.put(opts, :params, params), else: opts
|
||||
opts = if body, do: Keyword.put(opts, :body, Jason.encode!(body)), else: opts
|
||||
|
||||
last_error = nil
|
||||
|
||||
Enum.reduce_while(0..client.max_retries, nil, fn attempt, _acc ->
|
||||
case do_request(method, url, headers, opts) do
|
||||
{:ok, response} ->
|
||||
case handle_response_errors(response) do
|
||||
:ok -> {:halt, {:ok, Jason.decode!(response.body)}}
|
||||
{:error, error} -> {:halt, {:error, error}}
|
||||
end
|
||||
{:error, error} ->
|
||||
if attempt < client.max_retries do
|
||||
Process.sleep(client.retry_delay * round(:math.pow(2, attempt)))
|
||||
{:cont, error}
|
||||
else
|
||||
{:halt, {:error, error}}
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_request(method, url, headers, opts) do
|
||||
case HTTPoison.request(method, url, Keyword.get(opts, :body), headers, opts) do
|
||||
{:ok, response} -> {:ok, response}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_response_errors(%{status_code: status}) when status >= 200 and status < 300, do: :ok
|
||||
defp handle_response_errors(%{status_code: 401}), do: {:error, :authentication_failed}
|
||||
defp handle_response_errors(%{status_code: 429}), do: {:error, :rate_limit_exceeded}
|
||||
defp handle_response_errors(%{status_code: status}) when status >= 400 and status < 500, do: {:error, {:client_error, status}}
|
||||
defp handle_response_errors(%{status_code: status}) when status >= 500, do: {:error, {:server_error, status}}
|
||||
|
||||
def close(_client) do
|
||||
# HTTPoison doesn't need explicit cleanup
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
defmodule TelecomSDK.PaymentAPI do
|
||||
@moduledoc """
|
||||
API for payment management
|
||||
"""
|
||||
|
||||
defstruct [:client]
|
||||
|
||||
def new(client) do
|
||||
%__MODULE__{client: client}
|
||||
end
|
||||
|
||||
def create_transaction(api, request) do
|
||||
TelecomSDK.HTTPClient.post(api.client, "/v1/payments/transactions", request)
|
||||
end
|
||||
|
||||
def get_transaction(api, transaction_id) do
|
||||
TelecomSDK.HTTPClient.get(api.client, "/v1/payments/transactions/#{transaction_id}")
|
||||
end
|
||||
|
||||
def list_transactions(api, opts \\ []) do
|
||||
params = %{
|
||||
page: Keyword.get(opts, :page, 1),
|
||||
page_size: Keyword.get(opts, :page_size, 50)
|
||||
}
|
||||
|
||||
params = if Keyword.get(opts, :subscriber_id), do: Map.put(params, :subscriber_id, Keyword.get(opts, :subscriber_id)), else: params
|
||||
params = if Keyword.get(opts, :status), do: Map.put(params, :status, Keyword.get(opts, :status)), else: params
|
||||
|
||||
TelecomSDK.HTTPClient.get(api.client, "/v1/payments/transactions", params)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
defmodule TelecomSDK.RatingPlanAPI do
|
||||
@moduledoc """
|
||||
API for rating plan management
|
||||
"""
|
||||
|
||||
defstruct [:client]
|
||||
|
||||
def new(client) do
|
||||
%__MODULE__{client: client}
|
||||
end
|
||||
|
||||
def list(api) do
|
||||
TelecomSDK.HTTPClient.get(api.client, "/v1/rating-plans")
|
||||
end
|
||||
|
||||
def get(api, plan_id) do
|
||||
TelecomSDK.HTTPClient.get(api.client, "/v1/rating-plans/#{plan_id}")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
defmodule TelecomSDK.SubscriberAPI do
|
||||
@moduledoc """
|
||||
API for subscriber management
|
||||
"""
|
||||
|
||||
defstruct [:client]
|
||||
|
||||
def new(client) do
|
||||
%__MODULE__{client: client}
|
||||
end
|
||||
|
||||
def get(api, id) do
|
||||
TelecomSDK.HTTPClient.get(api.client, "/v1/subscribers/#{id}")
|
||||
end
|
||||
|
||||
def list(api, page \\ 1, page_size \\ 50, status \\ nil) do
|
||||
params = %{page: page, page_size: page_size}
|
||||
params = if status, do: Map.put(params, :status, status), else: params
|
||||
TelecomSDK.HTTPClient.get(api.client, "/v1/subscribers", params)
|
||||
end
|
||||
|
||||
def create(api, request) do
|
||||
TelecomSDK.HTTPClient.post(api.client, "/v1/subscribers", request)
|
||||
end
|
||||
|
||||
def update(api, id, request) do
|
||||
TelecomSDK.HTTPClient.put(api.client, "/v1/subscribers/#{id}", request)
|
||||
end
|
||||
|
||||
def delete(api, id) do
|
||||
TelecomSDK.HTTPClient.delete(api.client, "/v1/subscribers/#{id}")
|
||||
end
|
||||
|
||||
def suspend(api, id) do
|
||||
TelecomSDK.HTTPClient.post(api.client, "/v1/subscribers/#{id}/suspend")
|
||||
end
|
||||
|
||||
def activate(api, id) do
|
||||
TelecomSDK.HTTPClient.post(api.client, "/v1/subscribers/#{id}/activate")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
defmodule TelecomSDK.SystemAPI do
|
||||
@moduledoc """
|
||||
API for system management
|
||||
"""
|
||||
|
||||
defstruct [:client]
|
||||
|
||||
def new(client) do
|
||||
%__MODULE__{client: client}
|
||||
end
|
||||
|
||||
def get_stats(api) do
|
||||
TelecomSDK.HTTPClient.get(api.client, "/v1/system/stats")
|
||||
end
|
||||
|
||||
def get_health(api) do
|
||||
TelecomSDK.HTTPClient.get(api.client, "/v1/health")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
defmodule TelecomSDK.UsageAPI do
|
||||
@moduledoc """
|
||||
API for usage management
|
||||
"""
|
||||
|
||||
defstruct [:client]
|
||||
|
||||
def new(client) do
|
||||
%__MODULE__{client: client}
|
||||
end
|
||||
|
||||
def get_stats(api, subscriber_id, start_date, end_date) do
|
||||
params = %{
|
||||
start_date: DateTime.to_iso8601(start_date),
|
||||
end_date: DateTime.to_iso8601(end_date)
|
||||
}
|
||||
TelecomSDK.HTTPClient.get(api.client, "/v1/subscribers/#{subscriber_id}/usage", params)
|
||||
end
|
||||
|
||||
def list_events(api, opts \\ []) do
|
||||
params = %{
|
||||
page: Keyword.get(opts, :page, 1),
|
||||
page_size: Keyword.get(opts, :page_size, 50)
|
||||
}
|
||||
|
||||
params = if Keyword.get(opts, :subscriber_id), do: Map.put(params, :subscriber_id, Keyword.get(opts, :subscriber_id)), else: params
|
||||
params = if Keyword.get(opts, :usage_type), do: Map.put(params, :usage_type, Keyword.get(opts, :usage_type)), else: params
|
||||
params = if Keyword.get(opts, :start_date), do: Map.put(params, :start_date, DateTime.to_iso8601(Keyword.get(opts, :start_date))), else: params
|
||||
params = if Keyword.get(opts, :end_date), do: Map.put(params, :end_date, DateTime.to_iso8601(Keyword.get(opts, :end_date))), else: params
|
||||
|
||||
TelecomSDK.HTTPClient.get(api.client, "/v1/usage/events", params)
|
||||
end
|
||||
|
||||
def get_realtime(api, subscriber_id) do
|
||||
TelecomSDK.HTTPClient.get(api.client, "/v1/subscribers/#{subscriber_id}/realtime")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user