diff --git a/README.md b/README.md index 3c70e08..04e2533 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file +[![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 + + io.github.pgmarc.space + space-client + 0.0.1-SNAPSHOT + +``` + +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 diff --git a/examples/src/main/java/io/github/pgmarc/space/examples/FeatureEvaluation.java b/examples/src/main/java/io/github/pgmarc/space/examples/FeatureEvaluation.java new file mode 100644 index 0000000..9e9c808 --- /dev/null +++ b/examples/src/main/java/io/github/pgmarc/space/examples/FeatureEvaluation.java @@ -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); + } +} diff --git a/examples/src/main/java/io/github/pgmarc/space/examples/SingleService.java b/examples/src/main/java/io/github/pgmarc/space/examples/SingleService.java index 2eee104..086f433 100644 --- a/examples/src/main/java/io/github/pgmarc/space/examples/SingleService.java +++ b/examples/src/main/java/io/github/pgmarc/space/examples/SingleService.java @@ -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 @@ -20,7 +19,7 @@ * src/main/resources/single folder. *

*/ -public class SingleService { +final class SingleService { /** * Before executing the code snippets you have to do the following: @@ -28,7 +27,7 @@ public class SingleService { *
  • Start a SPACE instance
  • *
  • Get a MANAGER or ADMIN api key
  • * - * + *

    * You are free to experiment with the API and adjust the code to your needs. * Happy Hacking :) * @@ -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 SubscriptionRequest, i.e, create a Subscription in SPACE + * API you need the following parameters: + *

    + * + *

    + * An UserContact needs al least the following parameters: + *

    + *

    + * You can pass optional parameters using the UserContact.Builder methods + * + *

    + *

    
    +     * UserContact.builder("you_user_id", "janedoe")
    +     *     .firstName("Jane")
    +     *     .lastName("Doe")
    +     *     .email("janedoe@example.com")
    +     *     .phone("280-689-4646")
    +     *     .build();
    +     * 
    + *

    + * + *

    + * 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: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    PlanAdd-onPosible
    00No
    01Yes
    0NYes
    10Yes
    11Yes
    1NYes
    + *

    + */ + 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 @@ -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); } - }