Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 101 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,104 @@
# HTTP RESTful API for SPACE

[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=pgmarc_space-java-client&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=pgmarc_space-java-client)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=pgmarc_space-java-client&metric=coverage)](https://sonarcloud.io/summary/new_code?id=pgmarc_space-java-client)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=pgmarc_space-java-client&metric=coverage)](https://sonarcloud.io/summary/new_code?id=pgmarc_space-java-client)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=pgmarc_space-java-client&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=pgmarc_space-java-client)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=pgmarc_space-java-client&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=pgmarc_space-java-client)

HTTP REST client implemented in Java for Subscription and Pricing Access Control Engine (SPACE).

## Features

- Read, create and update user's subscriptions
- Verify a user's feature availability based on their subscription
- Configure SPACE client with:
- Host (required)
- Port (optional, by default SPACE listens on port `5403`)
- SPACE URL path prefix (optional, by default space is under `api/v1`, i.e., `http://example.com/api/v1`)
- Read and write timeout of HTTP client (optional, uses OkHTTP client under the hood)

SPACE client implements the following operations of [SPACE OAS](space-oas):

- `POST /contracts`
- `GET /contracts/{userId}`
- `PUT /contracts/{userId}`
- `POST /features/{userId}/{featureId}`
- `POST /features/{userId}/pricing-token`


## Installation

### Requirements

- Java 11 or later
- Apache Maven 3.6.3 or later (excluding `4.X`)

### Maven

```xml
<dependency>
<groupId>io.github.pgmarc.space</groupId>
<artifactId>space-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
```

To build and install the jar locally execute the following:

```bash
cd space-client
mvn clean install
```

### Usage

Before using `SpaceClient` you will need to do the following:
1. Start a SPACE instance in your machine
2. Get the corresponding api key for the required role
3. As `MANAGER` or `ADMIN` add a service in SPACE by uploading a pricing in `Pricing2Yaml` format specification

See [Pricing4SaaS docs](pricing4saas-docs) for more information.

> [!WARNING]
> API Keys are secrets and should be kept safe end encrypted.

Configure `SpaceClient` using the builder:
```java
SpaceClient client = SpaceClient.builder("example.com", apiKey)
.port(8080)
.path("example/path")
.build();
```

Create subscriptions:
```java
UserContact contact = UserContact
.builder("3f5f934c-951b-4a2d-ad10-b1679ac9b7ba", "example-user").build();

SubscriptionRequest createSubscriptionRequest = SubscriptionRequest.builder(contact)
.startService("Petclinic", "2024")
.plan("Enterprise")
.addOn("petLover", 1)
.endService()
.build();

Subscription subscription = client.contracts().addContract(createSubscriptionRequest);
```

Evaluate subscription features at runtime:

```java
String userId = "3f5f934c-951b-4a2d-ad10-b1679ac9b7ba";
FeatureEvaluationResult result = client
.features()
.evaluate(userid, "Petclinic", "pets");
```

These are just some examples, but you can find more in `examples` directory.

### Documentation

You read more documentation about SPACE in [Pricing4SaaS docs](pricing4saas-docs).

[pricing4saas-docs]: https://pricing4saas-docs.vercel.app/
[space-oas]: https://github.com/Alex-GF/space/blob/docs/oas/api/docs/space-api-docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.github.pgmarc.space.examples;

import io.github.pgmarc.space.SpaceClient;
import io.github.pgmarc.space.features.FeatureEvaluationResult;
import io.github.pgmarc.space.features.Revert;
import io.github.pgmarc.space.features.UsageLimitConsumption;

import java.io.IOException;
import java.util.Objects;

final class FeatureEvaluation {


public static void main(String[] args) throws IOException {

String apiKey = System.getenv("SPACE_API_KEY");
Objects.requireNonNull(apiKey, "you must set SPACE_API_KEY env variable");
SpaceClient client = SpaceClient.builder("localhost", apiKey).build();

String userId = "4427d118-073d-4da2-a145-f77a75b52595";
String service = "WireMock";
String feature = "mockAPI";
evaluateFeature(client, userId, service, feature);
evaluateWithConsumption(client, userId, service, feature);
revertFeatureOptimisticEvaluation(client, userId, service, feature);
getPricingToken(client, userId);
}

private static void evaluateFeature(SpaceClient client, String userId, String service, String feature) throws IOException {
FeatureEvaluationResult eval = client.features().evaluate(userId, service, feature);

if (eval.isAvailable()) {
System.out.println("User with id '" + userId + "' is able to use " + service + " " + feature);
} else {
System.out.println("User with id '" + userId + "' is not able to use " + service + " " + feature);
}
}

private static void evaluateWithConsumption(SpaceClient client, String userId, String service, String feature) throws IOException {
String usageLimit = "mockAPICallsLimit";
UsageLimitConsumption consumption = UsageLimitConsumption.builder(service)
.addInt(usageLimit, 1)
.build();
FeatureEvaluationResult eval = client.features().evaluateOptimistically(userId, service, feature, consumption);

if (eval.isAvailable()) {
System.out.println("User with id '" + userId + "' is able to use " + service + " " + feature);
Number consumed = eval.getConsumed(usageLimit).orElse(null);
Number limit = eval.getLimit(usageLimit).orElse(null);
if (consumed != null) {
System.out.println("Consumed " + consumed + " API calls out of " + limit);
}
} else {
System.out.println("User with id '" + userId + "' is not able to use " + service + " " + feature);
}
}

private static void revertFeatureOptimisticEvaluation(SpaceClient client, String userId, String service, String feature) throws IOException {
boolean success = client.features().revert(userId, service, feature, Revert.NEWEST_VALUE);
System.out.println("Revert operation has " + (success ? "succeeded" : "failed"));
}

private static void getPricingToken(SpaceClient client, String userId) throws IOException {

String pricingJwtToken = client.features().generatePricingTokenForUser(userId);
System.out.println(pricingJwtToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package io.github.pgmarc.space.examples;

import java.io.IOException;
import java.util.Objects;

import io.github.pgmarc.space.SpaceClient;
import io.github.pgmarc.space.contracts.Subscription;
import io.github.pgmarc.space.contracts.SubscriptionRequest;
import io.github.pgmarc.space.contracts.SubscriptionUpdateRequest;
import io.github.pgmarc.space.contracts.UserContact;
import okhttp3.*;

import java.io.IOException;
import java.util.Objects;

/**
* In this example you will be subscribing to a service with only a single
Expand All @@ -20,15 +19,15 @@
* <code>src/main/resources/single</code> folder.
* </p>
*/
public class SingleService {
final class SingleService {

/**
* Before executing the code snippets you have to do the following:
* <ul>
* <li>Start a SPACE instance</li>
* <li>Get a <code>MANAGER</code> or <code>ADMIN</code> api key</li>
* </ul>
*
* <p>
* You are free to experiment with the API and adjust the code to your needs.
* Happy Hacking :)
*
Expand All @@ -38,14 +37,99 @@ public class SingleService {
public static void main(String[] args) throws IOException {

String apiKey = Objects.requireNonNull(System.getenv("SPACE_API_KEY"),
"You need to set SPACE_API_KEY env variable") ;
"You need to set SPACE_API_KEY env variable");

SpaceClient client = SpaceClient.builder("localhost", apiKey).build();

// A subscription at least requires a user id and a username
String userId = "4427d118-073d-4da2-a145-f77a75b52595";

addContract(client, userId);
getUserContract(client, userId);
//updateContract(client, userId);

}

/**
* To create a <code>SubscriptionRequest</code>, i.e, create a Subscription in SPACE
* API you need the following parameters:
* <ul>
* <li>An <code>UserContact</code></li>
* <li>A <code>Service</code></li>
* </ul>
*
* <p>
* An <code>UserContact</code> needs al least the following parameters:
* <ul>
* <li><code>userId</code>: is the id of the user of <b>YOUR</b> application</li>
* <li><code>username</code>: alias that identifies the user of <b>YOUR</b> application</li>
* </ul>
* <p>
* You can pass optional parameters using the <code>UserContact.Builder</code> methods
*
* <p>
* <pre><code>
* UserContact.builder("you_user_id", "janedoe")
* .firstName("Jane")
* .lastName("Doe")
* .email("janedoe@example.com")
* .phone("280-689-4646")
* .build();
* </code></pre>
* </p>
*
* <p>
* A subscription al least contains one contracted service. A service
* can optionally have a plan or multiple add-ons, but at least you
* have to be subscribed to one of them. For example the following combinations
* are possible:
*
* <table>
* <thead>
* <tr>
* <th>Plan</th>
* <th>Add-on</th>
* <th>Posible</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>0</td>
* <td>0</td>
* <td>No</td>
* </tr>
* <tr>
* <td>0</td>
* <td>1</td>
* <td>Yes</td>
* </tr>
* <tr>
* <td>0</td>
* <td>N</td>
* <td>Yes</td>
* </tr>
* <tr>
* <td>1</td>
* <td>0</td>
* <td>Yes</td>
* </tr>
* <tr>
* <td>1</td>
* <td>1</td>
* <td>Yes</td>
* </tr>
* <tr>
* <td>1</td>
* <td>N</td>
* <td>Yes</td>
* </tr>
* </tbody>
* </table>
* </p>
*/
private static void addContract(SpaceClient client, String userId) throws IOException {
// A subscription at least requires a user id and a username
String username = "alex";
UserContact contact = UserContact.builder(userId, username).build();
UserContact contact1 = UserContact.builder(userId, username).build();

// But, you can provide more user contact information if you want
UserContact contact2 = UserContact
Expand All @@ -55,27 +139,33 @@ public static void main(String[] args) throws IOException {
.email("janedoe@example.com")
.phone("280-689-4646").build();

SubscriptionRequest subReq = SubscriptionRequest.builder(contact)
SubscriptionRequest subReq = SubscriptionRequest.builder(contact1)
.startService("WireMock", "2024")
.plan("Enterprise")
.plan("Enterprise")
.endService()
.build();

Subscription newSubscription = client.contracts().addContract(subReq);
System.out.println(newSubscription);
}

private static void getUserContract(SpaceClient client, String userId) throws IOException {
Subscription subscription = client.contracts().getContractByUserId(userId);
System.out.println(subscription);
}

// Updating (novating) the contract to version of 2025
/**
* Updating (in legal terms, novating) the subscription of WireMock
* version 2024 to WireMock version 2025
*/
private static void updateContract(SpaceClient client, String userId) throws IOException {
SubscriptionUpdateRequest upReq = SubscriptionUpdateRequest.builder()
.startService("WireMock", "2025")
.plan("Enterprise")
.plan("Enterprise")
.endService()
.build();

Subscription updatedSubscription = client.contracts().updateContractByUserId(userId, upReq);
System.out.println(updatedSubscription);
}

}