diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 159c41c..968404f 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -18,4 +18,6 @@ jobs: distribution: 'temurin' cache: maven - name: Test + env: + REGEXSOLVER_API_TOKEN: ${{ secrets.REGEXSOLVER_API_TOKEN }} run: mvn test diff --git a/.gitignore b/.gitignore index b425f09..2f11247 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ build/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +.env \ No newline at end of file diff --git a/README.md b/README.md index c9fb093..caf0f9e 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,7 @@ [Homepage](https://regexsolver.com) | [Online Demo](https://regexsolver.com/demo) | [Documentation](https://docs.regexsolver.com) | [Developer Console](https://console.regexsolver.com) -This repository contains the source code of the Java library for [RegexSolver](https://regexsolver.com) API. - -RegexSolver is a powerful regular expression manipulation toolkit, that gives you the power to manipulate regex as if -they were sets. +**RegexSolver** is a powerful toolkit for building, combining, and analyzing regular expressions. It is designed for constraint solvers, test generators, and other systems that need advanced regex operations. ## Installation @@ -19,20 +16,20 @@ they were sets. com.regexsolver.api RegexSolver - 1.0.2 + 1.1.0 ``` ### Gradle ```groovy -implementation "com.regexsolver.api:RegexSolver:1.0.2" +implementation "com.regexsolver.api:RegexSolver:1.1.0" ``` ## Usage -In order to use the library you need to generate an API Token on -our [Developer Console](https://console.regexsolver.com/). +1. Create an API token in the [Developer Console](https://console.regexsolver.com/). +2. Initialize the client and start working with terms: ```java import com.regexsolver.api.RegexSolver; @@ -43,181 +40,140 @@ import java.io.IOException; public class Main { public static void main(String[] args) throws IOException, ApiError { - RegexSolver.initialize("YOUR TOKEN HERE"); + // Set REGEXSOLVER_API_TOKEN in your env and call initialize(), + // or pass the token directly: + RegexSolver.initialize(); // or RegexSolver.initialize("YOUR_API_TOKEN"); + // Create terms Term term1 = Term.regex("(abc|de|fg){2,}"); Term term2 = Term.regex("de.*"); Term term3 = Term.regex(".*abc"); - Term term4 = Term.regex(".+(abc|de).+"); - + // Compute intersection and difference Term result = term1.intersection(term2, term3) - .subtraction(term4); - - System.out.println(result); + .difference(Term.regex(".+(abc|de).+")); + System.out.println(result.getPattern()); // de(fg)*abc } } ``` -## Features - -- [Intersection](#intersection) -- [Union](#union) -- [Subtraction / Difference](#subtraction--difference) -- [Equivalence](#equivalence) -- [Subset](#subset) -- [Details](#details) -- [Generate Strings](#generate-strings) -### Intersection +## Key Concepts & Limitations -#### Request +RegexSolver supports a subset of regular expressions that adhere to the principles of regular languages. Here are the key characteristics and limitations of the regular expressions supported by RegexSolver: +- **Anchored Expressions:** All regular expressions in RegexSolver are anchored. This means that the expressions are treated as if they start and end at the boundaries of the input text. For example, the expression `abc` will match the string "abc" but not "xabc" or "abcx". +- **Lookahead/Lookbehind:** RegexSolver does not support lookahead (`(?=...)`) or lookbehind (`(?<=...)`) assertions. Using them returns an error. +- **Pure Regular Expressions:** RegexSolver focuses on pure regular expressions as defined in regular language theory. This means features that extend beyond regular languages, such as backreferences (`\1`, `\2`, etc.), are not supported. Any use of backreference would return an error. +- **Greedy/Ungreedy Quantifiers:** The concept of ungreedy (`*?`, `+?`, `??`) quantifiers is not supported. All quantifiers are treated as greedy. For example, `a*` or `a*?` will match the longest possible sequence of "a"s. +- **Line Feed and Dot:** RegexSolver handles all characters the same way. The dot `.` matches any Unicode character including line feed (`\n`). +- **Empty Regular Expressions:** The empty language (matches no string) is represented by constructs like `[]` (empty character class). This is distinct from the empty string. -Compute the intersection of the provided terms and return the resulting term. - -The maximum number of terms is currently limited to 10. - -```java -Term.Regex term1 = Term.regex("(abc|de){2}"); -Term.Regex term2 = Term.regex("de.*"); -Term.Regex term3 = Term.regex(".*abc"); - -Term result = term1.intersection(term2, term3); -System.out.println(result); -``` - -#### Response - -``` -regex=deabc -``` - -### Union - -Compute the union of the provided terms and return the resulting term. - -The maximum number of terms is currently limited to 10. - -#### Request - -```java -Term.Regex term1 = Term.regex("abc"); -Term.Regex term2 = Term.regex("de"); -Term.Regex term3 = Term.regex("fghi"); - -Term result = term1.union(term2, term3); -System.out.println(result); -``` - -#### Response - -``` -regex=(abc|de|fghi) -``` -### Subtraction / Difference +## Response Formats -Compute the first term minus the second and return the resulting term. +The API can handle terms in two formats: +- `regex`: a regular expression pattern +- `fair`: FAIR (Fast Automaton Internal Representation), a stable, signed format used internally by the engine -#### Request +By default, the engine returns whatever the operation produces, with no extra convertion. Override with `responseFormat`: ```java -Term.Regex term1 = Term.regex("(abc|de)"); -Term.Regex term2 = Term.regex("de"); +Term term = Term.regex("abcde"); -Term result = term1.subtraction(term2); -System.out.println(result); -``` - -#### Response +OperationOptions operationOptions = OperationOptions.newDefault() + .responseFormat(ResponseFormat.REGEX); +Term result1 = term.union(operationOptions, Term.regex("de")); -``` -regex=abc -``` +System.out.println(result1); // regex=(abc)?de -### Equivalence +operationOptions = OperationOptions.newDefault() + .responseFormat(ResponseFormat.FAIR); +Term result2 = term.union(operationOptions, Term.regex("de")); -Analyze if the two provided terms are equivalent. - -#### Request - -```java -Term.Regex term1 = Term.regex("(abc|de)"); -Term.Fair term2 = Term.regex("(abc|de)*"); - -boolean result = term1.isEquivalentTo(term2); -System.out.println(result); +System.out.println(result2); // fair=... ``` -#### Response - -``` -false -``` +If the format does not matter, omit `responseFormat` or set it to `ResponseFormat.ANY`. -### Subset +Regardless of the format, you can always call `getPattern()` to obtain the regex pattern of a term. -Analyze if the second term is a subset of the first. +## Bounding execution time -#### Request +Set a server-side compute timeout in milliseconds with `executionTimeout`: ```java -Term.Regex term1 = Term.regex("de"); -Term.Regex term2 = Term.regex("(abc|de)"); - -boolean result = term1.isSubsetOf(term2); -System.out.println(result); -``` - -#### Response - -``` -true +// Limit the server-side compute time to 5 ms +try { + Term term1 = Term.regex(".*ab.*c(de|fg).*dab.*c(de|fg).*ab.*c(de|fg).*dab.*c"); + Term term2 = Term.regex(".*abc.*"); + + OperationOptions operationOptions = OperationOptions.newDefault() + .executionTimeout(5); + Term out = term1.difference(operationOptions, term2); +} catch (ApiError e) { + System.out.println(e.getMessage()); // The operation took too much time. +} ``` -### Details +Timeout is best effort. The exact time is not guaranteed. -Compute the details of the provided term. +## API Overview -The computed details are: +`Term` exposes the following methods. -- **Cardinality:** the number of possible values. -- **Length:** the minimum and maximum length of possible values. -- **Empty:** true if is an empty set (does not contain any value), false otherwise. -- **Total:** true if is a total set (contains all values), false otherwise. +### Build +| Method | Return | Description | +| -------- | ------- | ------- | +| `Term.fair(String fair)` | `Term` | Creates a term from a FAIR. | +| `Term.regex(String regex)` | `Term` | Creates a term from a regex pattern. | -#### Request +### Analyze -```java -Term.Regex term = Term.regex("(abc|de)"); +| Method | Return | Description | +| -------- | ------- | ------- | +| `t.equivalent(Term term)` | `boolean` | `true` if `t` and `term` accept exactly the same language. Supports `executionTimeout`. | +| `t.getCardinality()` | `Cardinality` | Returns the cardinality of the term (i.e., the number of possible matched strings). | +| `t.getDot()` | `String` | Returns a Graphviz DOT representation of the automaton for the term. | +| `t.getFair()` | `String` | Returns the FAIR of the term if defined. | +| `t.getLength()` | `Length` | Returns the minimum and maximum length of matched strings. | +| `t.getPattern()` | `String` | Returns a regular expression pattern for the term. | +| `t.isEmpty()` | `boolean` | `true` if the term matches no string. | +| `t.isEmptyString()` | `boolean` | `true` if the term matches only the empty string. | +| `t.isTotal()` | `boolean` | `true` if the term matches all possible strings. | +| `t.subset(Term term)` | `boolean` | `true` if every string matched by `t` is also matched by `term`. Supports `executionTimeout`. | -Details details = term.getDetails(); -System.out.println(details); -``` +### Compute -#### Response +| Method | Return | Description | +| -------- | ------- | ------- | +| `t.concat(Term... terms)` | `Term` | Concatenates `t` with the given terms. Supports `responseFormat` and `executionTimeout`. | +| `t.difference(Term term)` | `Term` | Computes the difference `t - term`. Supports `responseFormat` and `executionTimeout`. | +| `t.intersection(Term... terms)` | `Term` | Computes the intersection of `t` with the given terms. Supports `responseFormat` and `executionTimeout`. | +| `t.repeat(int min, Integer max)` | `Term` | Computes the repetition of the term between `min` and `max` times; if `max` is `null`, the repetition is unbounded. Supports `responseFormat` and `executionTimeout`. | +| `t.union(Term... terms)` | `Term` | Computes the union of `t` with the given terms. Supports `responseFormat` and `executionTimeout`. | -``` -Details[cardinality=Integer(2), length=Length[minimum=2, maximum=3], empty=false, total=false] -``` +### Generate -### Generate Strings +| Method | Return | Description | +| -------- | ------- | ------- | +| `t.generateStrings(int count)` | `String[]` | Generates up to `count` unique example strings matched by `t`. Supports `executionTimeout`. | -Generate the given number of strings that can be matched by the provided term. +### Other +| Method | Return | Description | +| -------- | ------- | ------- | +| `t.serialize()` | `String` | Returns a serialized form of `t`. | +| `Term.deserialize(String string)` | `Term` | Returns a deserialized term from the given `string`. | -The maximum number of strings to generate is currently limited to 200. +## Cross-Language Support -#### Request +If you want to use this library with other programming languages, we provide: +- [regexsolver-js](https://github.com/RegexSolver/regexsolver-js) +- [regexsolver-python](https://github.com/RegexSolver/regexsolver-python) -```java -Term.Regex term = Term.regex("(abc|de){2}"); +For more information about how to use the wrappers, you can refer to our [guide](https://docs.regexsolver.com/getting-started.html). -List strings = term.generateStrings(3); -System.out.println(strings); -``` +You can also take a look at [regexsolver](https://github.com/RegexSolver/regexsolver) which contains the source code of the engine. -#### Response +## License -``` -[abcde, dede, deabc] -``` +This project is licensed under the MIT License. diff --git a/pom.xml b/pom.xml index 4f04249..e284508 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.regexsolver.api RegexSolver - 1.0.2 + 1.1.0 https://regexsolver.com @@ -17,8 +17,7 @@ https://github.com/RegexSolver/regexsolver-java - RegexSolver allows you to manipulate regular expressions as sets, enabling operations such as intersection, - union, and subtraction. + RegexSolver is a powerful toolkit for building, combining, and analyzing regular expressions. @@ -44,12 +43,12 @@ com.squareup.retrofit2 retrofit - 2.11.0 + 2.12.0 com.squareup.retrofit2 converter-jackson - 2.11.0 + 2.12.0 junit diff --git a/src/main/java/com/regexsolver/api/OperationOptions.java b/src/main/java/com/regexsolver/api/OperationOptions.java new file mode 100644 index 0000000..836e039 --- /dev/null +++ b/src/main/java/com/regexsolver/api/OperationOptions.java @@ -0,0 +1,28 @@ +package com.regexsolver.api; + +public class OperationOptions { + protected ResponseFormat responseFormat; + protected Integer executionTimeout; + + public static OperationOptions newDefault() { + return new OperationOptions(); + } + + public OperationOptions responseFormat(ResponseFormat responseFormat) { + this.responseFormat = responseFormat; + return this; + } + + public ResponseFormat responseFormat() { + return responseFormat; + } + + public OperationOptions executionTimeout(Integer executionTimeout) { + this.executionTimeout = executionTimeout; + return this; + } + + public Integer executionTimeout() { + return executionTimeout; + } +} diff --git a/src/main/java/com/regexsolver/api/RegexSolver.java b/src/main/java/com/regexsolver/api/RegexSolver.java index 97e2ea7..612b0de 100644 --- a/src/main/java/com/regexsolver/api/RegexSolver.java +++ b/src/main/java/com/regexsolver/api/RegexSolver.java @@ -1,6 +1,10 @@ package com.regexsolver.api; public final class RegexSolver { + public static void initialize() { + RegexSolverApiWrapper.initialize(); + } + public static void initialize(String token) { RegexSolverApiWrapper.initialize(token); } diff --git a/src/main/java/com/regexsolver/api/RegexSolverApiWrapper.java b/src/main/java/com/regexsolver/api/RegexSolverApiWrapper.java index 6f1f050..f2332af 100644 --- a/src/main/java/com/regexsolver/api/RegexSolverApiWrapper.java +++ b/src/main/java/com/regexsolver/api/RegexSolverApiWrapper.java @@ -3,9 +3,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.regexsolver.api.Request.GenerateStringsRequest; import com.regexsolver.api.Request.MultiTermsRequest; +import com.regexsolver.api.Request.RepeatRequest; import com.regexsolver.api.Response.BooleanResponse; +import com.regexsolver.api.Response.StringResponse; import com.regexsolver.api.Response.StringsResponse; -import com.regexsolver.api.dto.Details; +import com.regexsolver.api.dto.Cardinality; +import com.regexsolver.api.dto.Length; import com.regexsolver.api.exception.ApiError; import com.regexsolver.api.exception.MissingAPITokenException; import okhttp3.OkHttpClient; @@ -21,13 +24,14 @@ import java.io.IOException; import java.util.List; import java.util.Objects; +import java.util.Optional; final class RegexSolverApiWrapper { private static final RegexSolverApiWrapper INSTANCE = new RegexSolverApiWrapper(); - private final static String DEFAULT_BASE_URL = "https://api.regexsolver.com/"; + private final static String DEFAULT_BASE_URL = "https://api.regexsolver.com/v1/"; - private final static String USER_AGENT = "RegexSolver Java / 1.0.2"; + private final static String USER_AGENT = "RegexSolver Java / 1.1.0"; private RegexApi api; @@ -39,8 +43,16 @@ private RegexSolverApiWrapper() { initializeInternal(null, DEFAULT_BASE_URL); } + private static String getConfiguredBaseUrl() { + return Optional.ofNullable(System.getenv("REGEXSOLVER_BASE_URL")).orElse(DEFAULT_BASE_URL); + } + + static void initialize() { + getInstance().initializeInternal(System.getenv("REGEXSOLVER_API_TOKEN"), getConfiguredBaseUrl()); + } + static void initialize(String token) { - getInstance().initializeInternal(token, DEFAULT_BASE_URL); + getInstance().initializeInternal(token, getConfiguredBaseUrl()); } static void initialize(String token, String baseUrl) { @@ -66,8 +78,10 @@ private void initializeInternal(String token, String baseUrl) { api = retrofit.create(RegexApi.class); } - public Term computeIntersection(MultiTermsRequest multiTermsRequest) throws ApiError, IOException { - Response response = api.computeIntersection(multiTermsRequest).execute(); + // Analyze + + public Cardinality analyzeCardinality(Term term) throws ApiError, IOException { + Response response = api.analyzeCardinality(term).execute(); if (response.isSuccessful()) { return response.body(); } else { @@ -75,26 +89,44 @@ public Term computeIntersection(MultiTermsRequest multiTermsRequest) throws ApiE } } - public Term computeUnion(MultiTermsRequest multiTermsRequest) throws ApiError, IOException { - Response response = api.computeUnion(multiTermsRequest).execute(); + public String analyzeDot(Term term) throws ApiError, IOException { + Response response = api.analyzeDot(term).execute(); if (response.isSuccessful()) { - return response.body(); + return response.body().value(); } else { throw getApiError(response); } } - public Term computeSubtraction(MultiTermsRequest multiTermsRequest) throws ApiError, IOException { - Response response = api.computeSubtraction(multiTermsRequest).execute(); + public boolean analyzeEquivalent(MultiTermsRequest multiTermsRequest) throws ApiError, IOException { + Response response = api.analyzeEquivalent(multiTermsRequest).execute(); if (response.isSuccessful()) { - return response.body(); + return response.body().value(); + } else { + throw getApiError(response); + } + } + + public boolean analyzeEmpty(Term term) throws ApiError, IOException { + Response response = api.analyzeEmpty(term).execute(); + if (response.isSuccessful()) { + return response.body().value(); + } else { + throw getApiError(response); + } + } + + public boolean analyzeEmptyString(Term term) throws ApiError, IOException { + Response response = api.analyzeEmptyString(term).execute(); + if (response.isSuccessful()) { + return response.body().value(); } else { throw getApiError(response); } } - public Details getDetails(Term term) throws ApiError, IOException { - Response
response = api.getDetails(term).execute(); + public Length analyzeLength(Term term) throws ApiError, IOException { + Response response = api.analyzeLength(term).execute(); if (response.isSuccessful()) { return response.body(); } else { @@ -102,8 +134,17 @@ public Details getDetails(Term term) throws ApiError, IOException { } } - public boolean equivalence(MultiTermsRequest multiTermsRequest) throws ApiError, IOException { - Response response = api.equivalence(multiTermsRequest).execute(); + public String analyzePattern(Term term) throws ApiError, IOException { + Response response = api.analyzePattern(term).execute(); + if (response.isSuccessful()) { + return response.body().value(); + } else { + throw getApiError(response); + } + } + + public boolean analyzeSubset(MultiTermsRequest multiTermsRequest) throws ApiError, IOException { + Response response = api.analyzeSubset(multiTermsRequest).execute(); if (response.isSuccessful()) { return response.body().value(); } else { @@ -111,8 +152,8 @@ public boolean equivalence(MultiTermsRequest multiTermsRequest) throws ApiError, } } - public boolean subset(MultiTermsRequest multiTermsRequest) throws ApiError, IOException { - Response response = api.subset(multiTermsRequest).execute(); + public boolean analyzeTotal(Term term) throws ApiError, IOException { + Response response = api.analyzeTotal(term).execute(); if (response.isSuccessful()) { return response.body().value(); } else { @@ -120,8 +161,56 @@ public boolean subset(MultiTermsRequest multiTermsRequest) throws ApiError, IOEx } } - public List generateStrings(Term term, int count) throws ApiError, IOException { - GenerateStringsRequest generateStringsRequest = new GenerateStringsRequest(term, count); + // Compute + + public Term computeConcat(MultiTermsRequest multiTermsRequest) throws ApiError, IOException { + Response response = api.computeConcat(multiTermsRequest).execute(); + if (response.isSuccessful()) { + return response.body(); + } else { + throw getApiError(response); + } + } + + public Term computeDifference(MultiTermsRequest multiTermsRequest) throws ApiError, IOException { + Response response = api.computeDifference(multiTermsRequest).execute(); + if (response.isSuccessful()) { + return response.body(); + } else { + throw getApiError(response); + } + } + + public Term computeIntersection(MultiTermsRequest multiTermsRequest) throws ApiError, IOException { + Response response = api.computeIntersection(multiTermsRequest).execute(); + if (response.isSuccessful()) { + return response.body(); + } else { + throw getApiError(response); + } + } + + public Term computeRepeat(RepeatRequest repeatRequest) throws ApiError, IOException { + Response response = api.computeRepeat(repeatRequest).execute(); + if (response.isSuccessful()) { + return response.body(); + } else { + throw getApiError(response); + } + } + + public Term computeUnion(MultiTermsRequest multiTermsRequest) throws ApiError, IOException { + Response response = api.computeUnion(multiTermsRequest).execute(); + if (response.isSuccessful()) { + return response.body(); + } else { + throw getApiError(response); + } + } + + // Generate + + public List generateStrings(GenerateStringsRequest generateStringsRequest) throws ApiError, IOException { Response response = api.generateStrings(generateStringsRequest).execute(); if (response.isSuccessful()) { return response.body().value(); @@ -140,25 +229,52 @@ private static ApiError getApiError(Response response) throws IOException } private interface RegexApi { - @POST("api/compute/intersection") - Call computeIntersection(@Body MultiTermsRequest multiTermsRequest); + // analyze + @POST("analyze/cardinality") + Call analyzeCardinality(@Body Term term); - @POST("api/compute/union") - Call computeUnion(@Body MultiTermsRequest multiTermsRequest); + @POST("analyze/dot") + Call analyzeDot(@Body Term term); - @POST("api/compute/subtraction") - Call computeSubtraction(@Body MultiTermsRequest multiTermsRequest); + @POST("analyze/equivalent") + Call analyzeEquivalent(@Body MultiTermsRequest multiTermsRequest); - @POST("api/analyze/details") - Call
getDetails(@Body Term term); + @POST("analyze/empty") + Call analyzeEmpty(@Body Term term); - @POST("api/analyze/equivalence") - Call equivalence(@Body MultiTermsRequest multiTermsRequest); + @POST("analyze/empty_string") + Call analyzeEmptyString(@Body Term term); - @POST("api/analyze/subset") - Call subset(@Body MultiTermsRequest multiTermsRequest); + @POST("analyze/length") + Call analyzeLength(@Body Term term); + + @POST("analyze/pattern") + Call analyzePattern(@Body Term term); + + @POST("analyze/subset") + Call analyzeSubset(@Body MultiTermsRequest multiTermsRequest); + + @POST("analyze/total") + Call analyzeTotal(@Body Term term); + + // compute + @POST("compute/concat") + Call computeConcat(@Body MultiTermsRequest multiTermsRequest); + + @POST("compute/difference") + Call computeDifference(@Body MultiTermsRequest multiTermsRequest); + + @POST("compute/intersection") + Call computeIntersection(@Body MultiTermsRequest multiTermsRequest); + + @POST("compute/repeat") + Call computeRepeat(@Body RepeatRequest repeatRequest); + + @POST("compute/union") + Call computeUnion(@Body MultiTermsRequest multiTermsRequest); - @POST("api/generate/strings") + // generate + @POST("generate/strings") Call generateStrings(@Body GenerateStringsRequest request); } } diff --git a/src/main/java/com/regexsolver/api/Request.java b/src/main/java/com/regexsolver/api/Request.java index cdd3a71..1aedbf4 100644 --- a/src/main/java/com/regexsolver/api/Request.java +++ b/src/main/java/com/regexsolver/api/Request.java @@ -1,33 +1,149 @@ package com.regexsolver.api; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; final class Request { + @JsonInclude(JsonInclude.Include.NON_NULL) + static final class RequestOptions { + private final ResponseOptions response; + private final ExecutionOptions execution; - public static final class MultiTermsRequest { + public static RequestOptions fromArgs( + ResponseFormat format, + Integer timeout) { + + ResponseOptions response = null; + if (format != null) { + response = new ResponseOptions(format); + } + + ExecutionOptions execution = null; + if (timeout != null) { + execution = new ExecutionOptions(timeout); + } + + if (response == null && execution == null) { + return null; + } else { + return new RequestOptions(response, execution); + } + } + + public RequestOptions( + @JsonProperty("response") ResponseOptions response, + @JsonProperty("execution") ExecutionOptions execution) { + this.response = response; + this.execution = execution; + } + + public int getSchemaVersion() { + return 1; + } + + public ResponseOptions getResponse() { + return response; + } + + public ExecutionOptions getExecution() { + return execution; + } + + public static final class ResponseOptions { + private final ResponseFormat format; + + public ResponseOptions(@JsonProperty("format") ResponseFormat format) { + this.format = format; + } + + public ResponseFormat getFormat() { + return format; + } + } + + public static final class ExecutionOptions { + private final Integer timeout; + + public ExecutionOptions(@JsonProperty("timeout") Integer timeout) { + this.timeout = timeout; + } + + public Integer getTimeout() { + return timeout; + } + } + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + static final class MultiTermsRequest { private final List terms; + private final RequestOptions options; - public MultiTermsRequest(@JsonProperty("terms") List terms) { + public MultiTermsRequest(@JsonProperty("terms") List terms, + @JsonProperty("options") RequestOptions options) { this.terms = terms; + this.options = options; } public List getTerms() { return terms; } + + public RequestOptions getOptions() { + return options; + } } - public static final class GenerateStringsRequest { + @JsonInclude(JsonInclude.Include.NON_NULL) + static final class RepeatRequest { + private final Term term; + private final int min; + private final Integer max; + private final RequestOptions options; + + public RepeatRequest( + @JsonProperty("term") Term term, + @JsonProperty("min") int min, + @JsonProperty("max") Integer max, + @JsonProperty("options") RequestOptions options) { + this.term = term; + this.min = min; + this.max = max; + this.options = options; + } + + public Term getTerm() { + return term; + } + + public int getMin() { + return min; + } + + public Integer getMax() { + return max; + } + + public RequestOptions getOptions() { + return options; + } + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + static final class GenerateStringsRequest { private final Term term; private final int count; + private final RequestOptions options; public GenerateStringsRequest( @JsonProperty("term") Term term, - @JsonProperty("count") int count - ) { + @JsonProperty("count") int count, + @JsonProperty("options") RequestOptions options) { this.term = term; this.count = count; + this.options = options; } public Term getTerm() { @@ -37,5 +153,9 @@ public Term getTerm() { public int getCount() { return count; } + + public RequestOptions getOptions() { + return options; + } } } diff --git a/src/main/java/com/regexsolver/api/Response.java b/src/main/java/com/regexsolver/api/Response.java index 5675052..27ed766 100644 --- a/src/main/java/com/regexsolver/api/Response.java +++ b/src/main/java/com/regexsolver/api/Response.java @@ -17,6 +17,18 @@ public boolean value() { } } + public static final class StringResponse implements ResponseContent { + private final String value; + + public StringResponse(@JsonProperty("value") String value) { + this.value = value; + } + + public String value() { + return value; + } + } + public static final class StringsResponse implements ResponseContent { private final List value; diff --git a/src/main/java/com/regexsolver/api/ResponseContent.java b/src/main/java/com/regexsolver/api/ResponseContent.java index a7ff289..5b4f260 100644 --- a/src/main/java/com/regexsolver/api/ResponseContent.java +++ b/src/main/java/com/regexsolver/api/ResponseContent.java @@ -3,14 +3,14 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.regexsolver.api.Response.BooleanResponse; +import com.regexsolver.api.Response.StringResponse; import com.regexsolver.api.Response.StringsResponse; -import com.regexsolver.api.dto.Details; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = Term.Fair.class, name = "fair"), @JsonSubTypes.Type(value = Term.Regex.class, name = "regex"), - @JsonSubTypes.Type(value = Details.class, name = "details"), + @JsonSubTypes.Type(value = StringResponse.class, name = "string"), @JsonSubTypes.Type(value = StringsResponse.class, name = "strings"), @JsonSubTypes.Type(value = BooleanResponse.class, name = "boolean"), }) diff --git a/src/main/java/com/regexsolver/api/ResponseFormat.java b/src/main/java/com/regexsolver/api/ResponseFormat.java new file mode 100644 index 0000000..a1aeee5 --- /dev/null +++ b/src/main/java/com/regexsolver/api/ResponseFormat.java @@ -0,0 +1,12 @@ +package com.regexsolver.api; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum ResponseFormat { + @JsonProperty("any") + ANY, + @JsonProperty("regex") + REGEX, + @JsonProperty("fair") + FAIR +} diff --git a/src/main/java/com/regexsolver/api/Term.java b/src/main/java/com/regexsolver/api/Term.java index 3673670..433f471 100644 --- a/src/main/java/com/regexsolver/api/Term.java +++ b/src/main/java/com/regexsolver/api/Term.java @@ -2,8 +2,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.regexsolver.api.Request.GenerateStringsRequest; import com.regexsolver.api.Request.MultiTermsRequest; -import com.regexsolver.api.dto.Details; +import com.regexsolver.api.Request.RepeatRequest; +import com.regexsolver.api.Request.RequestOptions; +import com.regexsolver.api.dto.Cardinality; +import com.regexsolver.api.dto.Length; import com.regexsolver.api.exception.ApiError; import java.io.IOException; @@ -13,7 +17,8 @@ import java.util.Optional; /** - * This abstract class represents a term on which it is possible to perform operations. + * This abstract class represents a term on which it is possible to perform + * operations. */ public abstract class Term implements ResponseContent { @JsonIgnore @@ -29,7 +34,19 @@ public abstract class Term implements ResponseContent { private transient String serialized = null; @JsonIgnore - private transient Details details; + private transient Cardinality cardinality; + @JsonIgnore + private transient Length length; + @JsonIgnore + private transient Boolean empty; + @JsonIgnore + private transient Boolean total; + @JsonIgnore + private transient Boolean emptyString; + @JsonIgnore + private transient String pattern; + @JsonIgnore + private transient String dot; /** * Create a new instance. @@ -60,40 +77,231 @@ public static Term.Fair fair(String fair) { return new Term.Fair(fair); } - String getValue() { + public String getValue() { return value; } + private static RequestOptions loadRequestOptions(OperationOptions opts) { + RequestOptions requestOptions = null; + if (opts != null) { + requestOptions = RequestOptions.fromArgs(opts.responseFormat(), opts.executionTimeout()); + } + return requestOptions; + } + + // Analyze + + /** + * Check equivalence with the given term. + * + * @param opts Execution options. + * @param term The term to check equivalence with. + * @return true if the terms are equivalent, false otherwise. + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public boolean equivalent(OperationOptions opts, Term term) throws IOException, ApiError { + return RegexSolverApiWrapper.getInstance() + .analyzeEquivalent(new MultiTermsRequest(getArgs(term), + loadRequestOptions(opts))); + } + + /** + * Check equivalence with the given term. + * + * @param term The term to check equivalence with. + * @return true if the terms are equivalent, false otherwise. + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public boolean equivalent(Term term) throws IOException, ApiError { + return equivalent(null, term); + } + /** - * Get the details of this term. - * Cache the result to avoid calling the API again if this method is called multiple times. + * Get the cardinality of this term. + * Cache the result to avoid calling the API again if this method is called + * multiple times. * - * @return The details of this term. + * @return A `Cardinality` object describing how many distinct strings are + * matched. * @throws IOException In case of issues requesting the API server. * @throws ApiError In case of error returned by the API. */ @JsonIgnore - public Details getDetails() throws IOException, ApiError { - if (details != null) { - return details; + public Cardinality getCardinality() throws IOException, ApiError { + if (cardinality != null) { + return cardinality; } - details = RegexSolverApiWrapper.getInstance().getDetails(this); - return details; + cardinality = RegexSolverApiWrapper.getInstance() + .analyzeCardinality(this); + return cardinality; } /** - * Generate the given number of unique strings matched by this term. + * Get the GraphViz DOT representation of this term. + * Cache the result to avoid calling the API again if this method is called + * multiple times. * - * @param count The number of unique strings to generate. - * @return A list of unique strings matched by this term. + * @return A DOT language string describing the automaton for this term. * @throws IOException In case of issues requesting the API server. * @throws ApiError In case of error returned by the API. */ @JsonIgnore - public List generateStrings(int count) throws IOException, ApiError { - return RegexSolverApiWrapper.getInstance().generateStrings(this, count); + public String getDot() throws IOException, ApiError { + if (dot != null) { + return dot; + } + dot = RegexSolverApiWrapper.getInstance() + .analyzeDot(this); + return dot; + } + + /** + * Return the Fast Automaton Internal Representation (FAIR). + * + * @return The FAIR. + */ + @JsonIgnore + public String getFair() throws IOException, ApiError { + return null; + } + + /** + * Get the length bounds of this term. + * Cache the result to avoid calling the API again if this method is called + * multiple times. + * + * @return A `Length` object with the minimum and maximum string length matched + * by this term. + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public Length getLength() throws IOException, ApiError { + if (length != null) { + return length; + } + + length = RegexSolverApiWrapper.getInstance() + .analyzeLength(this); + return length; + } + + /** + * Return the regular expression pattern. + * + * If the term is not a regex the pattern will be resolved. + * Cache the result to avoid calling the API again if this method is called + * multiple times. + * + * @return The regular expression pattern. + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public String getPattern() throws IOException, ApiError { + if (pattern != null) { + return pattern; + } + + pattern = RegexSolverApiWrapper.getInstance() + .analyzePattern(this); + return pattern; + } + + /** + * Check whether this term matches no string. + * Cache the result to avoid calling the API again if this method is called + * multiple times. + * + * @return true if the term is empty, false otherwise. + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public boolean isEmpty() throws IOException, ApiError { + if (empty != null) { + return empty; + } + + empty = RegexSolverApiWrapper.getInstance() + .analyzeEmpty(this); + return empty; + } + + /** + * Check whether this term matches only the empty string. + * Cache the result to avoid calling the API again if this method is called + * multiple times. + * + * @return true if the term only matches the empty string, false otherwise. + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public boolean isEmptyString() throws IOException, ApiError { + if (emptyString != null) { + return emptyString; + } + + emptyString = RegexSolverApiWrapper.getInstance() + .analyzeEmptyString(this); + return emptyString; + } + + /** + * Check whether this term matches all possible strings. + * Cache the result to avoid calling the API again if this method is called + * multiple times. + * + * @return true if the term matches all possible strings, false otherwise. + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public boolean isTotal() throws IOException, ApiError { + if (total != null) { + return total; + } + + total = RegexSolverApiWrapper.getInstance() + .analyzeTotal(this); + return total; + } + + /** + * Check if is a subset of the given term. + * + * @param opts Execution options. + * @param term The term to check if is the superset of this. + * @return true if this is a subset, false otherwise. + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public boolean subset(OperationOptions opts, Term term) throws IOException, ApiError { + return RegexSolverApiWrapper.getInstance() + .analyzeSubset(new MultiTermsRequest(getArgs(term), loadRequestOptions(opts))); + } + + /** + * Check if is a subset of the given term. + * + * @param term The term to check if is the superset of this. + * @return true if this is a subset, false otherwise. + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public boolean subset(Term term) throws IOException, ApiError { + return subset(null, term); } + // Compute + @JsonIgnore private List getArgs(Term... terms) { ArrayList args = new ArrayList<>(); @@ -102,6 +310,77 @@ private List getArgs(Term... terms) { return args; } + /** + * Compute the concat with the given terms and return the resulting term. + * + * @param opts Execution options. + * @param terms The terms to compute an concat with. + * @return The resulting term + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public Term concat(OperationOptions opts, Term... terms) throws IOException, ApiError { + return RegexSolverApiWrapper.getInstance() + .computeConcat(new MultiTermsRequest(getArgs(terms), loadRequestOptions(opts))); + } + + /** + * Compute the concat with the given terms and return the resulting term. + * + * @param terms The terms to compute an concat with. + * @return The resulting term + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public Term concat(Term... terms) throws IOException, ApiError { + return concat(null, terms); + } + + /** + * Compute the difference with the given term and return the resulting term. + * + * @param opts Execution options. + * @param term The term to subtract. + * @return The resulting term + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public Term difference(OperationOptions opts, Term term) throws IOException, ApiError { + return RegexSolverApiWrapper.getInstance() + .computeDifference(new MultiTermsRequest(getArgs(term), loadRequestOptions(opts))); + } + + /** + * Compute the difference with the given term and return the resulting term. + * + * @param term The term to subtract. + * @return The resulting term + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public Term difference(Term term) throws IOException, ApiError { + return difference(null, term); + } + + /** + * Compute the intersection with the given terms and return the resulting term. + * + * @param opts Execution options. + * @param terms The terms to compute an intersection with. + * @return The resulting term + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public Term intersection(OperationOptions opts, Term... terms) throws IOException, ApiError { + return RegexSolverApiWrapper.getInstance() + .computeIntersection(new MultiTermsRequest(getArgs(terms), loadRequestOptions(opts))); + } + /** * Compute the intersection with the given terms and return the resulting term. * @@ -112,68 +391,104 @@ private List getArgs(Term... terms) { */ @JsonIgnore public Term intersection(Term... terms) throws IOException, ApiError { + return intersection(null, terms); + } + + /** + * Computes the repetition of the term between `min` and `max` times; if `max` + * is `null`, the repetition is unbounded. + * + * @param opts Execution options. + * @param min The lower bound of the repetition. + * @param max The upper bound of the repetition, if `null` the repetition is + * unbounded. + * @return The resulting term + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public Term repeat(OperationOptions opts, int min, Integer max) throws IOException, ApiError { return RegexSolverApiWrapper.getInstance() - .computeIntersection(new MultiTermsRequest(getArgs(terms))); + .computeRepeat(new RepeatRequest(this, min, max, loadRequestOptions(opts))); + } + + /** + * Computes the repetition of the term between `min` and `max` times; if `max` + * is `null`, the repetition is unbounded. + * + * @param min The lower bound of the repetition. + * @param max The upper bound of the repetition, if `null` the repetition is + * unbounded. + * @return The resulting term + * @throws IOException In case of issues requesting the API server. + * @throws ApiError In case of error returned by the API. + */ + @JsonIgnore + public Term repeat(int min, Integer max) throws IOException, ApiError { + return repeat(null, min, max); } /** * Compute the union with the given terms and return the resulting term. * + * @param opts Execution options. * @param terms The terms to compute a union with. * @return The resulting term * @throws IOException In case of issues requesting the API server. * @throws ApiError In case of error returned by the API. */ @JsonIgnore - public Term union(Term... terms) throws IOException, ApiError { + public Term union(OperationOptions opts, Term... terms) throws IOException, ApiError { return RegexSolverApiWrapper.getInstance() - .computeUnion(new MultiTermsRequest(getArgs(terms))); + .computeUnion(new MultiTermsRequest(getArgs(terms), loadRequestOptions(opts))); } /** - * Compute the subtraction with the given term and return the resulting term. + * Compute the union with the given terms and return the resulting term. * - * @param term The term to subtract. + * @param terms The terms to compute a union with. * @return The resulting term * @throws IOException In case of issues requesting the API server. * @throws ApiError In case of error returned by the API. */ @JsonIgnore - public Term subtraction(Term term) throws IOException, ApiError { - return RegexSolverApiWrapper.getInstance() - .computeSubtraction(new MultiTermsRequest(getArgs(term))); + public Term union(Term... terms) throws IOException, ApiError { + return union(null, terms); } + // Generate + /** - * Check equivalence with the given term. + * Generate the given number of unique strings matched by this term. * - * @param term The term to check equivalence with. - * @return true if the terms are equivalent, false otherwise. + * @param opts Execution options. + * @param count The number of unique strings to generate. + * @return A list of unique strings matched by this term. * @throws IOException In case of issues requesting the API server. * @throws ApiError In case of error returned by the API. */ @JsonIgnore - public boolean isEquivalentTo(Term term) throws IOException, ApiError { + public List generateStrings(OperationOptions opts, int count) throws IOException, ApiError { return RegexSolverApiWrapper.getInstance() - .equivalence(new MultiTermsRequest(getArgs(term))); + .generateStrings(new GenerateStringsRequest(this, count, loadRequestOptions(opts))); } /** - * Check if is a subset of the given term. + * Generate the given number of unique strings matched by this term. * - * @param term The term to check if is the superset of this. - * @return true if this is a subset, false otherwise. + * @param count The number of unique strings to generate. + * @return A list of unique strings matched by this term. * @throws IOException In case of issues requesting the API server. * @throws ApiError In case of error returned by the API. */ @JsonIgnore - public boolean isSubsetOf(Term term) throws IOException, ApiError { - return RegexSolverApiWrapper.getInstance() - .subset(new MultiTermsRequest(getArgs(term))); + public List generateStrings(int count) throws IOException, ApiError { + return generateStrings(null, count); } /** - * Generate a string representation that can be parsed by {@link #deserialize(String)}. + * Generate a string representation that can be parsed by + * {@link #deserialize(String)}. * * @return A string representation of this term. */ @@ -194,7 +509,8 @@ public String serialize() { } /** - * Parse a string representation of a {@link Term} produced by {@link #serialize()}. + * Parse a string representation of a {@link Term} produced by + * {@link #serialize()}. * * @param string A string representation produced by {@link #serialize()}. * @return The parsed term, or empty if the method was not able to parse. @@ -216,8 +532,10 @@ public static Optional deserialize(String string) { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; Term term = (Term) o; return Objects.equals(term.serialize(), serialize()); } @@ -235,7 +553,8 @@ public String toString() { /** * This term represents a Fast Automaton Internal Representation (FAIR). *

- * You can learn more about FAIR in our documentation. + * You can learn more about FAIR in our + * documentation. *

*/ public static final class Fair extends Term { @@ -248,22 +567,18 @@ public Fair(@JsonProperty("value") String fair) { super(fair); } - /** - * Return the Fast Automaton Internal Representation (FAIR). - * - * @return The FAIR. - */ @JsonProperty("value") + @Override public String getFair() { return getValue(); } } - /** * This term represents a regular expression. *

- * You can learn more about regular expression in our documentation + * You can learn more about regular expression in our + * documentation *

*/ public static final class Regex extends Term { @@ -276,12 +591,8 @@ public Regex(@JsonProperty("value") String regex) { super(regex); } - /** - * Return the regular expression pattern. - * - * @return The regular expression pattern. - */ @JsonProperty("value") + @Override public String getPattern() { return getValue(); } diff --git a/src/main/java/com/regexsolver/api/dto/Cardinality.java b/src/main/java/com/regexsolver/api/dto/Cardinality.java index f62261b..d4689be 100644 --- a/src/main/java/com/regexsolver/api/dto/Cardinality.java +++ b/src/main/java/com/regexsolver/api/dto/Cardinality.java @@ -9,9 +9,9 @@ */ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ - @JsonSubTypes.Type(value = Cardinality.BigInteger.class, name = "BigInteger"), - @JsonSubTypes.Type(value = Cardinality.Infinite.class, name = "Infinite"), - @JsonSubTypes.Type(value = Cardinality.Integer.class, name = "Integer") + @JsonSubTypes.Type(value = Cardinality.BigInteger.class, name = "bigInteger"), + @JsonSubTypes.Type(value = Cardinality.Infinite.class, name = "infinite"), + @JsonSubTypes.Type(value = Cardinality.Integer.class, name = "integer") }) public abstract class Cardinality { /** diff --git a/src/main/java/com/regexsolver/api/dto/Details.java b/src/main/java/com/regexsolver/api/dto/Details.java deleted file mode 100644 index 0d78d67..0000000 --- a/src/main/java/com/regexsolver/api/dto/Details.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.regexsolver.api.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.regexsolver.api.ResponseContent; -import com.regexsolver.api.Term; - -import java.util.Objects; - -/** - * Contains details about the requested {@link Term}. - */ -public final class Details implements ResponseContent { - private final Cardinality cardinality; - private final Length length; - private final boolean empty; - private final boolean total; - - /** - * @param cardinality the number of possible values. - * @param length the minimum and maximum length of possible values. - * @param empty true if is an empty set (does not contain any value), false otherwise. - * @param total true if is a total set (contains all values), false otherwise. - */ - public Details( - @JsonProperty("cardinality") Cardinality cardinality, - @JsonProperty("length") Length length, - @JsonProperty("empty") boolean empty, - @JsonProperty("total") boolean total - ) { - this.cardinality = cardinality; - this.length = length; - this.empty = empty; - this.total = total; - } - - /** - * @return The number of possible values. - */ - public Cardinality getCardinality() { - return cardinality; - } - - /** - * @return The minimum and maximum length of possible values. - */ - public Length getLength() { - return length; - } - - /** - * @return true if is an empty set (does not contain any value), false otherwise. - */ - public boolean isEmpty() { - return empty; - } - - /** - * @return true if is a total set (contains all values), false otherwise. - */ - public boolean isTotal() { - return total; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - var that = (Details) obj; - return Objects.equals(this.cardinality, that.cardinality) && - Objects.equals(this.length, that.length) && - this.empty == that.empty && - this.total == that.total; - } - - @Override - public int hashCode() { - return Objects.hash(cardinality, length, empty, total); - } - - @Override - public String toString() { - return "Details[" + - "cardinality=" + cardinality + ", " + - "length=" + length + ", " + - "empty=" + empty + ", " + - "total=" + total + ']'; - } -} diff --git a/src/main/java/com/regexsolver/api/dto/Length.java b/src/main/java/com/regexsolver/api/dto/Length.java index 3e1200f..785870e 100644 --- a/src/main/java/com/regexsolver/api/dto/Length.java +++ b/src/main/java/com/regexsolver/api/dto/Length.java @@ -1,6 +1,7 @@ package com.regexsolver.api.dto; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -18,8 +19,10 @@ public final class Length { private final Long maximum; /** - * @param minimum the minimum length of possible values, empty if is an empty set. - * @param maximum the maximum length of possible values, empty if the maximum length is infinite or if is an empty set. + * @param minimum the minimum length of possible values, empty if is an empty + * set. + * @param maximum the maximum length of possible values, empty if the maximum + * length is infinite or if is an empty set. */ Length(Long minimum, Long maximum) { this.minimum = minimum; @@ -37,7 +40,8 @@ public OptionalLong getMinimum() { } /** - * @return The maximum length of possible values, empty if the maximum length is infinite or if is an empty set. + * @return The maximum length of possible values, empty if the maximum length is + * infinite or if is an empty set. */ public OptionalLong getMaximum() { if (maximum == null) { @@ -48,8 +52,10 @@ public OptionalLong getMaximum() { @Override public boolean equals(Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; var that = (Length) obj; return Objects.equals(this.minimum, that.minimum) && Objects.equals(this.maximum, that.maximum); @@ -71,15 +77,41 @@ static class LengthDeserializer extends JsonDeserializer { @Override public Length deserialize(JsonParser jp, DeserializationContext ctx) throws IOException { - Long[] lengthArray = jp.readValueAs(Long[].class); - if (lengthArray != null && lengthArray.length == 2) { - return new Length( - lengthArray[0], - lengthArray[1] - ); - } else { - throw new IOException("Invalid length array."); + JsonToken t = jp.currentToken(); + if (t == null) + t = jp.nextToken(); + + if (t == JsonToken.START_ARRAY) { + Long[] arr = jp.readValueAs(Long[].class); + if (arr == null || arr.length != 2) { + throw new IOException("Expected [minimum,maximum] array."); + } + return new Length(arr[0], arr[1]); + } + + if (t == JsonToken.START_OBJECT) { + Long min = null; + Long max = null; + + while (jp.nextToken() != JsonToken.END_OBJECT) { + String field = jp.currentName(); + jp.nextToken(); // move to value + if ("min".equals(field)) { + min = jp.currentToken() == JsonToken.VALUE_NULL ? null : jp.getLongValue(); + } else if ("max".equals(field)) { + max = jp.currentToken() == JsonToken.VALUE_NULL ? null : jp.getLongValue(); + } else { + jp.skipChildren(); // ignore unknown fields + } + } + return new Length(min, max); } + + if (t == JsonToken.VALUE_NULL) { + return null; + } + + throw new IOException("Expected [minimum,maximum] array, or {minimum,maximum} object."); } } } \ No newline at end of file diff --git a/src/main/java/com/regexsolver/api/exception/MissingAPITokenException.java b/src/main/java/com/regexsolver/api/exception/MissingAPITokenException.java index ddac5ec..4240691 100644 --- a/src/main/java/com/regexsolver/api/exception/MissingAPITokenException.java +++ b/src/main/java/com/regexsolver/api/exception/MissingAPITokenException.java @@ -5,11 +5,11 @@ */ public class MissingAPITokenException extends RuntimeException { /** - * The API token has not been set, call RegexSolverApiWrapper.initialize(\"YOUR_TOKEN\"); to set it. + * The API token has not been set, set the environment variable REGEXSOLVER_API_TOKEN and call RegexSolverApiWrapper.initialize() to set it. * To generate a token go to RegexSolver Console. */ public MissingAPITokenException() { - super("The API token has not been set, call RegexSolverApiWrapper.initialize(\"YOUR_TOKEN\") to set it.\n" + + super("The API token has not been set, set the environment variable REGEXSOLVER_API_TOKEN and call RegexSolverApiWrapper.initialize() to set it.\n" + "To generate a token go to https://console.regexsolver.com/."); } } diff --git a/src/main/java/com/regexsolver/api/package-info.java b/src/main/java/com/regexsolver/api/package-info.java index 1870e39..62b3b26 100644 --- a/src/main/java/com/regexsolver/api/package-info.java +++ b/src/main/java/com/regexsolver/api/package-info.java @@ -2,7 +2,7 @@ * Contains all the classes you need to start using the library. *

* To start using this library you need to first request an API token at RegexSolver Console, - * then call RegexSolverApiWrapper.initialize("YOUR_TOKEN") to set it. + * set it as environment variable in REGEXSOLVER_API_TOKEN then call RegexSolverApiWrapper.initialize(). *

*

* You can find some examples in our documentation. diff --git a/src/test/java/com/regexsolver/api/IntegrationTest.java b/src/test/java/com/regexsolver/api/IntegrationTest.java new file mode 100644 index 0000000..beba50d --- /dev/null +++ b/src/test/java/com/regexsolver/api/IntegrationTest.java @@ -0,0 +1,201 @@ +package com.regexsolver.api; + +import com.regexsolver.api.dto.Cardinality; +import com.regexsolver.api.dto.Length; +import com.regexsolver.api.exception.ApiError; + +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.*; + +public class IntegrationTest { + @Before + public void setUp() throws Exception { + RegexSolver.initialize(); + } + + // Analyze + + @Test + public void test_analyze_cardinality() throws Exception { + Term term = Term.regex("[0-4]"); + + Cardinality cardinality = term.getCardinality(); + assertEquals("Integer(5)", cardinality.toString()); + } + + @Test + public void test_analyze_dot() throws Exception { + Term term = Term.regex("(abc|de)"); + String dot = term.getDot(); + assertTrue(dot.startsWith("digraph ")); + } + + @Test + public void test_analyze_empty_string() throws Exception { + Term term = Term.regex(""); + boolean result = term.isEmptyString(); + assertTrue(result); + } + + @Test + public void test_analyze_empty() throws Exception { + Term term = Term.regex("[]"); + boolean result = term.isEmpty(); + assertTrue(result); + } + + @Test + public void test_analyze_total() throws Exception { + Term term = Term.regex(".*"); + boolean result = term.isTotal(); + assertTrue(result); + } + + @Test + public void test_analyze_equivalent() throws Exception { + Term term1 = Term.regex("(abc|de)"); + Term term2 = Term.fair( + " strings = term.generateStrings(10); + assertEquals(4, strings.size()); + } + + // README + + @Test + public void test_readme_quickstart() throws Exception { + Term term1 = Term.regex("(abc|de|fg){2,}"); + Term term2 = Term.regex("de.*"); + Term term3 = Term.regex(".*abc"); + + Term result = term1.intersection(term2, term3) + .difference(Term.regex(".+(abc|de).+")); + assertEquals("de(fg)*abc", result.getPattern()); + } + + @Test + public void test_readme_response_format() throws Exception { + Term term = Term.regex("abcde"); + + OperationOptions operationOptions = OperationOptions.newDefault() + .responseFormat(ResponseFormat.REGEX); + Term result1 = term.union(operationOptions, Term.regex("de")); + + assertEquals("regex=(abc)?de", result1.toString()); + + operationOptions = OperationOptions.newDefault() + .responseFormat(ResponseFormat.FAIR); + Term result2 = term.union(operationOptions, Term.regex("de")); + + assertTrue(result2.toString().startsWith("fair=")); + } + + @Test + public void test_readme_execution_timeout() throws Exception { + try { + Term term1 = Term.regex(".*ab.*c(de|fg).*dab.*c(de|fg).*ab.*c(de|fg).*dab.*c"); + Term term2 = Term.regex(".*abc.*"); + + OperationOptions operationOptions = OperationOptions.newDefault() + .executionTimeout(5); + term1.difference(operationOptions, term2); + } catch (ApiError e) { + System.out.println(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/regexsolver/api/TermOperationTest.java b/src/test/java/com/regexsolver/api/TermOperationTest.java index c7b7549..44d4e77 100644 --- a/src/test/java/com/regexsolver/api/TermOperationTest.java +++ b/src/test/java/com/regexsolver/api/TermOperationTest.java @@ -1,18 +1,13 @@ package com.regexsolver.api; -import com.regexsolver.api.dto.Cardinality; -import com.regexsolver.api.dto.Details; -import com.regexsolver.api.dto.Length; import com.regexsolver.api.exception.ApiError; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.IOException; -import java.util.List; import static org.junit.Assert.*; @@ -32,163 +27,6 @@ public void tearDown() throws IOException { server.shutdown(); } - @Test - public void test_getDetails() throws IOException, ApiError, InterruptedException { - int requestCount = server.getRequestCount(); - - MockResponse response = TestUtils.generateMockResponse(TestUtils.getResourceFileContent("response_getDetails.json")); - server.enqueue(response); - - Term.Regex regex = Term.regex("(abc|de)"); - - Details details = regex.getDetails(); - - Cardinality cardinality = details.getCardinality(); - assertTrue(cardinality instanceof Cardinality.Integer); - assertEquals(2, ((Cardinality.Integer) cardinality).getCount()); - - Length length = details.getLength(); - assertEquals(2L, length.getMinimum().getAsLong()); - assertEquals(3L, length.getMaximum().getAsLong()); - - assertFalse(details.isEmpty()); - assertFalse(details.isTotal()); - - RecordedRequest request = server.takeRequest(); - assertEquals("/api/analyze/details", request.getPath()); - assertEquals(regex, TestUtils.readBuffer(request.getBody(), Term.class)); - - Details detailsInCache = regex.getDetails(); - assertEquals(details, detailsInCache); - - assertEquals(1, server.getRequestCount() - requestCount); - } - - @Test - public void test_generateStrings() throws IOException, ApiError, InterruptedException { - MockResponse response = TestUtils.generateMockResponse(TestUtils.getResourceFileContent("response_generateStrings.json")); - server.enqueue(response); - - Term.Regex regex = Term.regex("(abc|de){2}"); - - List strings = regex.generateStrings(10); - assertEquals(4, strings.size()); - - RecordedRequest request = server.takeRequest(); - assertEquals("/api/generate/strings", request.getPath()); - - Request.GenerateStringsRequest generateStringsRequest = TestUtils.readBuffer(request.getBody(), Request.GenerateStringsRequest.class); - assertEquals(10, generateStringsRequest.getCount()); - assertEquals(regex, generateStringsRequest.getTerm()); - } - - @Test - public void test_intersection() throws IOException, ApiError, InterruptedException { - MockResponse response = TestUtils.generateMockResponse(TestUtils.getResourceFileContent("response_intersection.json")); - server.enqueue(response); - - Term.Regex term1 = Term.regex("(abc|de){2}"); - Term.Regex term2 = Term.regex("de.*"); - Term.Regex term3 = Term.regex(".*abc"); - - Term result = term1.intersection(term2, term3); - assertTrue(result instanceof Term.Regex); - assertEquals("deabc", ((Term.Regex) result).getPattern()); - - RecordedRequest request = server.takeRequest(); - assertEquals("/api/compute/intersection", request.getPath()); - - Request.MultiTermsRequest multiTermsRequest = TestUtils.readBuffer(request.getBody(), Request.MultiTermsRequest.class); - List terms = multiTermsRequest.getTerms(); - assertEquals(term1, terms.get(0)); - assertEquals(term2, terms.get(1)); - assertEquals(term3, terms.get(2)); - } - - @Test - public void test_union() throws IOException, ApiError, InterruptedException { - MockResponse response = TestUtils.generateMockResponse(TestUtils.getResourceFileContent("response_union.json")); - server.enqueue(response); - - Term.Regex term1 = Term.regex("abc"); - Term.Regex term2 = Term.regex("de"); - Term.Regex term3 = Term.regex("fghi"); - - Term result = term1.union(term2, term3); - assertTrue(result instanceof Term.Regex); - assertEquals("(abc|de|fghi)", ((Term.Regex) result).getPattern()); - - RecordedRequest request = server.takeRequest(); - assertEquals("/api/compute/union", request.getPath()); - - Request.MultiTermsRequest multiTermsRequest = TestUtils.readBuffer(request.getBody(), Request.MultiTermsRequest.class); - List terms = multiTermsRequest.getTerms(); - assertEquals(term1, terms.get(0)); - assertEquals(term2, terms.get(1)); - assertEquals(term3, terms.get(2)); - } - - @Test - public void test_subtraction() throws IOException, ApiError, InterruptedException { - MockResponse response = TestUtils.generateMockResponse(TestUtils.getResourceFileContent("response_subtraction.json")); - server.enqueue(response); - - Term.Regex term1 = Term.regex("(abc|de)"); - Term.Regex term2 = Term.regex("de"); - - Term result = term1.subtraction(term2); - assertTrue(result instanceof Term.Regex); - assertEquals("abc", ((Term.Regex) result).getPattern()); - - RecordedRequest request = server.takeRequest(); - assertEquals("/api/compute/subtraction", request.getPath()); - - Request.MultiTermsRequest multiTermsRequest = TestUtils.readBuffer(request.getBody(), Request.MultiTermsRequest.class); - List terms = multiTermsRequest.getTerms(); - assertEquals(term1, terms.get(0)); - assertEquals(term2, terms.get(1)); - } - - @Test - public void test_isEquivalentTo() throws IOException, ApiError, InterruptedException { - MockResponse response = TestUtils.generateMockResponse(TestUtils.getResourceFileContent("response_isEquivalentTo.json")); - server.enqueue(response); - - Term.Regex term1 = Term.regex("(abc|de)"); - Term.Fair term2 = Term.fair("rgmsW[1g2LvP=Gr&V>sLc#w-!No&(oq@Sf>X).?lI3{uh{80qWEH[#0.pHq@B-9o[LpP-a#fYI+"); - - boolean result = term1.isEquivalentTo(term2); - assertFalse(result); - - RecordedRequest request = server.takeRequest(); - assertEquals("/api/analyze/equivalence", request.getPath()); - - Request.MultiTermsRequest multiTermsRequest = TestUtils.readBuffer(request.getBody(), Request.MultiTermsRequest.class); - List terms = multiTermsRequest.getTerms(); - assertEquals(term1, terms.get(0)); - assertEquals(term2, terms.get(1)); - } - - @Test - public void test_isSubsetOf() throws IOException, ApiError, InterruptedException { - MockResponse response = TestUtils.generateMockResponse(TestUtils.getResourceFileContent("response_isSubsetOf.json")); - server.enqueue(response); - - Term.Regex term1 = Term.regex("de"); - Term.Regex term2 = Term.regex("(abc|de)"); - - boolean result = term1.isSubsetOf(term2); - assertTrue(result); - - RecordedRequest request = server.takeRequest(); - assertEquals("/api/analyze/subset", request.getPath()); - - Request.MultiTermsRequest multiTermsRequest = TestUtils.readBuffer(request.getBody(), Request.MultiTermsRequest.class); - List terms = multiTermsRequest.getTerms(); - assertEquals(term1, terms.get(0)); - assertEquals(term2, terms.get(1)); - } - @Test public void test_errorResponse() throws IOException { MockResponse response = TestUtils.generateErrorMockResponse(TestUtils.getResourceFileContent("response_error.json"), 400); diff --git a/src/test/resources/response_generateStrings.json b/src/test/resources/response_generateStrings.json deleted file mode 100644 index 9ee8883..0000000 --- a/src/test/resources/response_generateStrings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "type": "strings", - "value": [ - "abcde", - "dede", - "deabc", - "abcabc" - ] -} \ No newline at end of file diff --git a/src/test/resources/response_getDetails.json b/src/test/resources/response_getDetails.json deleted file mode 100644 index 65e0539..0000000 --- a/src/test/resources/response_getDetails.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "details", - "cardinality": { - "type": "Integer", - "value": 2 - }, - "length": [ - 2, - 3 - ], - "empty": false, - "total": false -} \ No newline at end of file diff --git a/src/test/resources/response_intersection.json b/src/test/resources/response_intersection.json deleted file mode 100644 index e6b1a7a..0000000 --- a/src/test/resources/response_intersection.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "regex", - "value": "deabc" -} \ No newline at end of file diff --git a/src/test/resources/response_isEquivalentTo.json b/src/test/resources/response_isEquivalentTo.json deleted file mode 100644 index 25147f3..0000000 --- a/src/test/resources/response_isEquivalentTo.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "boolean", - "value": false -} \ No newline at end of file diff --git a/src/test/resources/response_isSubsetOf.json b/src/test/resources/response_isSubsetOf.json deleted file mode 100644 index 84ed493..0000000 --- a/src/test/resources/response_isSubsetOf.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "boolean", - "value": true -} \ No newline at end of file diff --git a/src/test/resources/response_subtraction.json b/src/test/resources/response_subtraction.json deleted file mode 100644 index 478ac72..0000000 --- a/src/test/resources/response_subtraction.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "regex", - "value": "abc" -} \ No newline at end of file diff --git a/src/test/resources/response_union.json b/src/test/resources/response_union.json deleted file mode 100644 index 27dae5e..0000000 --- a/src/test/resources/response_union.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "regex", - "value": "(abc|de|fghi)" -} \ No newline at end of file