├── .gitignore ├── demo └── src │ ├── main │ ├── resources │ │ └── application.properties │ └── java │ │ ├── TopLevelService.java │ │ ├── io │ │ └── github │ │ │ └── zskamljic │ │ │ └── restahead │ │ │ └── demo │ │ │ ├── models │ │ │ ├── AuthResponse.java │ │ │ ├── ExternalFormBody.java │ │ │ └── HttpBinResponse.java │ │ │ ├── clients │ │ │ ├── InterceptedService.java │ │ │ ├── CustomAdapterService.java │ │ │ ├── FutureService.java │ │ │ ├── BodyResponsesService.java │ │ │ ├── AuthorizationService.java │ │ │ ├── BodyService.java │ │ │ ├── HttpBinMethodsService.java │ │ │ └── FormService.java │ │ │ ├── jaxrs │ │ │ └── JaxRsService.java │ │ │ ├── spring │ │ │ ├── SpringService.java │ │ │ ├── SpringApplicationDemo.java │ │ │ ├── DemoService.java │ │ │ ├── DemoController.java │ │ │ ├── DummyClient.java │ │ │ └── ConfigCombinations.java │ │ │ ├── interceptors │ │ │ ├── PreRequestInterceptor.java │ │ │ └── PostRequestInterceptor.java │ │ │ └── adapters │ │ │ └── SupplierAdapter.java │ │ └── TopLevel.java │ └── test │ └── java │ └── io │ └── github │ └── zskamljic │ └── restahead │ └── demo │ ├── spring │ ├── DemoControllerTest.java │ ├── PlaceholderServiceBeanTest.java │ └── ConfigCombinationsTest.java │ ├── clients │ ├── CustomAdapterServiceTest.java │ ├── BodyResponsesServiceTest.java │ ├── FutureServiceTest.java │ ├── BodyServiceTest.java │ └── AuthorizationServiceTest.java │ └── interceptors │ ├── PreRequestInterceptorTest.java │ └── PostRequestInterceptorTest.java ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── rest-ahead-processor └── src │ ├── main │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ ├── javax.annotation.processing.Processor │ │ │ └── io.github.zskamljic.restahead.polyglot.Dialect │ └── java │ │ └── io │ │ └── github │ │ └── zskamljic │ │ └── restahead │ │ ├── modeling │ │ ├── parameters │ │ │ ├── PartData.java │ │ │ ├── RequestParameter.java │ │ │ └── ParameterWithExceptions.java │ │ ├── conversion │ │ │ ├── OptionsConversion.java │ │ │ ├── Conversion.java │ │ │ ├── DirectConversion.java │ │ │ ├── BodyResponseConversion.java │ │ │ └── BodyAndErrorConversion.java │ │ ├── declaration │ │ │ ├── BodyParameter.java │ │ │ ├── ReturnAdapterCall.java │ │ │ ├── RequestParameterSpec.java │ │ │ ├── ReturnDeclaration.java │ │ │ ├── AdapterClassDeclaration.java │ │ │ ├── AdapterMethodDeclaration.java │ │ │ ├── ParameterDeclaration.java │ │ │ ├── CallDeclaration.java │ │ │ └── ServiceDeclaration.java │ │ └── validation │ │ │ ├── QueryValidator.java │ │ │ └── HeaderValidator.java │ │ ├── annotations │ │ ├── request │ │ │ ├── Headers.java │ │ │ ├── Header.java │ │ │ ├── Query.java │ │ │ ├── Body.java │ │ │ └── Path.java │ │ ├── verbs │ │ │ ├── Options.java │ │ │ ├── Get.java │ │ │ ├── Head.java │ │ │ ├── Post.java │ │ │ ├── Put.java │ │ │ ├── Patch.java │ │ │ └── Delete.java │ │ ├── form │ │ │ ├── Part.java │ │ │ ├── FormName.java │ │ │ └── FormUrlEncoded.java │ │ └── Adapter.java │ │ ├── encoding │ │ ├── BodyEncoding.java │ │ ├── FormBodyEncoding.java │ │ ├── ConvertBodyEncoding.java │ │ ├── MultiPartBodyEncoding.java │ │ ├── MultiPartParameter.java │ │ └── generation │ │ │ ├── FormConversionStrategy.java │ │ │ └── MapConversionStrategy.java │ │ ├── request │ │ ├── PresetValue.java │ │ ├── RequestLine.java │ │ ├── RequestSpec.java │ │ ├── BasicRequestLine.java │ │ └── path │ │ │ ├── StringPath.java │ │ │ ├── TemplatedPath.java │ │ │ └── RequestPath.java │ │ ├── polyglot │ │ ├── CompositeProcessingException.java │ │ └── ProcessingException.java │ │ └── generation │ │ ├── Variables.java │ │ └── ExceptionsGenerator.java │ └── test │ └── java │ └── io │ └── github │ └── zskamljic │ └── restahead │ ├── request │ └── path │ │ └── TemplatedPathTest.java │ └── generation │ └── methods │ ├── HeaderValidatorTest.java │ └── PathValidatorTest.java ├── rest-ahead-jax-rs ├── src │ └── main │ │ └── resources │ │ └── META-INF │ │ └── services │ │ └── io.github.zskamljic.restahead.polyglot.Dialect └── pom.xml ├── rest-ahead-spring-dialect ├── src │ └── main │ │ └── resources │ │ └── META-INF │ │ └── services │ │ └── io.github.zskamljic.restahead.polyglot.Dialect └── pom.xml ├── rest-ahead-spring ├── src │ └── main │ │ └── java │ │ └── io │ │ └── github │ │ └── zskamljic │ │ └── restahead │ │ └── spring │ │ ├── package-info.java │ │ ├── EnableRestAhead.java │ │ └── RestAheadService.java └── pom.xml ├── test-report-aggregator └── src │ └── test │ ├── resources │ ├── ValidService.java │ ├── query │ │ ├── QueryInPath.java │ │ ├── MissingQueryValue.java │ │ ├── MissingName.java │ │ ├── ValidQuery.java │ │ ├── EnumQuery.java │ │ ├── ValidCombinedQuery.java │ │ └── CollectionAndArray.java │ ├── basic │ │ ├── MethodClass.java │ │ ├── MethodService.java │ │ ├── NormalClassMethod.java │ │ ├── HeadObjectService.java │ │ ├── InterfaceWithNotAnnotatedMethod.java │ │ ├── MultipleAnnotations.java │ │ ├── InterfaceWithThrows.java │ │ ├── OptionsObjectService.java │ │ ├── HeadService.java │ │ ├── OptionsService.java │ │ └── InterfaceWithThrows$Impl.java │ ├── path │ │ ├── InvalidPath.java │ │ ├── PathAnnotation.java │ │ ├── PathWithIterable.java │ │ ├── PathAnnotationCodeName.java │ │ ├── PathWithDuplicates.java │ │ ├── PathWithoutPlaceholder.java │ │ ├── PathAnnotation$Impl.java │ │ └── PathAnnotationCodeName$Impl.java │ ├── headers │ │ ├── HeadersServiceInvalid.java │ │ ├── EmptyHeader.java │ │ ├── InvalidHeader.java │ │ ├── StringHeader.java │ │ ├── InvalidArrayHeader.java │ │ ├── UuidHeader.java │ │ ├── HeadersService.java │ │ ├── ValidArrayHeader.java │ │ ├── InvalidCollectionHeader.java │ │ ├── ValidCollectionHeader.java │ │ ├── PrimitiveHeader.java │ │ ├── BoxedHeader.java │ │ └── HeadersService$Impl.java │ ├── response │ │ ├── ServiceWithGenericResponse.java │ │ ├── ServiceWithResponse.java │ │ ├── ServiceWithUnknownResponse.java │ │ ├── FutureGenericResponse.java │ │ ├── ResponseWithBody.java │ │ ├── ResponseWithErrorBody.java │ │ ├── FutureGenericResponse$Impl.java │ │ └── ServiceWithUnknownResponse$Impl.java │ ├── parameters │ │ ├── BodyService.java │ │ ├── FormWithPart.java │ │ ├── FormWithPartInvalidType.java │ │ ├── FormOnInvalid.java │ │ ├── FormOnInvalidMap.java │ │ ├── FormAndPartSameField.java │ │ ├── FormOnRecordInvalid.java │ │ ├── Parameters.java │ │ ├── FormOnWithWrongField.java │ │ ├── FormOnRecord.java │ │ ├── FormOnClassInvalid.java │ │ ├── FormOnValidMap.java │ │ ├── FormOnClass.java │ │ ├── FormWithPart$Impl.java │ │ ├── BodyService$Impl.java │ │ └── FormOnClass$Impl.java │ ├── adapters │ │ ├── AdapterService.java │ │ └── AdapterService$Impl.java │ ├── jaxrs │ │ └── JaxRsService.java │ └── spring │ │ └── SpringService.java │ └── java │ └── io │ └── github │ └── zskamljic │ └── restahead │ └── processor │ ├── stock │ ├── ParametersProcessorTest.java │ ├── AdapterProcessorTest.java │ ├── QueryProcessorTest.java │ ├── PathProcessorTest.java │ └── ResponseProcessorTest.java │ ├── jaxrs │ └── JaxRsDialectTest.java │ ├── spring │ └── SpringDialectTest.java │ ├── UnclaimedProcessor.java │ └── CommonProcessorTest.java ├── rest-ahead-client ├── src │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── zskamljic │ │ │ └── restahead │ │ │ ├── intercepting │ │ │ ├── logging │ │ │ │ ├── BodyLoggingException.java │ │ │ │ └── RequestLogger.java │ │ │ ├── Interceptor.java │ │ │ └── Chain.java │ │ │ ├── client │ │ │ ├── responses │ │ │ │ ├── Response.java │ │ │ │ ├── BodyResponse.java │ │ │ │ └── BodyAndErrorResponse.java │ │ │ ├── requests │ │ │ │ ├── Verb.java │ │ │ │ ├── parts │ │ │ │ │ ├── FieldPart.java │ │ │ │ │ ├── MultiPart.java │ │ │ │ │ └── FilePart.java │ │ │ │ └── MultiPartRequest.java │ │ │ ├── Client.java │ │ │ └── JavaHttpClient.java │ │ │ ├── exceptions │ │ │ ├── RequestFailedException.java │ │ │ └── RestException.java │ │ │ ├── conversion │ │ │ ├── GenericReference.java │ │ │ ├── Converter.java │ │ │ ├── MapFormConverter.java │ │ │ └── OptionsConverter.java │ │ │ ├── util │ │ │ └── StringMultiMap.java │ │ │ └── adapter │ │ │ └── DefaultAdapters.java │ └── test │ │ └── java │ │ └── io │ │ └── github │ │ └── zskamljic │ │ └── restahead │ │ ├── conversion │ │ ├── GenericReferenceTest.java │ │ └── OptionsConverterTest.java │ │ ├── RestAheadTest.java │ │ └── client │ │ └── requests │ │ └── parts │ │ ├── FilePartTest.java │ │ └── FieldPartTest.java └── pom.xml ├── LICENSE ├── Dialects.md ├── .github └── workflows │ ├── maven-publish.yml │ └── sonar.yml └── rest-ahead-jackson-converter ├── src ├── test │ └── java │ │ └── io │ │ └── github │ │ └── zskamljic │ │ └── restahead │ │ └── JacksonConverterTest.java └── main │ └── java │ └── io │ └── github │ └── zskamljic │ └── restahead │ └── JacksonConverter.java └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/** 2 | **.iml 3 | **.class 4 | */target/** 5 | target/** 6 | -------------------------------------------------------------------------------- /demo/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | placeholder.url=https://httpbin.org -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zskamljic/rest-ahead/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | io.github.zskamljic.restahead.processor.RequestsProcessor -------------------------------------------------------------------------------- /rest-ahead-jax-rs/src/main/resources/META-INF/services/io.github.zskamljic.restahead.polyglot.Dialect: -------------------------------------------------------------------------------- 1 | io.github.zskamljic.restahead.jaxrs.dialect.JaxRsDialect -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/resources/META-INF/services/io.github.zskamljic.restahead.polyglot.Dialect: -------------------------------------------------------------------------------- 1 | io.github.zskamljic.restahead.polyglot.RestAheadDialect -------------------------------------------------------------------------------- /rest-ahead-spring-dialect/src/main/resources/META-INF/services/io.github.zskamljic.restahead.polyglot.Dialect: -------------------------------------------------------------------------------- 1 | io.github.zskamljic.restahead.spring.dialect.SpringDialect -------------------------------------------------------------------------------- /demo/src/main/java/TopLevelService.java: -------------------------------------------------------------------------------- 1 | import io.github.zskamljic.restahead.annotations.verbs.Get; 2 | 3 | public interface TopLevelService { 4 | @Get("/get") 5 | void get(); 6 | } 7 | -------------------------------------------------------------------------------- /rest-ahead-spring/src/main/java/io/github/zskamljic/restahead/spring/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | package io.github.zskamljic.restahead.spring; 3 | 4 | import org.springframework.lang.NonNullApi; -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/models/AuthResponse.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.models; 2 | 3 | public record AuthResponse( 4 | boolean authenticated, 5 | String token 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/parameters/PartData.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.parameters; 2 | 3 | /** 4 | * Contains name of the part. 5 | */ 6 | public record PartData(String value) { 7 | } 8 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/ValidService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | 5 | public interface ValidService { 6 | @Delete("/delete") 7 | void delete(); 8 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/query/QueryInPath.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | 5 | public interface QueryInPath { 6 | @Delete("/delete?q=1") 7 | void delete(); 8 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/request/Headers.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.request; 2 | 3 | /** 4 | * Add static headers to the request. 5 | */ 6 | public @interface Headers { 7 | String[] value(); 8 | } 9 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/basic/MethodClass.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | 5 | public abstract class MethodClass { 6 | @Delete("/delete") 7 | abstract void delete(); 8 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/path/InvalidPath.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | 5 | public interface InvalidPath { 6 | @Delete("/delete with invalid path") 7 | void delete(); 8 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/query/MissingQueryValue.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | 5 | public interface MissingQueryValue { 6 | @Delete("/delete?q=") 7 | void delete(); 8 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/basic/MethodService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | 5 | public interface MethodService { 6 | @Delete("/delete") 7 | default void delete() { 8 | } 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/basic/NormalClassMethod.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | 5 | public class NormalClassMethod { 6 | @Delete("/delete") 7 | void delete() { 8 | } 9 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/models/ExternalFormBody.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.models; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.FormName; 4 | 5 | public record ExternalFormBody( 6 | @FormName("snake_case") String field 7 | ) { 8 | } 9 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/parameters/RequestParameter.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.parameters; 2 | 3 | public record RequestParameter(Type type, String value) { 4 | public enum Type { 5 | HEADER, QUERY, PATH 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/conversion/OptionsConversion.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.conversion; 2 | 3 | import javax.lang.model.type.TypeMirror; 4 | 5 | public record OptionsConversion(TypeMirror verbListType) implements Conversion { 6 | } 7 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/encoding/BodyEncoding.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.encoding; 2 | 3 | /** 4 | * Used to specify types of encoding. 5 | */ 6 | public sealed interface BodyEncoding permits ConvertBodyEncoding, FormBodyEncoding, MultiPartBodyEncoding { 7 | } 8 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/basic/HeadObjectService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import java.util.Map; 4 | 5 | import io.github.zskamljic.restahead.annotations.verbs.Head; 6 | 7 | public interface HeadObjectService { 8 | @Head 9 | Map head(); 10 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/HeadersServiceInvalid.java: -------------------------------------------------------------------------------- 1 | import io.github.zskamljic.restahead.annotations.request.Headers; 2 | import io.github.zskamljic.restahead.annotations.verbs.Get; 3 | 4 | interface HeadersService { 5 | @Get 6 | @Headers("Invalid value") 7 | void performGetMultipleValues(); 8 | } -------------------------------------------------------------------------------- /demo/src/main/java/TopLevel.java: -------------------------------------------------------------------------------- 1 | import io.github.zskamljic.restahead.RestAhead; 2 | 3 | public class TopLevel { 4 | public static void main(String[] args) { 5 | var service = RestAhead.builder("https://httpbin.org") 6 | .build(TopLevelService.class); 7 | 8 | service.get(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/intercepting/logging/BodyLoggingException.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.intercepting.logging; 2 | 3 | public class BodyLoggingException extends RuntimeException { 4 | public BodyLoggingException(Exception exception) { 5 | super(exception); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/request/PresetValue.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.request; 2 | 3 | /** 4 | * Preset query information. 5 | * 6 | * @param name the query parameter name 7 | * @param value the value for the field 8 | */ 9 | public record PresetValue(String name, String value) { 10 | } 11 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/basic/InterfaceWithNotAnnotatedMethod.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | 5 | public interface InterfaceWithNotAnnotatedMethod { 6 | @Delete("/delete") 7 | void delete(); 8 | 9 | void missingAnnotation(); 10 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/query/MissingName.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Query; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface MissingName { 7 | @Delete("/delete") 8 | void delete(@Query("") String query); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/query/ValidQuery.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Query; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface ValidQuery { 7 | @Delete("/delete") 8 | void delete(@Query("q") String query); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/response/ServiceWithGenericResponse.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | 5 | import java.util.Map; 6 | 7 | public interface ServiceWithGenericResponse { 8 | @Delete("/delete") 9 | Map delete(); 10 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/response/ServiceWithResponse.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | import io.github.zskamljic.restahead.client.responses.Response; 5 | 6 | public interface ServiceWithResponse { 7 | @Delete("/delete") 8 | Response delete(); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/EmptyHeader.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface EmptyHeader { 7 | @Delete("/delete") 8 | void delete(@Header("") String header); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/path/PathAnnotation.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Path; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface PathAnnotation { 7 | @Delete("/{path}") 8 | void delete(@Path("path") String path); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/path/PathWithIterable.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Path; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface PathWithIterable { 7 | @Delete("/{path}") 8 | void delete(@Path("path") String[] path); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/query/EnumQuery.java: -------------------------------------------------------------------------------- 1 | import io.github.zskamljic.restahead.annotations.request.Query; 2 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 3 | 4 | public interface EnumQuery { 5 | @Delete("/delete") 6 | void delete(@Query("q") Sample query); 7 | 8 | enum Sample { 9 | ONE 10 | } 11 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/response/ServiceWithUnknownResponse.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | 5 | public interface ServiceWithUnknownResponse { 6 | @Delete("/delete") 7 | TestResponse delete(); 8 | 9 | record TestResponse() { 10 | } 11 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/InvalidHeader.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface InvalidHeader { 7 | @Delete("/delete") 8 | void delete(@Header("Accept") Object header); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/StringHeader.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface StringHeader { 7 | @Delete("/delete") 8 | void delete(@Header("Accept") String header); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/path/PathAnnotationCodeName.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Path; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface PathAnnotationCodeName { 7 | @Delete("/{path}") 8 | void delete(@Path String path); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/path/PathWithDuplicates.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Path; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface PathWithDuplicates { 7 | @Delete("/{path}/{path}") 8 | void delete(@Path("path") String path); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/path/PathWithoutPlaceholder.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Path; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface PathWithoutPlaceholder { 7 | @Delete("/delete") 8 | void delete(@Path("path") String path); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/query/ValidCombinedQuery.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Query; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface ValidCombinedQuery { 7 | @Delete("/delete?q=1") 8 | void delete(@Query("q") String query); 9 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/clients/InterceptedService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Get; 4 | import io.github.zskamljic.restahead.client.responses.Response; 5 | 6 | public interface InterceptedService { 7 | @Get("/invalid") 8 | Response get(); 9 | } 10 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/conversion/Conversion.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.conversion; 2 | 3 | /** 4 | * Contains strategies to use for converting the request body. 5 | */ 6 | public sealed interface Conversion permits BodyAndErrorConversion, BodyResponseConversion, DirectConversion, OptionsConversion { 7 | } 8 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/basic/MultipleAnnotations.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | import io.github.zskamljic.restahead.annotations.verbs.Patch; 5 | 6 | public interface MultipleAnnotations { 7 | @Delete("/delete") 8 | @Patch("/delete") 9 | void delete(); 10 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/InvalidArrayHeader.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface InvalidArrayHeader { 7 | @Delete("/delete") 8 | void delete(@Header("Accept") Object[] header); 9 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/response/FutureGenericResponse.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | 5 | import java.util.Map; 6 | import java.util.concurrent.Future; 7 | 8 | public interface FutureGenericResponse { 9 | @Delete("/delete") 10 | Future> delete(); 11 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/BodyService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Body; 4 | import io.github.zskamljic.restahead.annotations.verbs.Post; 5 | 6 | import java.util.Map; 7 | 8 | public interface BodyService { 9 | @Post("/post") 10 | void post(@Body Map body); 11 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/UuidHeader.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | import java.util.UUID; 7 | 8 | public interface UuidHeader { 9 | @Delete("/delete") 10 | void delete(@Header("Accept") UUID header); 11 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/basic/InterfaceWithThrows.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | import io.github.zskamljic.restahead.client.responses.Response; 5 | 6 | import java.io.IOException; 7 | 8 | public interface InterfaceWithThrows { 9 | @Delete("/delete") 10 | Response delete() throws IOException; 11 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormWithPart.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.Part; 4 | import io.github.zskamljic.restahead.annotations.request.Body; 5 | import io.github.zskamljic.restahead.annotations.verbs.Post; 6 | 7 | public interface FormWithPart { 8 | @Post 9 | void post(@Body @Part String body); 10 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormWithPartInvalidType.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.Part; 4 | import io.github.zskamljic.restahead.annotations.request.Body; 5 | import io.github.zskamljic.restahead.annotations.verbs.Post; 6 | 7 | public interface FormWithPartInvalidType { 8 | @Post 9 | void post(@Body @Part Object body); 10 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/clients/CustomAdapterService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Get; 4 | import io.github.zskamljic.restahead.demo.models.HttpBinResponse; 5 | 6 | import java.util.function.Supplier; 7 | 8 | public interface CustomAdapterService { 9 | @Get("/get") 10 | Supplier get(); 11 | } 12 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/jaxrs/JaxRsService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.jaxrs; 2 | 3 | import jakarta.ws.rs.GET; 4 | import jakarta.ws.rs.Path; 5 | import jakarta.ws.rs.PathParam; 6 | 7 | import java.util.Map; 8 | 9 | public interface JaxRsService { 10 | @GET 11 | @Path(("/get/{something}")) 12 | Map performGet(@PathParam("something") String something); 13 | } 14 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/HeadersService.java: -------------------------------------------------------------------------------- 1 | import io.github.zskamljic.restahead.annotations.request.Headers; 2 | import io.github.zskamljic.restahead.annotations.verbs.Get; 3 | 4 | interface HeadersService { 5 | @Get 6 | @Headers("Authorization: none") 7 | void performGet(); 8 | 9 | @Get 10 | @Headers({"Authorization: none", "Authorization: some", "Test: value"}) 11 | void performGetMultipleValues(); 12 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormOnInvalid.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.FormUrlEncoded; 4 | import io.github.zskamljic.restahead.annotations.request.Header; 5 | import io.github.zskamljic.restahead.annotations.verbs.Post; 6 | 7 | public interface FormOnInvalid { 8 | @Post 9 | void post(@FormUrlEncoded @Header("head") String multiAnnotated); 10 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/models/HttpBinResponse.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.models; 2 | 3 | import java.util.Map; 4 | 5 | public record HttpBinResponse( 6 | Map args, 7 | String data, 8 | Map files, 9 | Map form, 10 | Map headers, 11 | Map json, 12 | String origin, 13 | String url 14 | ) { 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/conversion/DirectConversion.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.conversion; 2 | 3 | import javax.lang.model.type.TypeMirror; 4 | 5 | /** 6 | * Used for converting the value directly, where no special handling is required. 7 | * 8 | * @param targetType the type to convert to. 9 | */ 10 | public record DirectConversion(TypeMirror targetType) implements Conversion { 11 | } 12 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/declaration/BodyParameter.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.declaration; 2 | 3 | import javax.lang.model.element.VariableElement; 4 | 5 | public record BodyParameter( 6 | VariableElement parameter, 7 | String httpName, 8 | String name, 9 | Type type 10 | ) { 11 | public enum Type { 12 | CONVERT, 13 | FORM, 14 | MULTIPART 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/spring/SpringService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.spring; 2 | 3 | import io.github.zskamljic.restahead.client.responses.Response; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestHeader; 6 | 7 | public interface SpringService { 8 | @GetMapping("/get") 9 | Response performGet(@RequestHeader("Authorization") String value); 10 | } 11 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/verbs/Options.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.verbs; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.SOURCE) 9 | @Target(ElementType.METHOD) 10 | public @interface Options { 11 | String value() default ""; 12 | } 13 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/conversion/BodyResponseConversion.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.conversion; 2 | 3 | import javax.lang.model.type.TypeMirror; 4 | 5 | /** 6 | * Used to transfer the body type to the code generator. 7 | * 8 | * @param targetType the type to convert the body if request is successful 9 | */ 10 | public record BodyResponseConversion(TypeMirror targetType) implements Conversion { 11 | } 12 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormOnInvalidMap.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.FormUrlEncoded; 4 | import io.github.zskamljic.restahead.annotations.request.Body; 5 | import io.github.zskamljic.restahead.annotations.verbs.Post; 6 | 7 | import java.util.Map; 8 | 9 | public interface FormOnInvalidMap { 10 | @Post 11 | void post(@FormUrlEncoded @Body Map body); 12 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/basic/OptionsObjectService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Head; 4 | import io.github.zskamljic.restahead.annotations.verbs.Options; 5 | import io.github.zskamljic.restahead.client.responses.BodyResponse; 6 | 7 | public interface OptionsObjectService { 8 | @Options 9 | Map options(); 10 | 11 | @Options 12 | BodyResponse options2(); 13 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/request/RequestLine.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.request; 2 | 3 | import io.github.zskamljic.restahead.client.requests.Verb; 4 | import io.github.zskamljic.restahead.request.path.RequestPath; 5 | 6 | /** 7 | * The processed request line. 8 | * 9 | * @param verb the verb to use for the request 10 | * @param path 11 | */ 12 | public record RequestLine( 13 | Verb verb, 14 | RequestPath path 15 | ) { 16 | } 17 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/basic/HeadService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Head; 4 | import io.github.zskamljic.restahead.client.responses.BodyResponse; 5 | import io.github.zskamljic.restahead.client.responses.Response; 6 | 7 | public interface HeadService { 8 | @Head 9 | void head(); 10 | 11 | @Head 12 | BodyResponse head2(); 13 | 14 | @Head 15 | Response head3(); 16 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/declaration/ReturnAdapterCall.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.declaration; 2 | 3 | /** 4 | * The type and method that should be called for a return value. 5 | * 6 | * @param adapterClass the type containing the function 7 | * @param adapterMethod the method to call 8 | */ 9 | public record ReturnAdapterCall( 10 | AdapterClassDeclaration adapterClass, 11 | AdapterMethodDeclaration adapterMethod 12 | ) { 13 | } 14 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/request/RequestSpec.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.request; 2 | 3 | import io.github.zskamljic.restahead.modeling.declaration.ParameterDeclaration; 4 | 5 | /** 6 | * Call specification 7 | * 8 | * @param requestLine the verb and path 9 | * @param parameters the function parts that need to be declared 10 | */ 11 | public record RequestSpec( 12 | BasicRequestLine requestLine, 13 | ParameterDeclaration parameters 14 | ) { 15 | } 16 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormAndPartSameField.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.FormUrlEncoded; 4 | import io.github.zskamljic.restahead.annotations.form.Part; 5 | import io.github.zskamljic.restahead.annotations.request.Body; 6 | import io.github.zskamljic.restahead.annotations.verbs.Post; 7 | 8 | public interface FormAndPartSameField { 9 | @Post 10 | void post(@FormUrlEncoded @Body @Part String body); 11 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormOnRecordInvalid.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.FormUrlEncoded; 4 | import io.github.zskamljic.restahead.annotations.request.Body; 5 | import io.github.zskamljic.restahead.annotations.verbs.Post; 6 | 7 | public interface FormOnRecordInvalid { 8 | @Post 9 | void post(@FormUrlEncoded @Body Sample body); 10 | 11 | record Sample(String first, Object second) { 12 | } 13 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/request/Header.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.request; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Add a header to the request. 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.PARAMETER) 13 | public @interface Header { 14 | String value(); 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/request/Query.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.request; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Add a query parameter for the value. 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.PARAMETER) 13 | public @interface Query { 14 | String value(); 15 | } 16 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/basic/OptionsService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Options; 4 | import io.github.zskamljic.restahead.client.requests.Verb; 5 | import io.github.zskamljic.restahead.client.responses.Response; 6 | 7 | import java.util.List; 8 | 9 | public interface OptionsService { 10 | @Options 11 | void options(); 12 | 13 | @Options 14 | List options2(); 15 | 16 | @Options 17 | Response options3(); 18 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/response/ResponseWithBody.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | import io.github.zskamljic.restahead.client.responses.BodyResponse; 5 | 6 | import java.util.Map; 7 | 8 | public interface ResponseWithBody { 9 | @Delete("/delete") 10 | BodyResponse delete(); 11 | 12 | @Delete("/delete") 13 | BodyResponse> deleteMap(); 14 | 15 | record SomeObject() { 16 | } 17 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/request/Body.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.request; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Specifies that the annotated parameter should be treated as request body. 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.PARAMETER) 13 | public @interface Body { 14 | } 15 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/request/BasicRequestLine.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.request; 2 | 3 | import io.github.zskamljic.restahead.client.requests.Verb; 4 | 5 | /** 6 | * Contains the request verb and path. 7 | * 8 | * @param verb the verb for the request 9 | * @param path the path for the request 10 | */ 11 | public record BasicRequestLine( 12 | Verb verb, 13 | String path 14 | ) { 15 | public boolean allowsBody() { 16 | return verb.allowsBody(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/form/Part.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.form; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Represents a part parameter for multipart requests. 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.PARAMETER) 13 | public @interface Part { 14 | String value() default ""; 15 | } 16 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/ValidArrayHeader.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface ValidArrayHeader { 7 | @Delete("/delete") 8 | void delete(@Header("Accept") String[] header); 9 | 10 | @Delete("/delete") 11 | void delete(@Header("Accept") int[] header); 12 | 13 | @Delete("/delete") 14 | void delete(@Header("Accept") Integer[] header); 15 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/verbs/Get.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.verbs; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Perform a GET request on path specified by param or "/" if not set 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.METHOD) 13 | public @interface Get { 14 | String value() default ""; 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/verbs/Head.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.verbs; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Perform a HEAD request on path specified by param or "/" if not set 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.METHOD) 13 | public @interface Head { 14 | String value() default ""; 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/verbs/Post.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.verbs; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Perform a POST request on path specified by param or "/" if not set 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.METHOD) 13 | public @interface Post { 14 | String value() default ""; 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/verbs/Put.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.verbs; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Perform a PUT request on path specified by param or "/" if not set 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.METHOD) 13 | public @interface Put { 14 | String value() default ""; 15 | } 16 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/InvalidCollectionHeader.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | import java.util.Collection; 7 | import java.util.List; 8 | 9 | public interface InvalidCollectionHeader { 10 | @Delete("/delete") 11 | void delete(@Header("Accept") List header); 12 | 13 | @Delete("/delete") 14 | void delete(@Header("Accept") Collection header); 15 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/client/responses/Response.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client.responses; 2 | 3 | import java.io.InputStream; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | /** 8 | * Response of an HTTP request. 9 | * 10 | * @param status the status code of the request 11 | * @param headers the response headers 12 | * @param body the response body 13 | */ 14 | public record Response( 15 | int status, 16 | Map> headers, 17 | InputStream body 18 | ) { 19 | } 20 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/Adapter.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Marks the function to use as an adapter during code generation. Instance of the class will need to be provided. 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.METHOD) 13 | public @interface Adapter { 14 | } 15 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/verbs/Patch.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.verbs; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Perform a PATCH request on path specified by param or "/" if not set 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.METHOD) 13 | public @interface Patch { 14 | String value() default ""; 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/polyglot/CompositeProcessingException.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.polyglot; 2 | 3 | import java.util.List; 4 | 5 | public class CompositeProcessingException extends Exception { 6 | private final List exceptions; 7 | 8 | public CompositeProcessingException(List exceptions) { 9 | this.exceptions = exceptions; 10 | } 11 | 12 | public List getExceptions() { 13 | return exceptions; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/clients/FutureService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Get; 4 | import io.github.zskamljic.restahead.demo.models.HttpBinResponse; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.concurrent.Future; 8 | 9 | public interface FutureService { 10 | @Get("/get") 11 | Future getFuture(); 12 | 13 | @Get("/get") 14 | CompletableFuture getCompletableFuture(); 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/form/FormName.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.form; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Declares that the method, when used in form body, should use the specified name. 10 | */ 11 | @Retention(RetentionPolicy.CLASS) 12 | @Target(ElementType.METHOD) 13 | public @interface FormName { 14 | String value(); 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/verbs/Delete.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.verbs; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Perform a DELETE request on path specified by param or "/" if not set 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.METHOD) 13 | public @interface Delete { 14 | String value() default ""; 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/encoding/FormBodyEncoding.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.encoding; 2 | 3 | import io.github.zskamljic.restahead.encoding.generation.FormConversionStrategy; 4 | 5 | /** 6 | * Specifies that the type should use form encoding. 7 | * 8 | * @param parameterName the name of parameter to encode 9 | * @param strategy the strategy to use when generating the conversion code 10 | */ 11 | public record FormBodyEncoding(String parameterName, FormConversionStrategy strategy) implements BodyEncoding { 12 | } 13 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/conversion/BodyAndErrorConversion.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.conversion; 2 | 3 | import javax.lang.model.type.TypeMirror; 4 | 5 | /** 6 | * Used to transfer both body and error body types to the code generator for deserialization. 7 | * 8 | * @param bodyType the type to convert in case of success 9 | * @param errorType the type to convert in case of error 10 | */ 11 | public record BodyAndErrorConversion(TypeMirror bodyType, TypeMirror errorType) implements Conversion { 12 | } 13 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/Parameters.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.request.Query; 5 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 6 | import io.github.zskamljic.restahead.annotations.verbs.Post; 7 | 8 | public interface Parameters { 9 | @Delete("/delete") 10 | void delete(String parameter); 11 | 12 | @Post 13 | void post(@Header("head") @Query("hello") String multiAnnotated); 14 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/form/FormUrlEncoded.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.form; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Specifies that the body parameter should be form url encoded and to set Content-Type appropriately. 10 | */ 11 | @Target(ElementType.PARAMETER) 12 | @Retention(RetentionPolicy.SOURCE) 13 | public @interface FormUrlEncoded { 14 | } 15 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/encoding/ConvertBodyEncoding.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.encoding; 2 | 3 | import javax.lang.model.type.TypeMirror; 4 | import java.util.List; 5 | 6 | /** 7 | * Encoding for when type needs to be converted using external converters. 8 | * 9 | * @param parameterName the name of the parameter to encode 10 | * @param exceptions the exceptions that can be thrown by the converter 11 | */ 12 | public record ConvertBodyEncoding(String parameterName, List exceptions) implements BodyEncoding { 13 | } 14 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/encoding/MultiPartBodyEncoding.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.encoding; 2 | 3 | import javax.lang.model.type.TypeMirror; 4 | import java.util.List; 5 | 6 | /** 7 | * Represents encoding for multipart bodies. 8 | * 9 | * @param parts the parts of the body 10 | * @param exceptions the exceptions that can be thrown when constructing the body 11 | */ 12 | public record MultiPartBodyEncoding( 13 | List parts, 14 | List exceptions 15 | ) implements BodyEncoding { 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Zan Skamljic 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormOnWithWrongField.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.FormName; 4 | import io.github.zskamljic.restahead.annotations.form.FormUrlEncoded; 5 | import io.github.zskamljic.restahead.annotations.request.Body; 6 | import io.github.zskamljic.restahead.annotations.verbs.Post; 7 | 8 | public interface FormOnWithWrongField { 9 | @Post 10 | void post(@FormUrlEncoded @Body Sample body); 11 | 12 | record Sample(String first, @FormName("") String second) { 13 | } 14 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/response/ResponseWithErrorBody.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 4 | import io.github.zskamljic.restahead.client.responses.BodyAndErrorResponse; 5 | 6 | import java.util.Map; 7 | 8 | public interface ResponseWithErrorBody { 9 | @Delete("/delete") 10 | BodyAndErrorResponse delete(); 11 | 12 | @Delete("/delete") 13 | BodyAndErrorResponse, Map> deleteMap(); 14 | 15 | record SomeObject() { 16 | } 17 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/spring/SpringApplicationDemo.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.spring; 2 | 3 | import io.github.zskamljic.restahead.spring.EnableRestAhead; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | /** 8 | * Spring boot app runner. 9 | */ 10 | @EnableRestAhead 11 | @SpringBootApplication 12 | public class SpringApplicationDemo { 13 | public static void main(String[] args) { 14 | SpringApplication.run(SpringApplicationDemo.class, args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/declaration/RequestParameterSpec.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.declaration; 2 | 3 | /** 4 | * Contains the header information. 5 | * 6 | * @param httpName the name of the parameter in HTTP request 7 | * @param codeName the name of the parameter in code, to be passed to the request 8 | * @param isIterable whether the parameter is iterable (needs to be used in a loop) 9 | */ 10 | public record RequestParameterSpec( 11 | String httpName, 12 | String codeName, 13 | boolean isIterable 14 | ) { 15 | } 16 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/java/io/github/zskamljic/restahead/processor/stock/ParametersProcessorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.processor.stock; 2 | 3 | import io.github.zskamljic.restahead.processor.CommonProcessorTest; 4 | import org.junit.jupiter.api.Test; 5 | 6 | class ParametersProcessorTest extends CommonProcessorTest { 7 | @Test 8 | void interfaceWithInvalidPathFailsToCompile() { 9 | commonCompilationAssertion("parameters/Parameters.java") 10 | .failsToCompile() 11 | .withErrorContaining("Exactly one annotation expected"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/spring/DemoService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.spring; 2 | 3 | import io.github.zskamljic.restahead.JacksonConverter; 4 | import io.github.zskamljic.restahead.annotations.verbs.Get; 5 | import io.github.zskamljic.restahead.spring.RestAheadService; 6 | 7 | import java.util.Map; 8 | 9 | /** 10 | * Service that will be automatically injected as a bean. 11 | */ 12 | @RestAheadService(url = "https://httpbin.org", converter = JacksonConverter.class) 13 | public interface DemoService { 14 | @Get("/get") 15 | Map performGet(); 16 | } 17 | -------------------------------------------------------------------------------- /Dialects.md: -------------------------------------------------------------------------------- 1 | # Custom dialects 2 | 3 | Defining a new dialect is possible by depending on rest-ahead-processor. Required steps are as following: 4 | 5 | 1. Create a class implementing `io.github.zskamljic.restahead.polyglot.Dialect`. 6 | 2. Create a file in `main/resources/META-INF/services` named `io.github.zskamljic.restahead.polyglot.Dialect` (note that 7 | this is a file name, not a Java package) 8 | 3. In created file add the line with FQCN of the class created in #1. 9 | 4. In project where you want the dialect to be used add the dependency with at least `provided` scope. 10 | 11 | For sample implementation see Spring Dialect. -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/annotations/request/Path.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.annotations.request; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Specifies that the given parameter should be injected into path. If no value is provided, the parameter name will be used. 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(ElementType.PARAMETER) 13 | public @interface Path { 14 | String value() default ""; 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/declaration/ReturnDeclaration.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.declaration; 2 | 3 | import io.github.zskamljic.restahead.modeling.conversion.Conversion; 4 | 5 | import java.util.Optional; 6 | 7 | /** 8 | * The return type specification. 9 | * 10 | * @param targetConversion the target type to convert to (the T in Future<T>) 11 | * @param adapterCall the adapter class and method to use 12 | */ 13 | public record ReturnDeclaration( 14 | Optional targetConversion, 15 | Optional adapterCall 16 | ) { 17 | } 18 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormOnRecord.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.FormName; 4 | import io.github.zskamljic.restahead.annotations.form.FormUrlEncoded; 5 | import io.github.zskamljic.restahead.annotations.request.Body; 6 | import io.github.zskamljic.restahead.annotations.verbs.Post; 7 | 8 | public interface FormOnRecord { 9 | @Post 10 | void post(@FormUrlEncoded @Body Sample body); 11 | 12 | @Post 13 | void post2(@FormUrlEncoded Sample body); 14 | 15 | record Sample(String first, @FormName("2nd") String second) { 16 | } 17 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormOnClassInvalid.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.FormUrlEncoded; 4 | import io.github.zskamljic.restahead.annotations.request.Body; 5 | import io.github.zskamljic.restahead.annotations.verbs.Post; 6 | 7 | public interface FormOnClassInvalid { 8 | @Post 9 | void post(@FormUrlEncoded @Body Sample body); 10 | 11 | class Sample { 12 | public String getFirst() { 13 | return "FIRST"; 14 | } 15 | 16 | public Object getSecond() { 17 | return 4; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/ValidCollectionHeader.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | import java.util.Collection; 7 | import java.util.List; 8 | 9 | public interface ValidCollectionHeader { 10 | @Delete("/delete") 11 | void delete(@Header("Accept") List header); 12 | 13 | @Delete("/delete") 14 | void delete(@Header("Accept") Collection header); 15 | 16 | @Delete("/delete") 17 | void deleteInts(@Header("Accept") Collection header); 18 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/client/requests/Verb.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client.requests; 2 | 3 | /** 4 | * The verb for HTTP request. 5 | */ 6 | public enum Verb { 7 | DELETE, 8 | GET, 9 | HEAD, 10 | OPTIONS, 11 | PATCH, 12 | POST, 13 | PUT; 14 | 15 | /** 16 | * Whether the verb allows body. 17 | * 18 | * @return true if body can be present, false otherwise. 19 | */ 20 | public boolean allowsBody() { 21 | return switch (this) { 22 | case DELETE, HEAD, GET, OPTIONS -> false; 23 | default -> true; 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/parameters/ParameterWithExceptions.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.parameters; 2 | 3 | import io.github.zskamljic.restahead.encoding.MultiPartParameter; 4 | 5 | import javax.lang.model.type.TypeMirror; 6 | import java.util.Set; 7 | 8 | /** 9 | * Represents a parameter with exceptions that can be thrown when it's used 10 | * 11 | * @param parameter the parameter that is added 12 | * @param exceptions the exceptions that can be thrown 13 | */ 14 | public record ParameterWithExceptions( 15 | MultiPartParameter parameter, 16 | Set exceptions 17 | ) { 18 | } 19 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/query/CollectionAndArray.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Query; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | import java.util.List; 7 | 8 | public interface CollectionAndArray { 9 | @Delete("/delete") 10 | void deleteArray(@Query("q") String[] query); 11 | 12 | @Delete("/delete") 13 | void deleteList(@Query("q") List query); 14 | 15 | @Delete("/delete") 16 | void deleteIntegerList(@Query("q") List query); 17 | 18 | @Delete("/delete") 19 | void deletePrimitives(@Query("q") int query); 20 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/java/io/github/zskamljic/restahead/processor/jaxrs/JaxRsDialectTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.processor.jaxrs; 2 | 3 | import com.google.testing.compile.JavaFileObjects; 4 | import io.github.zskamljic.restahead.processor.CommonProcessorTest; 5 | import org.junit.jupiter.api.Test; 6 | 7 | class JaxRsDialectTest extends CommonProcessorTest { 8 | @Test 9 | void processorGeneratesForJaxRs() { 10 | commonCompilationAssertion("jaxrs/JaxRsService.java") 11 | .compilesWithoutWarnings() 12 | .and() 13 | .generatesSources(JavaFileObjects.forResource("jaxrs/JaxRsService$Impl.java")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/java/io/github/zskamljic/restahead/processor/spring/SpringDialectTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.processor.spring; 2 | 3 | import com.google.testing.compile.JavaFileObjects; 4 | import io.github.zskamljic.restahead.processor.CommonProcessorTest; 5 | import org.junit.jupiter.api.Test; 6 | 7 | class SpringDialectTest extends CommonProcessorTest { 8 | @Test 9 | void processorGeneratesForSpring() { 10 | commonCompilationAssertion("spring/SpringService.java") 11 | .compilesWithoutWarnings() 12 | .and() 13 | .generatesSources(JavaFileObjects.forResource("spring/SpringService$Impl.java")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/java/io/github/zskamljic/restahead/processor/stock/AdapterProcessorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.processor.stock; 2 | 3 | import com.google.testing.compile.JavaFileObjects; 4 | import io.github.zskamljic.restahead.processor.CommonProcessorTest; 5 | import org.junit.jupiter.api.Test; 6 | 7 | class AdapterProcessorTest extends CommonProcessorTest { 8 | @Test 9 | void adapterIsPickedUp() { 10 | commonCompilationAssertion("adapters/AdapterService.java") 11 | .compilesWithoutWarnings() 12 | .and() 13 | .generatesSources(JavaFileObjects.forResource("adapters/AdapterService$Impl.java")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-spring/src/main/java/io/github/zskamljic/restahead/spring/EnableRestAhead.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.spring; 2 | 3 | import org.springframework.context.annotation.Import; 4 | 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * Scans for interfaces that have {@link RestAheadService} annotation and instantiates them as a bean. 13 | */ 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.TYPE) 16 | @Documented 17 | @Import(RestAheadRegistrar.class) 18 | public @interface EnableRestAhead { 19 | } 20 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/exceptions/RequestFailedException.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.exceptions; 2 | 3 | import java.io.InputStream; 4 | 5 | public class RequestFailedException extends RestException { 6 | private final int code; 7 | private final transient InputStream errorBody; 8 | 9 | public RequestFailedException(int code, InputStream inputStream) { 10 | super("Request failed with code " + code); 11 | this.code = code; 12 | this.errorBody = inputStream; 13 | } 14 | 15 | public int getCode() { 16 | return code; 17 | } 18 | 19 | public InputStream getErrorBody() { 20 | return errorBody; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/polyglot/ProcessingException.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.polyglot; 2 | 3 | import javax.annotation.processing.Messager; 4 | import javax.lang.model.element.Element; 5 | import javax.tools.Diagnostic; 6 | 7 | public class ProcessingException extends Exception { 8 | private final transient Element target; 9 | private final String message; 10 | 11 | public ProcessingException(Element target, String message) { 12 | this.target = target; 13 | this.message = message; 14 | } 15 | 16 | public void report(Messager messager) { 17 | messager.printMessage(Diagnostic.Kind.ERROR, message, target); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/clients/BodyResponsesService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Get; 4 | import io.github.zskamljic.restahead.annotations.verbs.Put; 5 | import io.github.zskamljic.restahead.client.responses.BodyAndErrorResponse; 6 | import io.github.zskamljic.restahead.client.responses.BodyResponse; 7 | import io.github.zskamljic.restahead.demo.models.HttpBinResponse; 8 | 9 | import java.util.Map; 10 | 11 | public interface BodyResponsesService { 12 | @Put("/put") 13 | BodyResponse> put(); 14 | 15 | @Get("/put") 16 | BodyAndErrorResponse> put2(); 17 | } 18 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/intercepting/logging/RequestLogger.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.intercepting.logging; 2 | 3 | /** 4 | * Logger to use for output. 5 | */ 6 | public interface RequestLogger { 7 | /** 8 | * Whether this logger is enabled. Some loggers may only log for specific levels, this function can check if a level is enabled. 9 | * 10 | * @return if data should be logged or not 11 | */ 12 | default boolean isEnabled() { 13 | return true; 14 | } 15 | 16 | /** 17 | * Output the given string. Provided parameter may be multiline. 18 | * 19 | * @param output the data to output. 20 | */ 21 | void output(String output); 22 | } 23 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/request/path/StringPath.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.request.path; 2 | 3 | import java.net.URI; 4 | import java.net.URISyntaxException; 5 | 6 | /** 7 | * Wraps the path string, where no substitutions need to be done. 8 | */ 9 | public final class StringPath extends RequestPath { 10 | public StringPath(String path) { 11 | super(path); 12 | } 13 | 14 | /** 15 | * Parses the given path URI. 16 | * 17 | * @return the parsed URI 18 | * @throws URISyntaxException if any invalid characters are present or the format is not valid. 19 | */ 20 | @Override 21 | public URI uri() throws URISyntaxException { 22 | return new URI(path); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/src/test/java/io/github/zskamljic/restahead/demo/spring/DemoControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.spring; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.junit.jupiter.SpringExtension; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertNotNull; 10 | 11 | @SpringBootTest 12 | @ExtendWith(SpringExtension.class) 13 | class DemoControllerTest { 14 | @Autowired 15 | private DemoController demoController; 16 | 17 | @Test 18 | void testServiceInjected() { 19 | assertNotNull(demoController.demoService); 20 | } 21 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/declaration/AdapterClassDeclaration.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.declaration; 2 | 3 | import javax.lang.model.element.TypeElement; 4 | import java.util.List; 5 | 6 | /** 7 | * Contains data relevant to the adapter. 8 | * 9 | * @param adapterType the type of the adapter 10 | * @param adapterMethods the methods qualifying for use as an adapter 11 | */ 12 | public record AdapterClassDeclaration( 13 | TypeElement adapterType, 14 | List adapterMethods 15 | ) { 16 | public String variableName() { 17 | var typeName = adapterType.getSimpleName().toString(); 18 | return typeName.substring(0, 1).toLowerCase() + typeName.substring(1); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/adapters/AdapterService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.Adapter; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | import io.github.zskamljic.restahead.client.responses.Response; 6 | 7 | import java.util.concurrent.ExecutionException; 8 | import java.util.concurrent.Future; 9 | import java.util.stream.Stream; 10 | 11 | public interface AdapterService { 12 | @Delete("/delete") 13 | Stream delete(); 14 | 15 | static class StreamAdapter { 16 | @Adapter 17 | public Stream adapt(Future response) throws ExecutionException, InterruptedException { 18 | return Stream.of(response.get()); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormOnValidMap.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.FormUrlEncoded; 4 | import io.github.zskamljic.restahead.annotations.request.Body; 5 | import io.github.zskamljic.restahead.annotations.verbs.Post; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.UUID; 10 | 11 | public interface FormOnValidMap { 12 | @Post 13 | void post(@FormUrlEncoded @Body Map body); 14 | 15 | @Post 16 | void post2(@FormUrlEncoded @Body Map body); 17 | 18 | @Post 19 | void post3(@FormUrlEncoded @Body Map body); 20 | 21 | @Post 22 | void post4(@FormUrlEncoded @Body HashMap body); 23 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/interceptors/PreRequestInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.interceptors; 2 | 3 | import io.github.zskamljic.restahead.client.responses.Response; 4 | import io.github.zskamljic.restahead.client.requests.Request; 5 | import io.github.zskamljic.restahead.intercepting.Chain; 6 | import io.github.zskamljic.restahead.intercepting.Interceptor; 7 | 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | public class PreRequestInterceptor implements Interceptor { 11 | @Override 12 | public CompletableFuture intercept(Chain chain, Request request) { 13 | var newRequest = request.buildUpon() 14 | .setPath("get") 15 | .build(); 16 | return chain.proceed(newRequest); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/clients/AuthorizationService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.request.Path; 5 | import io.github.zskamljic.restahead.annotations.verbs.Get; 6 | import io.github.zskamljic.restahead.client.responses.Response; 7 | import io.github.zskamljic.restahead.demo.models.AuthResponse; 8 | 9 | public interface AuthorizationService { 10 | @Get("/basic-auth/{user}/{password}") 11 | Response getBasicAuth(@Path String user, @Path("password") String password, @Header("Authorization") String authorization); 12 | 13 | @Get("/bearer") 14 | AuthResponse getBearer(@Header("Authorization") String authorization); 15 | } 16 | -------------------------------------------------------------------------------- /rest-ahead-client/src/test/java/io/github/zskamljic/restahead/conversion/GenericReferenceTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.conversion; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.lang.reflect.ParameterizedType; 6 | import java.util.Map; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | class GenericReferenceTest { 12 | @Test 13 | void genericReferenceReturnsParameterizedType() { 14 | var reference = new GenericReference>() { 15 | }; 16 | var type = reference.getType(); 17 | 18 | assertTrue(type instanceof ParameterizedType); 19 | assertEquals("java.util.Map", type.toString()); 20 | } 21 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/adapters/SupplierAdapter.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.adapters; 2 | 3 | import io.github.zskamljic.restahead.annotations.Adapter; 4 | import io.github.zskamljic.restahead.exceptions.RestException; 5 | 6 | import java.util.concurrent.ExecutionException; 7 | import java.util.concurrent.Future; 8 | import java.util.function.Supplier; 9 | 10 | /** 11 | * Demonstration on how an adapter can be declared. 12 | */ 13 | public class SupplierAdapter { 14 | @Adapter 15 | public Supplier adapt(Future future) { 16 | return () -> { 17 | try { 18 | return future.get(); 19 | } catch (InterruptedException | ExecutionException e) { 20 | throw new RestException(e); 21 | } 22 | }; 23 | } 24 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/client/responses/BodyResponse.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client.responses; 2 | 3 | import java.io.InputStream; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Optional; 7 | 8 | /** 9 | * Represents a response from the server where call will not fail for non 2xx codes. 10 | * 11 | * @param status the response code of the request 12 | * @param headers the headers present in the response 13 | * @param body the body, if request was successful 14 | * @param errorBody the error body if request was not successful 15 | * @param the type of body to deserialize 16 | */ 17 | public record BodyResponse( 18 | int status, 19 | Map> headers, 20 | Optional body, 21 | Optional errorBody 22 | ) { 23 | } 24 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/encoding/MultiPartParameter.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.encoding; 2 | 3 | import io.github.zskamljic.restahead.client.requests.parts.MultiPart; 4 | 5 | import java.util.Optional; 6 | 7 | /** 8 | * A parameter that specifies a part of multipart request. 9 | * 10 | * @param httpName the name to use in http transport 11 | * @param name the name of function parameter 12 | * @param type which type to use when generating the code, empty if it's already an appropriate type 13 | * @param extraParameters extra parameters that should be sent to the constructor 14 | */ 15 | public record MultiPartParameter( 16 | String httpName, 17 | String name, 18 | Optional> type, 19 | Optional extraParameters 20 | ) { 21 | } 22 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/intercepting/Interceptor.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.intercepting; 2 | 3 | import io.github.zskamljic.restahead.client.responses.Response; 4 | import io.github.zskamljic.restahead.client.requests.Request; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | /** 9 | * This interceptor with receive the request and apply some common logic on all calls. 10 | */ 11 | public interface Interceptor { 12 | /** 13 | * Intercepts the request, allowing modification of the request or response. 14 | * 15 | * @param chain the chain used for proceeding through the chain 16 | * @param request the request that was intercepted 17 | * @return the response to return to the next element in the chain 18 | */ 19 | CompletableFuture intercept(Chain chain, Request request); 20 | } 21 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormOnClass.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.FormName; 4 | import io.github.zskamljic.restahead.annotations.form.FormUrlEncoded; 5 | import io.github.zskamljic.restahead.annotations.request.Body; 6 | import io.github.zskamljic.restahead.annotations.verbs.Post; 7 | 8 | public interface FormOnClass { 9 | @Post 10 | void post(@FormUrlEncoded @Body Sample body); 11 | 12 | class Sample { 13 | public String getFirst() { 14 | return "FIRST"; 15 | } 16 | 17 | @FormName("smth") 18 | public int getSecond() { 19 | return 4; 20 | } 21 | 22 | public static void getSomething() { 23 | 24 | } 25 | 26 | private Object getIgnored() { 27 | return null; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/clients/BodyService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Body; 4 | import io.github.zskamljic.restahead.annotations.verbs.Get; 5 | import io.github.zskamljic.restahead.annotations.verbs.Patch; 6 | import io.github.zskamljic.restahead.annotations.verbs.Post; 7 | import io.github.zskamljic.restahead.annotations.verbs.Put; 8 | import io.github.zskamljic.restahead.demo.models.HttpBinResponse; 9 | 10 | import java.util.Map; 11 | 12 | public interface BodyService { 13 | @Get 14 | void get(); 15 | 16 | @Patch("/patch") 17 | HttpBinResponse patch(@Body Map body); 18 | 19 | @Post("/post") 20 | HttpBinResponse post(@Body Map body); 21 | 22 | @Put("/put") 23 | HttpBinResponse put(@Body Map body); 24 | } 25 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/spring/DemoController.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.spring; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | /** 7 | * Simple controller to showcase usage in spring boot app 8 | */ 9 | @RestController 10 | public class DemoController { 11 | /** 12 | * Field is package private so it's accessible from tests. 13 | */ 14 | final DemoService demoService; 15 | 16 | public DemoController(DemoService demoService) { 17 | this.demoService = demoService; 18 | } 19 | 20 | /** 21 | * Proxy httpbin.org through RestAhead 22 | * 23 | * @return whatever the service returns 24 | */ 25 | @GetMapping("/get") 26 | public Object performGet() { 27 | return demoService.performGet(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/client/responses/BodyAndErrorResponse.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client.responses; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | 7 | /** 8 | * Represents a response from the server where call will not fail for non 2xx codes. Body will be deserialized in the errorBody. 9 | * 10 | * @param status the response code of the request 11 | * @param headers the headers present in the response 12 | * @param body the body, if request was successful 13 | * @param errorBody the error body if request was not successful 14 | * @param the type of successful body 15 | * @param the type of error body (codes not in 2xx range) 16 | */ 17 | public record BodyAndErrorResponse( 18 | int status, 19 | Map> headers, 20 | Optional body, 21 | Optional errorBody 22 | ) { 23 | } 24 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/declaration/AdapterMethodDeclaration.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.declaration; 2 | 3 | import javax.lang.model.element.ExecutableElement; 4 | import javax.lang.model.type.DeclaredType; 5 | import javax.lang.model.type.TypeMirror; 6 | import java.util.List; 7 | 8 | /** 9 | * The method that can be used as an adapter. 10 | * 11 | * @param name the name of the method 12 | * @param returnType the output of this adapter 13 | * @param adapterParameters the parts of the adapter 14 | * @param exceptions the exceptions thrown by this adapter 15 | * @param executableElement the executable element itself 16 | */ 17 | public record AdapterMethodDeclaration( 18 | String name, 19 | TypeMirror returnType, 20 | List adapterParameters, 21 | List exceptions, 22 | ExecutableElement executableElement 23 | ) { 24 | } 25 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/PrimitiveHeader.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | 6 | public interface PrimitiveHeader { 7 | @Delete("/delete") 8 | void delete(@Header("Accept") boolean header); 9 | 10 | @Delete("/delete") 11 | void delete(@Header("Accept") byte header); 12 | 13 | @Delete("/delete") 14 | void delete(@Header("Accept") char header); 15 | 16 | @Delete("/delete") 17 | void delete(@Header("Accept") double header); 18 | 19 | @Delete("/delete") 20 | void delete(@Header("Accept") float header); 21 | 22 | @Delete("/delete") 23 | void delete(@Header("Accept") int header); 24 | 25 | @Delete("/delete") 26 | void delete(@Header("Accept") long header); 27 | 28 | @Delete("/delete") 29 | void delete(@Header("Accept") short header); 30 | } -------------------------------------------------------------------------------- /demo/src/test/java/io/github/zskamljic/restahead/demo/clients/CustomAdapterServiceTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.JacksonConverter; 4 | import io.github.zskamljic.restahead.RestAhead; 5 | import io.github.zskamljic.restahead.demo.adapters.SupplierAdapter; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertNotNull; 10 | 11 | class CustomAdapterServiceTest { 12 | private CustomAdapterService service; 13 | 14 | @BeforeEach 15 | void setUp() { 16 | service = RestAhead.builder("https://httpbin.org/") 17 | .converter(new JacksonConverter()) 18 | .addAdapter(new SupplierAdapter()) 19 | .build(CustomAdapterService.class); 20 | } 21 | 22 | @Test 23 | void supplierAdapterReturnsValue() { 24 | var response = service.get().get(); 25 | 26 | assertNotNull(response); 27 | } 28 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/java/io/github/zskamljic/restahead/processor/UnclaimedProcessor.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.processor; 2 | 3 | import javax.annotation.processing.AbstractProcessor; 4 | import javax.annotation.processing.Generated; 5 | import javax.annotation.processing.RoundEnvironment; 6 | import javax.lang.model.SourceVersion; 7 | import javax.lang.model.element.TypeElement; 8 | import java.util.Set; 9 | 10 | /** 11 | * Claim other annotations to prevent compile warnings. 12 | */ 13 | public class UnclaimedProcessor extends AbstractProcessor { 14 | @Override 15 | public boolean process(Set annotations, RoundEnvironment roundEnv) { 16 | return true; 17 | } 18 | 19 | @Override 20 | public SourceVersion getSupportedSourceVersion() { 21 | return SourceVersion.latestSupported(); 22 | } 23 | 24 | @Override 25 | public Set getSupportedAnnotationTypes() { 26 | return Set.of(Generated.class.getCanonicalName()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo/src/test/java/io/github/zskamljic/restahead/demo/clients/BodyResponsesServiceTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.JacksonConverter; 4 | import io.github.zskamljic.restahead.RestAhead; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | class BodyResponsesServiceTest { 12 | private BodyResponsesService service; 13 | 14 | @BeforeEach 15 | void setUp() { 16 | service = RestAhead.builder("https://httpbin.org/") 17 | .converter(new JacksonConverter()) 18 | .build(BodyResponsesService.class); 19 | } 20 | 21 | @Test 22 | void putReturnsBodyAndCode() { 23 | var response = service.put(); 24 | 25 | assertEquals(200, response.status()); 26 | assertTrue(response.body().isPresent()); 27 | assertTrue(response.errorBody().isEmpty()); 28 | } 29 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/interceptors/PostRequestInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.interceptors; 2 | 3 | import io.github.zskamljic.restahead.client.responses.Response; 4 | import io.github.zskamljic.restahead.client.requests.Request; 5 | import io.github.zskamljic.restahead.intercepting.Chain; 6 | import io.github.zskamljic.restahead.intercepting.Interceptor; 7 | 8 | import java.util.concurrent.CompletableFuture; 9 | import java.util.concurrent.atomic.AtomicReference; 10 | 11 | public class PostRequestInterceptor implements Interceptor { 12 | private final AtomicReference response = new AtomicReference<>(); 13 | 14 | @Override 15 | public CompletableFuture intercept(Chain chain, Request request) { 16 | var originalResponse = chain.proceed(request); 17 | return originalResponse.thenApply(value -> { 18 | response.set(value); 19 | return value; 20 | }); 21 | } 22 | 23 | public Response getResponse() { 24 | return response.get(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/conversion/GenericReference.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.conversion; 2 | 3 | import java.lang.reflect.ParameterizedType; 4 | import java.lang.reflect.Type; 5 | 6 | /** 7 | * Generic reference used for deserialization of generic object such as Collections. 8 | * Intended only for generated code, instantiating manually voids all warranty. 9 | * 10 | * @param the contained type. 11 | */ 12 | @SuppressWarnings("unused") // T is used via reflection, so the compiler believes it is unused 13 | public abstract class GenericReference { 14 | private final Type type; 15 | 16 | protected GenericReference() { 17 | var superclass = getClass().getGenericSuperclass(); 18 | if (!(superclass instanceof ParameterizedType parameterizedType)) { 19 | throw new IllegalArgumentException("Class is not a generic type."); 20 | } 21 | 22 | type = parameterizedType.getActualTypeArguments()[0]; 23 | } 24 | 25 | public Type getType() { 26 | return type; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/java/io/github/zskamljic/restahead/processor/CommonProcessorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.processor; 2 | 3 | import com.google.common.truth.Truth; 4 | import com.google.testing.compile.CompileTester; 5 | import com.google.testing.compile.JavaFileObjects; 6 | import com.google.testing.compile.JavaSourcesSubjectFactory; 7 | 8 | import javax.annotation.processing.AbstractProcessor; 9 | import java.util.Arrays; 10 | 11 | public abstract class CommonProcessorTest { 12 | private final AbstractProcessor requestProcessor = new RequestsProcessor(); 13 | private final AbstractProcessor generatedProcessor = new UnclaimedProcessor(); 14 | 15 | protected CompileTester commonCompilationAssertion(String... files) { 16 | var sources = Arrays.stream(files) 17 | .map(JavaFileObjects::forResource) 18 | .toList(); 19 | 20 | return Truth.assert_() 21 | .about(JavaSourcesSubjectFactory.javaSources()) 22 | .that(sources) 23 | .processedWith(requestProcessor, generatedProcessor); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/test/java/io/github/zskamljic/restahead/demo/interceptors/PreRequestInterceptorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.interceptors; 2 | 3 | import io.github.zskamljic.restahead.JacksonConverter; 4 | import io.github.zskamljic.restahead.RestAhead; 5 | import io.github.zskamljic.restahead.client.JavaHttpClient; 6 | import io.github.zskamljic.restahead.demo.clients.InterceptedService; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertNotNull; 11 | 12 | class PreRequestInterceptorTest { 13 | @Test 14 | void interceptorIsCalled() { 15 | var client = new JavaHttpClient(); 16 | client.addInterceptor(new PreRequestInterceptor()); 17 | 18 | var service = RestAhead.builder("https://httpbin.org/") 19 | .client(client) 20 | .converter(new JacksonConverter()) 21 | .build(InterceptedService.class); 22 | 23 | var response = service.get(); 24 | assertNotNull(response); 25 | assertEquals(200, response.status()); 26 | } 27 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/generation/Variables.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.generation; 2 | 3 | /** 4 | * The names of the variables used in generated code, collected to prevent duplication and declaration in multiple places. 5 | */ 6 | public final class Variables { 7 | private Variables() { 8 | } 9 | 10 | public static final String BASE_URL = "baseUrl"; 11 | public static final String CLIENT = "client"; 12 | public static final String CONVERSION_TYPE_HOLDER = "conversionTypeHolder"; 13 | public static final String CONVERTED_NAME = "convertedResponse"; 14 | public static final String CONVERTER = "converter"; 15 | public static final String DESERIALIZED = "deserializedResponse"; 16 | public static final String FORM_ENCODE = "formEncode"; 17 | public static final String HEADER_ITEM = "headerItem"; 18 | public static final String QUERY_ITEM = "queryItem"; 19 | public static final String REQUEST_BUILDER = "httpRequestBuilder"; 20 | public static final String RESPONSE = "response"; 21 | public static final String VALUE = "value"; 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/maven-publish.yml: -------------------------------------------------------------------------------- 1 | name: Maven publish 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - develop 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up JDK 17 and central repository 14 | uses: actions/setup-java@v2 15 | with: 16 | distribution: 'adopt' 17 | java-version: '17' 18 | cache: maven 19 | server-id: ossrh 20 | server-username: MAVEN_USERNAME 21 | server-password: MAVEN_PASSWORD 22 | - name: Install GPG secret key 23 | run: | 24 | cat <(echo -e "${{ secrets.OSSRH_GPG_SECRET_KEY }}") | gpg --batch --import 25 | gpg --list-secret-keys --keyid-format LONG 26 | - name: Publish to Maven 27 | env: 28 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} 29 | MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 30 | run: | 31 | ./mvnw clean test 32 | ./mvnw -Dgpg.passphrase="${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }}" -pl '!:test-report-aggregator,!:demo' deploy -DskipTests -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/client/requests/parts/FieldPart.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client.requests.parts; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.InputStream; 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.UUID; 7 | 8 | /** 9 | * Represents a multipart field. 10 | */ 11 | public final class FieldPart extends MultiPart { 12 | private final String value; 13 | 14 | public FieldPart(String fieldName, String value) { 15 | super(fieldName); 16 | this.value = value; 17 | } 18 | 19 | public FieldPart(String fieldName, UUID value) { 20 | this(fieldName, value.toString()); 21 | } 22 | 23 | public FieldPart(String fieldName, Number number) { 24 | this(fieldName, String.valueOf(number)); 25 | } 26 | 27 | public FieldPart(String fieldName, Character character) { 28 | this(fieldName, String.valueOf(character)); 29 | } 30 | 31 | @Override 32 | protected InputStream bodyContent() { 33 | return new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/conversion/Converter.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.conversion; 2 | 3 | import io.github.zskamljic.restahead.client.responses.Response; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.lang.reflect.Type; 8 | 9 | /** 10 | * Used to convert types from their serialized form to Java objects. 11 | */ 12 | public interface Converter { 13 | /** 14 | * Deserialize the response to given type 15 | * 16 | * @param response the response to deserialize from 17 | * @param type the type to deserialize to 18 | * @param the return type 19 | * @return the deserialized value 20 | */ 21 | T deserialize(Response response, Type type) throws IOException; 22 | 23 | /** 24 | * Serialize the object to input stream. 25 | * 26 | * @param object the object to serialize 27 | * @return the {@link InputStream} containing the serialized object 28 | * @throws IOException if an exception occurred while serializing 29 | */ 30 | InputStream serialize(Object object) throws IOException; 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/test/java/io/github/zskamljic/restahead/demo/clients/FutureServiceTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.JacksonConverter; 4 | import io.github.zskamljic.restahead.RestAhead; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.concurrent.ExecutionException; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertNotNull; 11 | 12 | class FutureServiceTest { 13 | private FutureService service; 14 | 15 | @BeforeEach 16 | void setUp() { 17 | service = RestAhead.builder("https://httpbin.org/") 18 | .converter(new JacksonConverter()) 19 | .build(FutureService.class); 20 | } 21 | 22 | @Test 23 | void futureGetSucceeds() throws ExecutionException, InterruptedException { 24 | var response = service.getFuture().get(); 25 | 26 | assertNotNull(response); 27 | } 28 | 29 | @Test 30 | void completableFutureGetSucceeds() throws ExecutionException, InterruptedException { 31 | var response = service.getCompletableFuture().get(); 32 | 33 | assertNotNull(response); 34 | } 35 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/exceptions/RestException.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.exceptions; 2 | 3 | import java.util.concurrent.ExecutionException; 4 | 5 | /** 6 | * Exception used for general errors during execution of RestAhead generated classes. 7 | */ 8 | public class RestException extends RuntimeException { 9 | public RestException(Throwable throwable) { 10 | super(throwable); 11 | } 12 | 13 | public RestException(String message) { 14 | super(message); 15 | } 16 | 17 | /** 18 | * Unwraps the exception or constructs a new one depending on type to prevent nesting causes. 19 | * 20 | * @param throwable the throwable to unwrap or wrap 21 | * @return the RestException for the given throwable 22 | */ 23 | public static RestException getAppropriateException(Throwable throwable) { 24 | if (throwable instanceof ExecutionException exec) { 25 | return getAppropriateException(exec.getCause()); 26 | } else if (throwable instanceof RestException restException) { 27 | return restException; 28 | } 29 | return new RestException(throwable); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/BoxedHeader.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 5 | import io.github.zskamljic.restahead.annotations.verbs.Get; 6 | import io.github.zskamljic.restahead.annotations.verbs.Patch; 7 | import io.github.zskamljic.restahead.annotations.verbs.Post; 8 | import io.github.zskamljic.restahead.annotations.verbs.Put; 9 | 10 | public interface BoxedHeader { 11 | @Delete("/delete") 12 | void delete(@Header("Accept") Boolean header); 13 | 14 | @Get("/delete") 15 | void delete(@Header("Accept") Byte header); 16 | 17 | @Patch("/delete") 18 | void delete(@Header("Accept") Character header); 19 | 20 | @Post("/delete") 21 | void delete(@Header("Accept") Double header); 22 | 23 | @Put("/delete") 24 | void delete(@Header("Accept") Float header); 25 | 26 | @Delete("/delete") 27 | void delete(@Header("Accept") Integer header); 28 | 29 | @Delete("/delete") 30 | void delete(@Header("Accept") Long header); 31 | 32 | @Delete("/delete") 33 | void delete(@Header("Accept") Short header); 34 | } -------------------------------------------------------------------------------- /rest-ahead-spring/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | RestAhead 7 | io.github.zskamljic 8 | 0.5.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | rest-ahead-spring 13 | 14 | 15 | 17 16 | 17 17 | 18 | 19 | 20 | 21 | org.springframework 22 | spring-context 23 | ${spring.version} 24 | 25 | 26 | io.github.zskamljic 27 | rest-ahead-client 28 | 0.5.0-SNAPSHOT 29 | 30 | 31 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/conversion/MapFormConverter.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.conversion; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.InputStream; 5 | import java.net.URLEncoder; 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Map; 8 | import java.util.stream.Collectors; 9 | 10 | /** 11 | * The default converter for Map classes. 12 | */ 13 | public final class MapFormConverter { 14 | private MapFormConverter() { 15 | } 16 | 17 | /** 18 | * Encodes the given value in format [key]=[URL encoded value] separated by & 19 | * @param value the values to encode 20 | * @param type of the key 21 | * @param type of the value 22 | * @return the {@link InputStream} with encoded data 23 | */ 24 | public static InputStream formEncode(Map value) { 25 | var stringValue = value.entrySet() 26 | .stream() 27 | .map(entry -> entry.getKey() + "=" + URLEncoder.encode(String.valueOf(entry.getValue()), StandardCharsets.UTF_8)) 28 | .collect(Collectors.joining("&")); 29 | return new ByteArrayInputStream(stringValue.getBytes(StandardCharsets.UTF_8)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/util/StringMultiMap.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.util; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * Common collection to use for map of string to list of strings (i.e. headers, query items). 11 | */ 12 | public class StringMultiMap extends HashMap> { 13 | /** 14 | * Creates a deep copy of this instance. 15 | * 16 | * @return the copied value 17 | */ 18 | public StringMultiMap mutableCopy() { 19 | var copiedMap = new StringMultiMap(); 20 | forEach((key, value) -> copiedMap.put(key, List.copyOf(value))); 21 | return copiedMap; 22 | } 23 | 24 | /** 25 | * Creates a new immutable copy of this instance. 26 | * 27 | * @return the immutable copy 28 | */ 29 | public Map> immutableCopy() { 30 | return entrySet() 31 | .stream() 32 | .collect(Collectors.toUnmodifiableMap( 33 | Map.Entry::getKey, 34 | entry -> Collections.unmodifiableList(entry.getValue()) 35 | )); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: Sonar 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | 9 | jobs: 10 | analyze: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | java: ['17'] 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up JDK 18 | uses: actions/setup-java@v3 19 | with: 20 | distribution: 'adopt' 21 | java-version: ${{ matrix.java }} 22 | cache: maven 23 | - name: Cache SonarCloud packages 24 | uses: actions/cache@v2 25 | with: 26 | path: ~/.sonar/cache 27 | key: ${{ runner.os }}-sonar 28 | restore-keys: ${{ runner.os }}-sonar 29 | - name: Install GPG secret key 30 | run: | 31 | cat <(echo -e "${{ secrets.OSSRH_GPG_SECRET_KEY }}") | gpg --batch --import 32 | gpg --list-secret-keys --keyid-format LONG 33 | - name: Build with Maven 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 37 | run: ./mvnw -Dgpg.passphrase="${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }}" -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=zskamljic_rest-ahead -------------------------------------------------------------------------------- /demo/src/test/java/io/github/zskamljic/restahead/demo/interceptors/PostRequestInterceptorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.interceptors; 2 | 3 | import io.github.zskamljic.restahead.JacksonConverter; 4 | import io.github.zskamljic.restahead.RestAhead; 5 | import io.github.zskamljic.restahead.client.JavaHttpClient; 6 | import io.github.zskamljic.restahead.demo.clients.InterceptedService; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertNotNull; 11 | 12 | class PostRequestInterceptorTest { 13 | @Test 14 | void adaptersExecutedCorrectly() { 15 | var client = new JavaHttpClient(); 16 | var postRequestInterceptor = new PostRequestInterceptor(); 17 | client.addInterceptor(postRequestInterceptor); 18 | client.addInterceptor(new PreRequestInterceptor()); 19 | 20 | var service = RestAhead.builder("https://httpbin.org/") 21 | .client(client) 22 | .converter(new JacksonConverter()) 23 | .build(InterceptedService.class); 24 | 25 | var response = service.get(); 26 | assertNotNull(response); 27 | assertEquals(200, response.status()); 28 | var mappedResponse = postRequestInterceptor.getResponse(); 29 | assertEquals(response, mappedResponse); 30 | } 31 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/conversion/OptionsConverter.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.conversion; 2 | 3 | import io.github.zskamljic.restahead.client.requests.Verb; 4 | import io.github.zskamljic.restahead.client.responses.Response; 5 | 6 | import java.util.Arrays; 7 | import java.util.Collection; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.function.BiConsumer; 11 | import java.util.function.Consumer; 12 | 13 | public final class OptionsConverter { 14 | private OptionsConverter() { 15 | } 16 | 17 | public static List parseOptions(Response response) { 18 | return response.headers().entrySet() 19 | .stream() 20 | .filter(e -> e.getKey().equalsIgnoreCase("allow")) 21 | .map(Map.Entry::getValue) 22 | .flatMap(Collection::stream) 23 | .map(allow -> allow.split(", ")) 24 | .flatMap(Arrays::stream) 25 | .map(String::trim) 26 | .mapMulti((BiConsumer>) (value, consumer) -> { 27 | if ("*".equals(value)) { 28 | Arrays.stream(Verb.values()).forEach(consumer); 29 | return; 30 | } 31 | consumer.accept(Verb.valueOf(value)); 32 | }) 33 | .distinct() 34 | .toList(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/adapter/DefaultAdapters.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.adapter; 2 | 3 | import java.util.concurrent.ExecutionException; 4 | import java.util.concurrent.Future; 5 | 6 | /** 7 | * The default, built-in adapters for RestAhead. 8 | */ 9 | public class DefaultAdapters { 10 | /** 11 | * Adapter to convert async Future calls to blocking calls. 12 | * 13 | * @param response the response to convert 14 | * @param the type that can be converted automatically 15 | * @return the value of provided future 16 | * @throws ExecutionException if an error occurred during future execution 17 | * @throws InterruptedException if the future was interrupted 18 | */ 19 | public T syncAdapter(Future response) throws ExecutionException, InterruptedException { 20 | return response.get(); 21 | } 22 | 23 | /** 24 | * Converts Future calls to blocking call, without a response. 25 | * 26 | * @param response the response to convert 27 | * @param the of the request 28 | * @throws ExecutionException if an error occurred during future execution 29 | * @throws InterruptedException if the future was interrupted 30 | */ 31 | public void syncVoidAdapter(Future response) throws ExecutionException, InterruptedException { 32 | response.get(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/declaration/ParameterDeclaration.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.declaration; 2 | 3 | import io.github.zskamljic.restahead.encoding.BodyEncoding; 4 | import io.github.zskamljic.restahead.request.PresetValue; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | /** 11 | * Parameter holder. 12 | * 13 | * @param headers the header parts 14 | * @param query the query parts 15 | * @param paths the path parts 16 | * @param body the body encoding 17 | * @param presetQueries the queries present in request line 18 | * @param presetHeaders the headers present in request line 19 | */ 20 | public record ParameterDeclaration( 21 | List headers, 22 | List query, 23 | List paths, 24 | Optional body, 25 | List presetQueries, 26 | List presetHeaders 27 | ) { 28 | public ParameterDeclaration( 29 | List headers, 30 | List query, 31 | List paths, 32 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional body 33 | ) { 34 | this(headers, query, paths, body, new ArrayList<>(), new ArrayList<>()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/spring/DummyClient.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.spring; 2 | 3 | import io.github.zskamljic.restahead.client.Client; 4 | import io.github.zskamljic.restahead.client.requests.Request; 5 | import io.github.zskamljic.restahead.client.responses.Response; 6 | import io.github.zskamljic.restahead.intercepting.Interceptor; 7 | 8 | import java.io.InputStream; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.concurrent.CompletableFuture; 13 | 14 | public class DummyClient extends Client { 15 | private final List interceptors = new ArrayList<>(); 16 | protected static final List requests = new ArrayList<>(); 17 | 18 | public List getInterceptors() { 19 | return interceptors; 20 | } 21 | 22 | @Override 23 | public void addInterceptor(Interceptor interceptor) { 24 | super.addInterceptor(interceptor); 25 | interceptors.add(interceptor); 26 | } 27 | 28 | @Override 29 | protected CompletableFuture performRequest(Request request) { 30 | requests.add(new ClientRequestPair(this, request)); 31 | return CompletableFuture.completedFuture(new Response(200, Map.of(), InputStream.nullInputStream())); 32 | } 33 | 34 | record ClientRequestPair(Client client, Request request) { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/declaration/CallDeclaration.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.declaration; 2 | 3 | import io.github.zskamljic.restahead.encoding.FormBodyEncoding; 4 | import io.github.zskamljic.restahead.request.RequestLine; 5 | 6 | import javax.lang.model.element.ExecutableElement; 7 | import javax.lang.model.type.TypeMirror; 8 | import java.util.List; 9 | 10 | /** 11 | * The call as declared by the interface 12 | * 13 | * @param function the function that needs to be overridden 14 | * @param exceptions the exceptions thrown by this call 15 | * @param requestLine the request and path to be used 16 | * @param parameters the parts of the request 17 | * @param returnDeclaration the return value of the request 18 | */ 19 | public record CallDeclaration( 20 | ExecutableElement function, 21 | List exceptions, 22 | RequestLine requestLine, 23 | ParameterDeclaration parameters, 24 | ReturnDeclaration returnDeclaration 25 | ) { 26 | /** 27 | * Whether the body requires a converter to be present in the service. 28 | * 29 | * @return true if a converter is required, false otherwise 30 | */ 31 | public boolean requiresConverter() { 32 | return parameters.body().isPresent() && !(parameters.body().get() instanceof FormBodyEncoding) || 33 | returnDeclaration.targetConversion().isPresent(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/request/path/TemplatedPath.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.request.path; 2 | 3 | import java.net.URI; 4 | import java.net.URISyntaxException; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * Represents a templated path, that will need to be filled before use. 10 | */ 11 | public final class TemplatedPath extends RequestPath { 12 | private final String cleanedPath; 13 | private final List requiredParameters = new ArrayList<>(); 14 | 15 | public TemplatedPath(String path) { 16 | super(path); 17 | cleanedPath = path.replaceAll(PATH_VARIABLE.pattern(), "$1$2"); 18 | var matcher = PATH_VARIABLE.matcher(path); 19 | while (matcher.find()) { 20 | requiredParameters.add(matcher.group(2)); 21 | } 22 | } 23 | 24 | /** 25 | * Find the parts that need to be provided. 26 | * 27 | * @return list of parts 28 | */ 29 | public List getRequiredParameters() { 30 | return requiredParameters; 31 | } 32 | 33 | /** 34 | * Creates a valid, parsed version of the internal path with the query. Parameters will be unwrapped for the representation. 35 | * 36 | * @return the parsed URI 37 | * @throws URISyntaxException if URI is malformed 38 | */ 39 | @Override 40 | public URI uri() throws URISyntaxException { 41 | return new URI(cleanedPath); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/test/java/io/github/zskamljic/restahead/request/path/TemplatedPathTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.request.path; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.CsvSource; 6 | 7 | import java.net.URISyntaxException; 8 | import java.util.List; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | class TemplatedPathTest { 13 | @ParameterizedTest 14 | @CsvSource({ 15 | "path,{path}", 16 | "/path,/{path}", 17 | "/path/path,/path/{path}", 18 | "/path/path1/path2/path3,/path/{path1}/{path2}/path3", 19 | "/path,/{path}?q=1" 20 | }) 21 | void cleanedPathReturnsValidPath(String expected, String input) throws URISyntaxException { 22 | var templatedPath = new TemplatedPath(input); 23 | 24 | assertEquals(expected, templatedPath.uri().getPath()); 25 | } 26 | 27 | @Test 28 | void requiredParametersReturnsCorrectList() { 29 | var string = "/path/{path1}/{path2}/path3"; 30 | var templatedPath = new TemplatedPath(string); 31 | 32 | assertEquals(List.of("path1", "path2"), templatedPath.getRequiredParameters()); 33 | } 34 | 35 | @Test 36 | void toStringReturnsPathWithoutQuery() { 37 | var string = "/{path}?q=1"; 38 | var templatedPath = new TemplatedPath(string); 39 | 40 | assertEquals("/{path}", templatedPath.toString()); 41 | } 42 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/intercepting/Chain.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.intercepting; 2 | 3 | import io.github.zskamljic.restahead.client.responses.Response; 4 | import io.github.zskamljic.restahead.client.requests.Request; 5 | 6 | import java.util.List; 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.function.Function; 9 | 10 | /** 11 | * Represents a chain through which the request passes on the way to response. 12 | */ 13 | public class Chain { 14 | private final List interceptors; 15 | private final Function> exchangeFunction; 16 | private int currentInterceptor; 17 | 18 | public Chain( 19 | List interceptors, 20 | Function> exchangeFunction 21 | ) { 22 | this.interceptors = interceptors; 23 | this.exchangeFunction = exchangeFunction; 24 | } 25 | 26 | /** 27 | * Proceeds with the request, using the next interceptor in the chain. If no more interceptors are left call the client. 28 | * 29 | * @param request the request to use 30 | * @return the response obtained from client 31 | */ 32 | public CompletableFuture proceed(Request request) { 33 | if (currentInterceptor == interceptors.size()) { 34 | return exchangeFunction.apply(request); 35 | } 36 | var current = currentInterceptor; 37 | currentInterceptor++; 38 | return interceptors.get(current).intercept(this, request); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /demo/src/test/java/io/github/zskamljic/restahead/demo/spring/PlaceholderServiceBeanTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.spring; 2 | 3 | import io.github.zskamljic.restahead.client.Client; 4 | import io.github.zskamljic.restahead.client.responses.Response; 5 | import io.github.zskamljic.restahead.conversion.Converter; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.test.context.junit.jupiter.SpringExtension; 12 | 13 | import java.io.InputStream; 14 | import java.util.Map; 15 | import java.util.concurrent.CompletableFuture; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertNotNull; 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.Mockito.doReturn; 20 | 21 | @SpringBootTest 22 | @ExtendWith(SpringExtension.class) 23 | class PlaceholderServiceBeanTest { 24 | @MockBean 25 | private Client mockClient; 26 | 27 | @MockBean 28 | private Converter mockConverter; 29 | 30 | @Autowired 31 | private ConfigCombinations.PlaceholderService placeholderService; 32 | 33 | @Test 34 | void placeHolderServiceInjectsBeans() { 35 | doReturn(CompletableFuture.completedFuture(new Response(200, Map.of(), InputStream.nullInputStream()))) 36 | .when(mockClient).execute(any()); 37 | 38 | var response = placeholderService.get(); 39 | 40 | assertNotNull(response); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/clients/HttpBinMethodsService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.annotations.request.Header; 4 | import io.github.zskamljic.restahead.annotations.request.Query; 5 | import io.github.zskamljic.restahead.annotations.verbs.Delete; 6 | import io.github.zskamljic.restahead.annotations.verbs.Get; 7 | import io.github.zskamljic.restahead.annotations.verbs.Head; 8 | import io.github.zskamljic.restahead.annotations.verbs.Options; 9 | import io.github.zskamljic.restahead.annotations.verbs.Patch; 10 | import io.github.zskamljic.restahead.annotations.verbs.Post; 11 | import io.github.zskamljic.restahead.annotations.verbs.Put; 12 | import io.github.zskamljic.restahead.client.requests.Verb; 13 | import io.github.zskamljic.restahead.demo.models.HttpBinResponse; 14 | 15 | import java.util.List; 16 | 17 | public interface HttpBinMethodsService { 18 | @Delete("/delete") 19 | HttpBinResponse delete(@Query("q") String query, @Header("Test-Header") String headers); 20 | 21 | @Get("/get") 22 | HttpBinResponse get(@Query("q") String query, @Header("Test-Header") String headers); 23 | 24 | @Head("/get") 25 | void head(); 26 | 27 | @Options("/get") 28 | List options(); 29 | 30 | @Patch("/patch") 31 | HttpBinResponse patch(@Query("q") String query, @Header("Test-Header") String headers); 32 | 33 | @Post("/post") 34 | HttpBinResponse post(@Query("q") String query, @Header("Test-Header") String headers); 35 | 36 | @Put("/put") 37 | HttpBinResponse put(@Query("q") String query, @Header("Test-Header") String headers); 38 | } 39 | -------------------------------------------------------------------------------- /rest-ahead-jackson-converter/src/test/java/io/github/zskamljic/restahead/JacksonConverterTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead; 2 | 3 | import io.github.zskamljic.restahead.client.responses.Response; 4 | import io.github.zskamljic.restahead.conversion.GenericReference; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.IOException; 9 | import java.util.Map; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertInstanceOf; 12 | import static org.junit.jupiter.api.Assertions.assertNotNull; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | class JacksonConverterTest { 16 | @Test 17 | void deserializeWorksForNormalType() throws IOException { 18 | var converter = new JacksonConverter(); 19 | 20 | var result = converter.deserialize(new Response(200, Map.of(), new ByteArrayInputStream(""" 21 | { 22 | "a": true 23 | } 24 | """.getBytes())), Object.class); 25 | 26 | assertNotNull(result); 27 | } 28 | 29 | @Test 30 | void deserializeWorksForGenericTypes() throws IOException { 31 | var converter = new JacksonConverter(); 32 | 33 | Map result = converter.deserialize(new Response(200, Map.of(), new ByteArrayInputStream(""" 34 | { 35 | "a": true 36 | } 37 | """.getBytes())), new GenericReference>() { 38 | }.getType()); 39 | 40 | assertNotNull(result); 41 | assertInstanceOf(Map.class, result); 42 | assertTrue(result.containsKey("a")); 43 | assertTrue(result.get("a")); 44 | } 45 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/declaration/ServiceDeclaration.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.declaration; 2 | 3 | import javax.lang.model.element.TypeElement; 4 | import java.util.List; 5 | import java.util.Optional; 6 | 7 | /** 8 | * The service representation based on the interface 9 | * 10 | * @param packageName the package name where the class should be generated 11 | * @param serviceType the type to implement 12 | * @param calls the calls that need to be implemented 13 | * @param requiresConverter if any of the calls in generated code need conversion 14 | */ 15 | public record ServiceDeclaration( 16 | String packageName, 17 | TypeElement serviceType, 18 | List calls, 19 | boolean requiresConverter 20 | ) { 21 | private static final String GENERATED_SUFFIX = "$Impl"; 22 | 23 | /** 24 | * Get the name of the generated class 25 | * 26 | * @return the name of the generated class 27 | */ 28 | public String generatedName() { 29 | return serviceType.getSimpleName().toString() + GENERATED_SUFFIX; 30 | } 31 | 32 | /** 33 | * Collects all the required adapters for this service. 34 | * 35 | * @return a list of required adapters 36 | */ 37 | public List requiredAdapters() { 38 | return calls.stream() 39 | .map(CallDeclaration::returnDeclaration) 40 | .map(ReturnDeclaration::adapterCall) 41 | .flatMap(Optional::stream) 42 | .map(ReturnAdapterCall::adapterClass) 43 | .distinct() 44 | .toList(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/java/io/github/zskamljic/restahead/processor/stock/QueryProcessorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.processor.stock; 2 | 3 | import io.github.zskamljic.restahead.processor.CommonProcessorTest; 4 | import org.junit.jupiter.api.Test; 5 | 6 | class QueryProcessorTest extends CommonProcessorTest { 7 | @Test 8 | void queryWithMissingNameFailsToCompile() { 9 | commonCompilationAssertion("query/MissingName.java") 10 | .failsToCompile() 11 | .withErrorContaining("Generating query parameter"); 12 | } 13 | 14 | @Test 15 | void pathWithMissingQueryFailsToCompile() { 16 | commonCompilationAssertion("query/MissingQueryValue.java") 17 | .failsToCompile() 18 | .withErrorContaining("Malformed query"); 19 | } 20 | 21 | @Test 22 | void queryInPathCompiles() { 23 | commonCompilationAssertion("query/QueryInPath.java") 24 | .compilesWithoutWarnings(); 25 | } 26 | 27 | @Test 28 | void combinedQueryCompiles() { 29 | commonCompilationAssertion("query/ValidCombinedQuery.java") 30 | .compilesWithoutWarnings(); 31 | } 32 | 33 | @Test 34 | void validQueryCompiles() { 35 | commonCompilationAssertion("query/ValidQuery.java") 36 | .compilesWithoutWarnings(); 37 | } 38 | 39 | @Test 40 | void iterablesAndPrimitivesCompile() { 41 | commonCompilationAssertion("query/CollectionAndArray.java") 42 | .compilesWithoutWarnings(); 43 | } 44 | 45 | @Test 46 | void enumCompiles() { 47 | commonCompilationAssertion("query/EnumQuery.java") 48 | .compilesWithoutWarnings(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /demo/src/test/java/io/github/zskamljic/restahead/demo/clients/BodyServiceTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.JacksonConverter; 4 | import io.github.zskamljic.restahead.RestAhead; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.Map; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.fail; 12 | 13 | class BodyServiceTest { 14 | private BodyService service; 15 | 16 | private static final Map DEMO_BODY = Map.of( 17 | "simple", 123, 18 | "object", Map.of( 19 | "subobject", "string" 20 | ) 21 | ); 22 | 23 | @BeforeEach 24 | void setUp() { 25 | service = RestAhead.builder("https://httpbin.org/") 26 | .converter(new JacksonConverter()) 27 | .build(BodyService.class); 28 | } 29 | 30 | @Test 31 | void getCompletesNormally() { 32 | try { 33 | service.get(); 34 | } catch (Exception e) { 35 | fail("Method must not throw"); 36 | } 37 | } 38 | 39 | @Test 40 | void patchRequestsSendsBody() { 41 | var response = service.patch(DEMO_BODY); 42 | 43 | assertEquals(DEMO_BODY, response.json()); 44 | } 45 | 46 | @Test 47 | void postRequestsSendsBody() { 48 | var response = service.post(DEMO_BODY); 49 | 50 | assertEquals(DEMO_BODY, response.json()); 51 | } 52 | 53 | @Test 54 | void putRequestsSendsBody() { 55 | var response = service.put(DEMO_BODY); 56 | 57 | assertEquals(DEMO_BODY, response.json()); 58 | } 59 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/validation/QueryValidator.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.validation; 2 | 3 | import io.github.zskamljic.restahead.modeling.declaration.RequestParameterSpec; 4 | 5 | import javax.annotation.processing.Messager; 6 | import javax.lang.model.element.VariableElement; 7 | import javax.lang.model.util.Elements; 8 | import javax.lang.model.util.Types; 9 | import javax.tools.Diagnostic; 10 | import java.util.Optional; 11 | 12 | /** 13 | * Used to validate queries and obtain parameter info. 14 | */ 15 | public class QueryValidator extends CommonParameterValidator { 16 | 17 | /** 18 | * Construct a new instance. 19 | * 20 | * @param messager the message to send errors to 21 | * @param elements the elements to use for class lookup 22 | * @param types an instance of Types utility 23 | */ 24 | public QueryValidator(Messager messager, Elements elements, Types types) { 25 | super(messager, elements, types); 26 | } 27 | 28 | /** 29 | * Get the query info. 30 | * 31 | * @param value the annotation value 32 | * @param parameter the parameter where to extract type, name etc. from 33 | * @return empty if errors were present, non-empty otherwise 34 | */ 35 | public Optional getQuerySpec(String value, VariableElement parameter) { 36 | if (value.isEmpty()) { 37 | messager.printMessage(Diagnostic.Kind.ERROR, "Generating query parameter from function parameter names not yet supported", parameter); 38 | return Optional.empty(); 39 | } 40 | 41 | return extractSpec(parameter, value); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/path/PathAnnotation$Impl.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.adapter.DefaultAdapters; 4 | import io.github.zskamljic.restahead.client.Client; 5 | import io.github.zskamljic.restahead.client.requests.Request; 6 | import io.github.zskamljic.restahead.client.requests.Verb; 7 | import io.github.zskamljic.restahead.exceptions.RestException; 8 | import java.lang.InterruptedException; 9 | import java.lang.Override; 10 | import java.lang.String; 11 | import java.util.concurrent.ExecutionException; 12 | import javax.annotation.processing.Generated; 13 | 14 | @Generated("Generated by RestAhead") 15 | public final class PathAnnotation$Impl implements PathAnnotation { 16 | private final String baseUrl; 17 | 18 | private final Client client; 19 | 20 | private final DefaultAdapters defaultAdapters; 21 | 22 | public PathAnnotation$Impl(String baseUrl, Client client, DefaultAdapters defaultAdapters) { 23 | this.baseUrl = baseUrl; 24 | this.client = client; 25 | this.defaultAdapters = defaultAdapters; 26 | } 27 | 28 | @Override 29 | public final void delete(String path) { 30 | var httpRequestBuilder = new Request.Builder() 31 | .setVerb(Verb.DELETE) 32 | .setBaseUrl(baseUrl) 33 | .setPath("/{path}".replace("{path}", String.valueOf(path))); 34 | var response = client.execute(httpRequestBuilder.build()); 35 | try { 36 | defaultAdapters.syncVoidAdapter(response); 37 | } catch (ExecutionException | InterruptedException exception) { 38 | throw RestException.getAppropriateException(exception); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/request/path/RequestPath.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.request.path; 2 | 3 | import java.net.URI; 4 | import java.net.URISyntaxException; 5 | import java.util.regex.Pattern; 6 | 7 | /** 8 | * The common request path logic. 9 | */ 10 | public abstract sealed class RequestPath permits StringPath, TemplatedPath { 11 | protected static final Pattern PATH_VARIABLE = Pattern.compile("(^|/)\\{(\\w+)}(?=$|/|\\?)"); 12 | protected final String path; 13 | 14 | protected RequestPath(String path) { 15 | this.path = path; 16 | } 17 | 18 | /** 19 | * Creates a valid, parsed version of the internal path with the query. 20 | * 21 | * @return the parsed URI 22 | * @throws URISyntaxException if URI is malformed 23 | */ 24 | public abstract URI uri() throws URISyntaxException; 25 | 26 | /** 27 | * Returns only the path from provided parameter. 28 | * 29 | * @return clean, unmodified path only substring 30 | */ 31 | @Override 32 | public String toString() { 33 | var queryStart = path.indexOf("?"); 34 | if (queryStart < 0) { 35 | return path; 36 | } 37 | return path.substring(0, queryStart); 38 | } 39 | 40 | /** 41 | * Parse the path with query, returning appropriate. 42 | * 43 | * @param path the path to parse 44 | * @return {@link TemplatedPath} if there's any placeholders present, {@link StringPath} if no processing is required 45 | */ 46 | public static RequestPath parse(String path) { 47 | if (PATH_VARIABLE.matcher(path).find()) { 48 | return new TemplatedPath(path); 49 | } 50 | return new StringPath(path); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/adapters/AdapterService$Impl.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.client.Client; 4 | import io.github.zskamljic.restahead.client.requests.Request; 5 | import io.github.zskamljic.restahead.client.requests.Verb; 6 | import io.github.zskamljic.restahead.client.responses.Response; 7 | import io.github.zskamljic.restahead.exceptions.RestException; 8 | import java.lang.InterruptedException; 9 | import java.lang.Override; 10 | import java.lang.String; 11 | import java.util.concurrent.ExecutionException; 12 | import java.util.stream.Stream; 13 | import javax.annotation.processing.Generated; 14 | 15 | @Generated("Generated by RestAhead") 16 | public final class AdapterService$Impl implements AdapterService { 17 | private final String baseUrl; 18 | 19 | private final Client client; 20 | 21 | private final AdapterService.StreamAdapter streamAdapter; 22 | 23 | public AdapterService$Impl(String baseUrl, Client client, 24 | AdapterService.StreamAdapter streamAdapter) { 25 | this.baseUrl = baseUrl; 26 | this.client = client; 27 | this.streamAdapter = streamAdapter; 28 | } 29 | 30 | @Override 31 | public final Stream delete() { 32 | var httpRequestBuilder = new Request.Builder() 33 | .setVerb(Verb.DELETE) 34 | .setBaseUrl(baseUrl) 35 | .setPath("/delete"); 36 | var response = client.execute(httpRequestBuilder.build()); 37 | try { 38 | return streamAdapter.adapt(response); 39 | } catch (ExecutionException | InterruptedException exception) { 40 | throw RestException.getAppropriateException(exception); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/client/Client.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client; 2 | 3 | import io.github.zskamljic.restahead.client.requests.Request; 4 | import io.github.zskamljic.restahead.client.responses.Response; 5 | import io.github.zskamljic.restahead.intercepting.Chain; 6 | import io.github.zskamljic.restahead.intercepting.Interceptor; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Objects; 11 | import java.util.concurrent.CompletableFuture; 12 | 13 | /** 14 | * Instances of Client should handle {@link Request} instances and return raw responses. 15 | */ 16 | public abstract class Client { 17 | private final List interceptors = new ArrayList<>(); 18 | 19 | /** 20 | * Adds a new interceptor to the list. 21 | * 22 | * @param interceptor the interceptor to add 23 | */ 24 | public void addInterceptor(Interceptor interceptor) { 25 | Objects.requireNonNull(interceptor); 26 | this.interceptors.add(interceptor); 27 | } 28 | 29 | /** 30 | * Execute the specified request, passing values through the interceptors. 31 | * 32 | * @param request the request to execute 33 | * @return the result from interceptors and internal client implementation 34 | */ 35 | public CompletableFuture execute(Request request) { 36 | return new Chain(interceptors, this::performRequest).proceed(request); 37 | } 38 | 39 | /** 40 | * Execute the specified request with internal implementation. 41 | * 42 | * @param request the request to perform 43 | * @return response from executing the request 44 | */ 45 | protected abstract CompletableFuture performRequest(Request request); 46 | } 47 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/test/java/io/github/zskamljic/restahead/generation/methods/HeaderValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.generation.methods; 2 | 3 | import io.github.zskamljic.restahead.modeling.validation.HeaderValidator; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.ValueSource; 8 | import org.mockito.Mock; 9 | import org.mockito.junit.jupiter.MockitoExtension; 10 | 11 | import javax.annotation.processing.Messager; 12 | import javax.lang.model.element.TypeElement; 13 | import javax.lang.model.element.VariableElement; 14 | import javax.lang.model.util.Elements; 15 | import javax.lang.model.util.Types; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertTrue; 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.Mockito.doReturn; 20 | import static org.mockito.Mockito.mock; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | class HeaderValidatorTest { 24 | @Mock 25 | private Messager messager; 26 | 27 | @Mock 28 | private Elements elements; 29 | 30 | @Mock 31 | private Types types; 32 | 33 | private HeaderValidator headerValidator; 34 | 35 | @BeforeEach 36 | void setUp() { 37 | doReturn(mock(TypeElement.class)).when(elements).getTypeElement(any()); 38 | headerValidator = new HeaderValidator(messager, elements, types); 39 | } 40 | 41 | @ParameterizedTest 42 | @ValueSource(strings = {"", "A B"}) 43 | void validatorReturnsEmptyForInvalidName(String header) { 44 | var result = headerValidator.getHeaderSpec(header, mock(VariableElement.class)); 45 | 46 | assertTrue(result.isEmpty()); 47 | } 48 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/path/PathAnnotationCodeName$Impl.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.adapter.DefaultAdapters; 4 | import io.github.zskamljic.restahead.client.Client; 5 | import io.github.zskamljic.restahead.client.requests.Request; 6 | import io.github.zskamljic.restahead.client.requests.Verb; 7 | import io.github.zskamljic.restahead.exceptions.RestException; 8 | import java.lang.InterruptedException; 9 | import java.lang.Override; 10 | import java.lang.String; 11 | import java.util.concurrent.ExecutionException; 12 | import javax.annotation.processing.Generated; 13 | 14 | @Generated("Generated by RestAhead") 15 | public final class PathAnnotationCodeName$Impl implements PathAnnotationCodeName { 16 | private final String baseUrl; 17 | 18 | private final Client client; 19 | 20 | private final DefaultAdapters defaultAdapters; 21 | 22 | public PathAnnotationCodeName$Impl(String baseUrl, Client client, 23 | DefaultAdapters defaultAdapters) { 24 | this.baseUrl = baseUrl; 25 | this.client = client; 26 | this.defaultAdapters = defaultAdapters; 27 | } 28 | 29 | @Override 30 | public final void delete(String path) { 31 | var httpRequestBuilder = new Request.Builder() 32 | .setVerb(Verb.DELETE) 33 | .setBaseUrl(baseUrl) 34 | .setPath("/{path}".replace("{path}", String.valueOf(path))); 35 | var response = client.execute(httpRequestBuilder.build()); 36 | try { 37 | defaultAdapters.syncVoidAdapter(response); 38 | } catch (ExecutionException | InterruptedException exception) { 39 | throw RestException.getAppropriateException(exception); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/jaxrs/JaxRsService.java: -------------------------------------------------------------------------------- 1 | package restahead.jaxrs; 2 | 3 | import io.github.zskamljic.restahead.client.requests.Verb; 4 | import io.github.zskamljic.restahead.client.responses.Response; 5 | import jakarta.ws.rs.Consumes; 6 | import jakarta.ws.rs.DELETE; 7 | import jakarta.ws.rs.FormParam; 8 | import jakarta.ws.rs.GET; 9 | import jakarta.ws.rs.HEAD; 10 | import jakarta.ws.rs.HeaderParam; 11 | import jakarta.ws.rs.OPTIONS; 12 | import jakarta.ws.rs.PATCH; 13 | import jakarta.ws.rs.POST; 14 | import jakarta.ws.rs.PUT; 15 | import jakarta.ws.rs.Path; 16 | import jakarta.ws.rs.PathParam; 17 | import jakarta.ws.rs.Produces; 18 | import java.util.List; 19 | 20 | public interface JaxRsService { 21 | @DELETE 22 | @Path("/delete") 23 | Response performDelete( 24 | @HeaderParam("custom") String header1, 25 | @HeaderParam("header2") String header2 26 | ); 27 | 28 | @GET 29 | @Path("/{get}/{hello}") 30 | Response performGet( 31 | @PathParam("") String get, 32 | @PathParam("hello") String second 33 | ); 34 | 35 | @HEAD 36 | @Path("/{get}/{hello}") 37 | Response performHead( 38 | @PathParam("") String get, 39 | @PathParam("hello") String second 40 | ); 41 | 42 | @OPTIONS 43 | @Path("/{get}/{hello}") 44 | List performOptions( 45 | @PathParam("") String get, 46 | @PathParam("hello") String second 47 | ); 48 | 49 | @PATCH 50 | @Path("/patch") 51 | Response performPatch(); 52 | 53 | @POST 54 | @Path("/post") 55 | Response performPost(); 56 | 57 | @PUT 58 | @Path("/put") 59 | @Consumes("application/json") 60 | @Produces("text/plain") 61 | Response performPut(); 62 | 63 | record FormBody(String field) { 64 | } 65 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/spring/SpringService.java: -------------------------------------------------------------------------------- 1 | package restahead.spring; 2 | 3 | import io.github.zskamljic.restahead.client.responses.Response; 4 | import org.springframework.web.bind.annotation.DeleteMapping; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PatchMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.PutMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RequestHeader; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestMethod; 14 | import org.springframework.web.bind.annotation.RequestPart; 15 | import org.springframework.web.multipart.MultipartFile; 16 | 17 | import java.util.Map; 18 | 19 | interface SpringService { 20 | @DeleteMapping("/delete") 21 | Response performDelete( 22 | @RequestHeader("custom") String header1, 23 | @RequestHeader("header2") String header2 24 | ); 25 | 26 | @GetMapping("/{get}/{hello}") 27 | Response performGet( 28 | @PathVariable String get, 29 | @PathVariable("hello") String second 30 | ); 31 | 32 | @PatchMapping("/patch") 33 | Response performPatch( 34 | @RequestBody Map body 35 | ); 36 | 37 | @PostMapping("/post") 38 | Response performPost( 39 | @RequestPart String part, 40 | @RequestPart MultipartFile file 41 | ); 42 | 43 | @PutMapping("/put") 44 | Response performPut(); 45 | 46 | @RequestMapping(method = RequestMethod.GET, value = "/get") 47 | Response customGet(); 48 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/test/java/io/github/zskamljic/restahead/RestAheadTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead; 2 | 3 | import io.github.zskamljic.restahead.client.Client; 4 | import io.github.zskamljic.restahead.conversion.Converter; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertNotNull; 8 | import static org.junit.jupiter.api.Assertions.assertThrows; 9 | import static org.mockito.Mockito.mock; 10 | 11 | class RestAheadTest { 12 | @Test 13 | void buildCreatesInstanceForClientOnly() { 14 | var instance = RestAhead.builder("https://httpbin.org") 15 | .build(SimpleGet.class); 16 | 17 | assertNotNull(instance); 18 | } 19 | 20 | @Test 21 | void buildCreatesInstanceForConverter() { 22 | var instance = RestAhead.builder("https://httpbin.org") 23 | .converter(mock(Converter.class)) 24 | .build(ConverterGet.class); 25 | 26 | assertNotNull(instance); 27 | } 28 | 29 | @Test 30 | void buildThrowsForMissingConverter() { 31 | assertThrows(IllegalStateException.class, () -> { 32 | RestAhead.builder("https://httpbin.org") 33 | .build(ConverterGet.class); 34 | }); 35 | } 36 | 37 | interface SimpleGet { 38 | void doGet(); 39 | } 40 | 41 | interface ConverterGet extends SimpleGet { 42 | } 43 | } 44 | 45 | class SimpleGet$Impl implements RestAheadTest.SimpleGet { 46 | public SimpleGet$Impl(String url, Client client) { 47 | } 48 | 49 | @Override 50 | public void doGet() { 51 | } 52 | } 53 | 54 | class ConverterGet$Impl implements RestAheadTest.ConverterGet { 55 | public ConverterGet$Impl(String url, Client client, Converter converter) { 56 | } 57 | 58 | @Override 59 | public void doGet() { 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/basic/InterfaceWithThrows$Impl.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.adapter.DefaultAdapters; 4 | import io.github.zskamljic.restahead.client.Client; 5 | import io.github.zskamljic.restahead.client.requests.Request; 6 | import io.github.zskamljic.restahead.client.requests.Verb; 7 | import io.github.zskamljic.restahead.client.responses.Response; 8 | import io.github.zskamljic.restahead.exceptions.RestException; 9 | import java.io.IOException; 10 | import java.lang.InterruptedException; 11 | import java.lang.Override; 12 | import java.lang.String; 13 | import java.util.concurrent.ExecutionException; 14 | import javax.annotation.processing.Generated; 15 | 16 | @Generated("Generated by RestAhead") 17 | public final class InterfaceWithThrows$Impl implements InterfaceWithThrows { 18 | private final String baseUrl; 19 | 20 | private final Client client; 21 | 22 | private final DefaultAdapters defaultAdapters; 23 | 24 | public InterfaceWithThrows$Impl(String baseUrl, Client client, 25 | DefaultAdapters defaultAdapters) { 26 | this.baseUrl = baseUrl; 27 | this.client = client; 28 | this.defaultAdapters = defaultAdapters; 29 | } 30 | 31 | @Override 32 | public final Response delete() throws IOException { 33 | var httpRequestBuilder = new Request.Builder() 34 | .setVerb(Verb.DELETE) 35 | .setBaseUrl(baseUrl) 36 | .setPath("/delete"); 37 | var response = client.execute(httpRequestBuilder.build()); 38 | try { 39 | return defaultAdapters.syncAdapter(response); 40 | } catch (ExecutionException | InterruptedException exception) { 41 | throw RestException.getAppropriateException(exception); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/test/java/io/github/zskamljic/restahead/conversion/OptionsConverterTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.conversion; 2 | 3 | import io.github.zskamljic.restahead.client.requests.Verb; 4 | import io.github.zskamljic.restahead.client.responses.Response; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.io.InputStream; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | class OptionsConverterTest { 16 | @Test 17 | void generateOptionsListWildcard() { 18 | var headers = Map.of( 19 | "Allow", List.of("*") 20 | ); 21 | var response = new Response(0, headers, InputStream.nullInputStream()); 22 | 23 | var verbs = OptionsConverter.parseOptions(response); 24 | assertTrue(Arrays.stream(Verb.values()).allMatch(verbs::contains)); 25 | } 26 | 27 | @Test 28 | void generateOptionsListDuplicates() { 29 | var headers = Map.of( 30 | "Allow", List.of("GET", "GET") 31 | ); 32 | var response = new Response(0, headers, InputStream.nullInputStream()); 33 | 34 | var verbs = OptionsConverter.parseOptions(response); 35 | assertEquals(1, verbs.size()); 36 | assertEquals(Verb.GET, verbs.get(0)); 37 | } 38 | 39 | @Test 40 | void generateOptionsListMultiple() { 41 | var headers = Map.of( 42 | "Allow", List.of("GET", "POST") 43 | ); 44 | var response = new Response(0, headers, InputStream.nullInputStream()); 45 | 46 | var verbs = OptionsConverter.parseOptions(response); 47 | assertEquals(2, verbs.size()); 48 | assertTrue(verbs.contains(Verb.POST)); 49 | assertTrue(verbs.contains(Verb.GET)); 50 | } 51 | } -------------------------------------------------------------------------------- /demo/src/test/java/io/github/zskamljic/restahead/demo/clients/AuthorizationServiceTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.JacksonConverter; 4 | import io.github.zskamljic.restahead.RestAhead; 5 | import io.github.zskamljic.restahead.exceptions.RequestFailedException; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertThrows; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | class AuthorizationServiceTest { 14 | private static final String TOKEN = "token"; 15 | private static final String USERNAME = "user"; 16 | private static final String PASSWORD = "password"; 17 | 18 | private AuthorizationService service; 19 | 20 | @BeforeEach 21 | void setUp() { 22 | service = RestAhead.builder("https://httpbin.org/") 23 | .converter(new JacksonConverter()) 24 | .build(AuthorizationService.class); 25 | } 26 | 27 | @Test 28 | void basicAuthHandles401() { 29 | var response = service.getBasicAuth(USERNAME, PASSWORD, ""); 30 | 31 | assertEquals(401, response.status()); 32 | } 33 | 34 | @Test 35 | void basicAuthSucceeds() { 36 | var response = service.getBasicAuth(USERNAME, PASSWORD, "Basic dXNlcjpwYXNzd29yZA=="); 37 | 38 | assertEquals(200, response.status()); 39 | } 40 | 41 | @Test 42 | void bearerAuthSucceeds() { 43 | var response = service.getBearer("Bearer " + TOKEN); 44 | 45 | assertTrue(response.authenticated()); 46 | assertEquals(TOKEN, response.token()); 47 | } 48 | 49 | @Test 50 | void bearerThrowsForNonSuccess() { 51 | assertThrows(RequestFailedException.class, () -> service.getBearer("")); 52 | } 53 | } -------------------------------------------------------------------------------- /rest-ahead-jax-rs/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | RestAhead 7 | io.github.zskamljic 8 | 0.5.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | rest-ahead-jax-rs 13 | 14 | 15 | 17 16 | 17 17 | 18 | 19 | 20 | 21 | io.github.zskamljic 22 | rest-ahead-processor 23 | 0.5.0-SNAPSHOT 24 | 25 | 26 | jakarta.ws.rs 27 | jakarta.ws.rs-api 28 | 3.0.0 29 | 30 | 31 | 32 | 33 | 34 | 35 | org.apache.maven.plugins 36 | maven-compiler-plugin 37 | ${compiler.plugin.version} 38 | 39 | 40 | default-compile 41 | 42 | -proc:none 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /rest-ahead-spring-dialect/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | RestAhead 7 | io.github.zskamljic 8 | 0.5.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | rest-ahead-spring-dialect 13 | 14 | 15 | 17 16 | 17 17 | 18 | 19 | 20 | 21 | io.github.zskamljic 22 | rest-ahead-processor 23 | 0.5.0-SNAPSHOT 24 | 25 | 26 | org.springframework 27 | spring-web 28 | ${spring.version} 29 | 30 | 31 | 32 | 33 | 34 | 35 | org.apache.maven.plugins 36 | maven-compiler-plugin 37 | ${compiler.plugin.version} 38 | 39 | 40 | default-compile 41 | 42 | -proc:none 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/generation/ExceptionsGenerator.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.generation; 2 | 3 | import io.github.zskamljic.restahead.exceptions.RestException; 4 | import com.squareup.javapoet.MethodSpec; 5 | 6 | import javax.lang.model.type.TypeMirror; 7 | import java.util.List; 8 | import java.util.function.Predicate; 9 | 10 | /** 11 | * Generates exception code for code that may or may not require exceptions. 12 | */ 13 | public class ExceptionsGenerator { 14 | /** 15 | * Surrounds the statement returned by {@param statementGenerator} by try-catch block if needed. 16 | * 17 | * @param builder the builder to add the exception to 18 | * @param declaredExceptions the exceptions that the function declares - these will not be caught 19 | * @param actualExceptions the exceptions that can be thrown by the surrounded statement 20 | * @param statementGenerator the runnable that generates 21 | */ 22 | public void generateTryCatchIfNeeded( 23 | MethodSpec.Builder builder, 24 | List declaredExceptions, 25 | List actualExceptions, 26 | Runnable statementGenerator 27 | ) { 28 | var missingExceptions = actualExceptions.stream() 29 | .filter(Predicate.not(declaredExceptions::contains)) 30 | .toList(); 31 | if (!missingExceptions.isEmpty()) { 32 | builder.beginControlFlow("try"); 33 | } 34 | statementGenerator.run(); 35 | if (!missingExceptions.isEmpty()) { 36 | var exceptionList = String.join(" | ", missingExceptions.stream().map(e -> "$T").toList()); 37 | builder.nextControlFlow("catch (" + exceptionList + " exception)", missingExceptions.toArray(Object[]::new)) 38 | .addStatement("throw $T.getAppropriateException(exception)", RestException.class) 39 | .endControlFlow(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /rest-ahead-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | RestAhead 7 | io.github.zskamljic 8 | 0.5.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | rest-ahead-client 13 | jar 14 | ${project.groupId}:${project.artifactId} 15 | Libraries to define basic HTTP client wrapper and logic to use generated REST services akin to Retrofit 16 | and Feign. 17 | 18 | 19 | 20 | 21 | 22 | org.junit.jupiter 23 | junit-jupiter-api 24 | ${junit.version} 25 | test 26 | 27 | 28 | org.junit.jupiter 29 | junit-jupiter-engine 30 | ${junit.version} 31 | test 32 | 33 | 34 | org.mockito 35 | mockito-junit-jupiter 36 | ${mockito.version} 37 | test 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.apache.maven.plugins 45 | maven-surefire-plugin 46 | ${surefire.version} 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/java/io/github/zskamljic/restahead/processor/stock/PathProcessorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.processor.stock; 2 | 3 | import com.google.testing.compile.JavaFileObjects; 4 | import io.github.zskamljic.restahead.processor.CommonProcessorTest; 5 | import org.junit.jupiter.api.Test; 6 | 7 | class PathProcessorTest extends CommonProcessorTest { 8 | @Test 9 | void interfaceWithInvalidPathFailsToCompile() { 10 | commonCompilationAssertion("path/InvalidPath.java") 11 | .failsToCompile() 12 | .withErrorContaining("path"); 13 | } 14 | 15 | @Test 16 | void pathAnnotationCompilesCorrectly() { 17 | commonCompilationAssertion("path/PathAnnotation.java") 18 | .compilesWithoutWarnings() 19 | .and() 20 | .generatesSources(JavaFileObjects.forResource("path/PathAnnotation$Impl.java")); 21 | } 22 | 23 | @Test 24 | void pathAnnotationCompilesWithParameterName() { 25 | commonCompilationAssertion("path/PathAnnotationCodeName.java") 26 | .compilesWithoutWarnings() 27 | .and() 28 | .generatesSources(JavaFileObjects.forResource("path/PathAnnotationCodeName$Impl.java")); 29 | } 30 | 31 | @Test 32 | void pathAnnotationWithoutPlaceholderFails() { 33 | commonCompilationAssertion("path/PathWithoutPlaceholder.java") 34 | .failsToCompile() 35 | .withErrorContaining("parts are present, but there are none expected"); 36 | } 37 | 38 | @Test 39 | void pathAnnotationFailsForDuplicatePlaceholders() { 40 | commonCompilationAssertion("path/PathWithDuplicates.java") 41 | .failsToCompile() 42 | .withErrorContaining("duplicate"); 43 | } 44 | 45 | @Test 46 | void pathWithIterableFailsToCompile() { 47 | commonCompilationAssertion("path/PathWithIterable.java") 48 | .failsToCompile() 49 | .withErrorContaining("singular"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/client/requests/parts/MultiPart.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client.requests.parts; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.InputStream; 5 | import java.io.SequenceInputStream; 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | /** 11 | * Common logic for both fields and files. 12 | */ 13 | public abstract sealed class MultiPart permits FieldPart, FilePart { 14 | protected static final String SEPARATOR = "\r\n"; 15 | private final String fieldName; 16 | 17 | protected MultiPart(String fieldName) { 18 | this.fieldName = fieldName; 19 | } 20 | 21 | /** 22 | * Creates an input stream containing the boundary and content. 23 | * 24 | * @param boundary the boundary to use 25 | * @return the {@link InputStream} with written body 26 | */ 27 | public final InputStream inputStream(String boundary) { 28 | var prefix = "--" + boundary + SEPARATOR + 29 | contentDisposition() + SEPARATOR + SEPARATOR; 30 | 31 | var prefixStream = new ByteArrayInputStream(prefix.getBytes(StandardCharsets.US_ASCII)); 32 | var suffixStream = new ByteArrayInputStream(SEPARATOR.getBytes(StandardCharsets.US_ASCII)); 33 | return new SequenceInputStream(Collections.enumeration(List.of( 34 | prefixStream, 35 | bodyContent(), 36 | suffixStream 37 | ))); 38 | } 39 | 40 | /** 41 | * Content disposition line for this type. 42 | * 43 | * @return the filled disposition 44 | */ 45 | protected String contentDisposition() { 46 | return "Content-Disposition: form-data; name=\"" + fieldName + "\""; 47 | } 48 | 49 | /** 50 | * Get content for this part. 51 | * 52 | * @return the stream that when read will provide content of this part's body 53 | */ 54 | protected abstract InputStream bodyContent(); 55 | } 56 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/spring/ConfigCombinations.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.spring; 2 | 3 | import io.github.zskamljic.restahead.annotations.verbs.Get; 4 | import io.github.zskamljic.restahead.client.requests.Request; 5 | import io.github.zskamljic.restahead.client.responses.Response; 6 | import io.github.zskamljic.restahead.demo.adapters.SupplierAdapter; 7 | import io.github.zskamljic.restahead.intercepting.Chain; 8 | import io.github.zskamljic.restahead.intercepting.Interceptor; 9 | import io.github.zskamljic.restahead.spring.RestAheadService; 10 | 11 | import java.util.concurrent.CompletableFuture; 12 | import java.util.function.Supplier; 13 | 14 | public class ConfigCombinations { 15 | private static final String BASE_URL = "https://httpbin.org/"; 16 | 17 | public static class PassThroughInterceptor implements Interceptor { 18 | @Override 19 | public CompletableFuture intercept(Chain chain, Request request) { 20 | return chain.proceed(request); 21 | } 22 | } 23 | 24 | @RestAheadService(url = BASE_URL, client = DummyClient.class) 25 | interface ClientOnlyService { 26 | @Get("/get") 27 | Response get(); 28 | } 29 | 30 | @RestAheadService(url = BASE_URL, client = DummyClient.class, interceptors = PassThroughInterceptor.class) 31 | interface ClientAndInterceptorService { 32 | @Get("/get") 33 | Response get(); 34 | } 35 | 36 | @RestAheadService(url = BASE_URL, interceptors = PassThroughInterceptor.class) 37 | interface InterceptorService { 38 | @Get("/get") 39 | Response get(); 40 | } 41 | 42 | @RestAheadService(url = BASE_URL, adapters = SupplierAdapter.class) 43 | interface AdapterService { 44 | @Get("/get") 45 | Supplier get(); 46 | } 47 | 48 | @RestAheadService(url = "${placeholder.url}") 49 | interface PlaceholderService { 50 | @Get("/get") 51 | Response get(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/zskamljic/restahead/demo/clients/FormService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.clients; 2 | 3 | import io.github.zskamljic.restahead.annotations.form.FormName; 4 | import io.github.zskamljic.restahead.annotations.form.FormUrlEncoded; 5 | import io.github.zskamljic.restahead.annotations.form.Part; 6 | import io.github.zskamljic.restahead.annotations.request.Body; 7 | import io.github.zskamljic.restahead.annotations.verbs.Post; 8 | import io.github.zskamljic.restahead.client.requests.parts.FilePart; 9 | import io.github.zskamljic.restahead.demo.models.ExternalFormBody; 10 | import io.github.zskamljic.restahead.demo.models.HttpBinResponse; 11 | 12 | import java.io.File; 13 | import java.nio.file.Path; 14 | import java.util.Map; 15 | 16 | public interface FormService { 17 | @Post("/post") 18 | HttpBinResponse post(@FormUrlEncoded @Body Map body); 19 | 20 | @Post("/post") 21 | HttpBinResponse postRecord(@FormUrlEncoded @Body Sample body); 22 | 23 | @Post("/post") 24 | HttpBinResponse postClass(@FormUrlEncoded @Body SampleClass body); 25 | 26 | @Post("/post") 27 | HttpBinResponse postOtherModel(@FormUrlEncoded ExternalFormBody body); 28 | 29 | @Post("/post") 30 | HttpBinResponse postMultiPart( 31 | @Part String part, 32 | @Body @Part("two") String part2, 33 | @Part File file, 34 | @Part Path path, 35 | @Part FilePart input, 36 | @Part FilePart body 37 | ); 38 | 39 | record Sample(String first, @FormName("2nd") String second) { 40 | } 41 | 42 | class SampleClass { 43 | private final String first; 44 | private final String second; 45 | 46 | public SampleClass(String first, String second) { 47 | this.first = first; 48 | this.second = second; 49 | } 50 | 51 | public String getFirst() { 52 | return first; 53 | } 54 | 55 | @FormName("2nd") 56 | public String getSecond() { 57 | return second; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormWithPart$Impl.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.adapter.DefaultAdapters; 4 | import io.github.zskamljic.restahead.client.Client; 5 | import io.github.zskamljic.restahead.client.requests.MultiPartRequest; 6 | import io.github.zskamljic.restahead.client.requests.Request; 7 | import io.github.zskamljic.restahead.client.requests.Verb; 8 | import io.github.zskamljic.restahead.client.requests.parts.FieldPart; 9 | import io.github.zskamljic.restahead.conversion.Converter; 10 | import io.github.zskamljic.restahead.exceptions.RestException; 11 | import java.lang.InterruptedException; 12 | import java.lang.Override; 13 | import java.lang.String; 14 | import java.util.concurrent.ExecutionException; 15 | import javax.annotation.processing.Generated; 16 | 17 | @Generated("Generated by RestAhead") 18 | public final class FormWithPart$Impl implements FormWithPart { 19 | private final String baseUrl; 20 | 21 | private final Client client; 22 | 23 | private final Converter converter; 24 | 25 | private final DefaultAdapters defaultAdapters; 26 | 27 | public FormWithPart$Impl(String baseUrl, Client client, Converter converter, 28 | DefaultAdapters defaultAdapters) { 29 | this.baseUrl = baseUrl; 30 | this.client = client; 31 | this.converter = converter; 32 | this.defaultAdapters = defaultAdapters; 33 | } 34 | 35 | @Override 36 | public final void post(String body) { 37 | var httpRequestBuilder = new Request.Builder() 38 | .setVerb(Verb.POST) 39 | .setBaseUrl(baseUrl) 40 | .setPath(""); 41 | MultiPartRequest.builder() 42 | .addPart(new FieldPart("body", body)) 43 | .buildInto(httpRequestBuilder); 44 | var response = client.execute(httpRequestBuilder.build()); 45 | try { 46 | defaultAdapters.syncVoidAdapter(response); 47 | } catch (ExecutionException | InterruptedException exception) { 48 | throw RestException.getAppropriateException(exception); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/encoding/generation/FormConversionStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.encoding.generation; 2 | 3 | import javax.annotation.processing.Messager; 4 | import javax.lang.model.type.TypeMirror; 5 | import javax.lang.model.util.Elements; 6 | import javax.lang.model.util.Types; 7 | import java.util.Optional; 8 | import java.util.stream.Stream; 9 | 10 | /** 11 | * Outlines the generation strategy for some type. 12 | */ 13 | public sealed interface FormConversionStrategy permits ClassGenerationStrategy, MapConversionStrategy, RecordGenerationStrategy { 14 | /** 15 | * The type that this strategy applies to. Return from this value will be used to ensure that only one converter 16 | * will be generated for each type. 17 | * 18 | * @return the type this converter applies to 19 | */ 20 | TypeMirror type(); 21 | 22 | /** 23 | * Selects an appropriate generation strategy for given type. 24 | * 25 | * @param messager the messager to report errors to 26 | * @param elements the elements to fetch type information from 27 | * @param types the types utility to use for typing info 28 | * @param mirror the type for which to find a strategy 29 | * @return the strategy or empty if none was found 30 | */ 31 | static Optional select(Messager messager, Elements elements, Types types, TypeMirror mirror) { 32 | Stream providers = Stream.of( 33 | MapConversionStrategy::getIfSupported, 34 | RecordGenerationStrategy::getIfSupported, 35 | ClassGenerationStrategy::getIfSupported 36 | ); 37 | return providers.map(provider -> provider.getIfSupported(messager, elements, types, mirror)) 38 | .flatMap(Optional::stream) 39 | .findFirst(); 40 | } 41 | 42 | /** 43 | * Common signature for static functions 44 | */ 45 | @FunctionalInterface 46 | interface OptionalStrategyProvider { 47 | Optional getIfSupported(Messager messager, Elements elements, Types types, TypeMirror mirror); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/BodyService$Impl.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.adapter.DefaultAdapters; 4 | import io.github.zskamljic.restahead.client.Client; 5 | import io.github.zskamljic.restahead.client.requests.Request; 6 | import io.github.zskamljic.restahead.client.requests.Verb; 7 | import io.github.zskamljic.restahead.conversion.Converter; 8 | import io.github.zskamljic.restahead.exceptions.RestException; 9 | import java.io.IOException; 10 | import java.lang.InterruptedException; 11 | import java.lang.Object; 12 | import java.lang.Override; 13 | import java.lang.String; 14 | import java.util.Map; 15 | import java.util.concurrent.ExecutionException; 16 | import javax.annotation.processing.Generated; 17 | 18 | @Generated("Generated by RestAhead") 19 | public final class BodyService$Impl implements BodyService { 20 | private final String baseUrl; 21 | 22 | private final Client client; 23 | 24 | private final Converter converter; 25 | 26 | private final DefaultAdapters defaultAdapters; 27 | 28 | public BodyService$Impl(String baseUrl, Client client, Converter converter, 29 | DefaultAdapters defaultAdapters) { 30 | this.baseUrl = baseUrl; 31 | this.client = client; 32 | this.converter = converter; 33 | this.defaultAdapters = defaultAdapters; 34 | } 35 | 36 | @Override 37 | public final void post(Map body) { 38 | var httpRequestBuilder = new Request.Builder() 39 | .setVerb(Verb.POST) 40 | .setBaseUrl(baseUrl) 41 | .setPath("/post"); 42 | try { 43 | httpRequestBuilder.setBody(converter.serialize(body)); 44 | } catch (IOException exception) { 45 | throw RestException.getAppropriateException(exception); 46 | } 47 | var response = client.execute(httpRequestBuilder.build()); 48 | try { 49 | defaultAdapters.syncVoidAdapter(response); 50 | } catch (ExecutionException | InterruptedException exception) { 51 | throw RestException.getAppropriateException(exception); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/test/java/io/github/zskamljic/restahead/client/requests/parts/FilePartTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client.requests.parts; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | class FilePartTest { 11 | private static final String FIELD_NAME = "field"; 12 | private static final String FILE_NAME = "fileName"; 13 | 14 | @Test 15 | void inputFileGeneratesValidInput() throws IOException { 16 | var tmpFile = File.createTempFile("pre", "post"); 17 | 18 | var input = new FilePart(FIELD_NAME, FILE_NAME, tmpFile); 19 | 20 | assertEquals(""" 21 | --boundary\r 22 | Content-Disposition: form-data; name="field"; filename="fileName"\r 23 | Content-Type: application/octet-stream\r 24 | \r 25 | \r 26 | """, readStringFrom(input)); 27 | } 28 | 29 | @Test 30 | void inputStreamGeneratesValidInput() throws IOException { 31 | var tmpFile = File.createTempFile("pre", "post"); 32 | 33 | var input = new FilePart(FIELD_NAME, FILE_NAME, tmpFile.toPath()); 34 | 35 | assertEquals(""" 36 | --boundary\r 37 | Content-Disposition: form-data; name="field"; filename="fileName"\r 38 | Content-Type: application/octet-stream\r 39 | \r 40 | \r 41 | """, readStringFrom(input)); 42 | } 43 | 44 | @Test 45 | void bytesGeneratesValidInput() throws IOException { 46 | var data = "data".getBytes(); 47 | 48 | var input = new FilePart(FIELD_NAME, FILE_NAME, "application/octet-stream", data); 49 | 50 | assertEquals(""" 51 | --boundary\r 52 | Content-Disposition: form-data; name="field"; filename="fileName"\r 53 | Content-Type: application/octet-stream\r 54 | \r 55 | data\r 56 | """, readStringFrom(input)); 57 | } 58 | 59 | private String readStringFrom(FilePart field) throws IOException { 60 | return new String(field.inputStream("boundary").readAllBytes()); 61 | } 62 | } -------------------------------------------------------------------------------- /test-report-aggregator/src/test/java/io/github/zskamljic/restahead/processor/stock/ResponseProcessorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.processor.stock; 2 | 3 | import com.google.testing.compile.JavaFileObjects; 4 | import io.github.zskamljic.restahead.processor.CommonProcessorTest; 5 | import org.junit.jupiter.api.Test; 6 | 7 | class ResponseProcessorTest extends CommonProcessorTest { 8 | @Test 9 | void interfaceWithResponseCompiles() { 10 | commonCompilationAssertion("response/ServiceWithResponse.java") 11 | .compilesWithoutWarnings(); 12 | } 13 | 14 | @Test 15 | void interfaceWithUnknownResponseCompiles() { 16 | commonCompilationAssertion("response/ServiceWithUnknownResponse.java") 17 | .compilesWithoutWarnings() 18 | .and() 19 | .generatesSources(JavaFileObjects.forResource("response/ServiceWithUnknownResponse$Impl.java")); 20 | } 21 | 22 | @Test 23 | void interfaceWithGenericResponseCompiles() { 24 | commonCompilationAssertion("response/ServiceWithGenericResponse.java") 25 | .compilesWithoutWarnings() 26 | .and() 27 | .generatesSources(JavaFileObjects.forResource("response/ServiceWithGenericResponse$Impl.java")); 28 | } 29 | 30 | @Test 31 | void futureGenericResponseCompiles() { 32 | commonCompilationAssertion("response/FutureGenericResponse.java") 33 | .compilesWithoutWarnings() 34 | .and() 35 | .generatesSources(JavaFileObjects.forResource("response/FutureGenericResponse$Impl.java")); 36 | } 37 | 38 | @Test 39 | void responseBodyCompiles() { 40 | commonCompilationAssertion("response/ResponseWithBody.java") 41 | .compilesWithoutWarnings() 42 | .and() 43 | .generatesSources(JavaFileObjects.forResource("response/ResponseWithBody$Impl.java")); 44 | } 45 | 46 | @Test 47 | void responseBodyAndErrorCompiles() { 48 | commonCompilationAssertion("response/ResponseWithErrorBody.java") 49 | .compilesWithoutWarnings() 50 | .and() 51 | .generatesSources(JavaFileObjects.forResource("response/ResponseWithErrorBody$Impl.java")); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/encoding/generation/MapConversionStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.encoding.generation; 2 | 3 | import io.github.zskamljic.restahead.modeling.TypeValidator; 4 | 5 | import javax.annotation.processing.Messager; 6 | import javax.lang.model.type.DeclaredType; 7 | import javax.lang.model.type.TypeMirror; 8 | import javax.lang.model.util.Elements; 9 | import javax.lang.model.util.Types; 10 | import javax.tools.Diagnostic; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | 14 | /** 15 | * Generates a form converter for maps. 16 | */ 17 | public record MapConversionStrategy(TypeMirror type) implements FormConversionStrategy { 18 | 19 | /** 20 | * Checks if provided type is a Map or one of the subclasses that has string representable keys and values. 21 | * 22 | * @param elements the elements to fetch type information from 23 | * @param types the types utility to use for typing info 24 | * @param mirror the type for which to find a strategy 25 | * @return a strategy if data is valid, empty otherwise 26 | */ 27 | public static Optional getIfSupported(Messager messager, Elements elements, Types types, TypeMirror mirror) { 28 | var type = elements.getTypeElement(Map.class.getCanonicalName()) 29 | .asType(); 30 | if (!types.isAssignable(types.erasure(mirror), type)) { 31 | return Optional.empty(); 32 | } 33 | var mapType = (DeclaredType) mirror; 34 | var genericArguments = mapType.getTypeArguments(); 35 | if (genericArguments.size() != 2) return Optional.empty(); 36 | 37 | var stringValidator = new TypeValidator(elements, types); 38 | var key = genericArguments.get(0); 39 | var value = genericArguments.get(1); 40 | 41 | if (stringValidator.isUnsupportedType(key) || stringValidator.isUnsupportedType(value)) { 42 | messager.printMessage(Diagnostic.Kind.ERROR, "Maps must consist of string representable values to be formEncoded", mapType.asElement()); 43 | return Optional.empty(); 44 | } 45 | 46 | return Optional.of(new MapConversionStrategy(types.erasure(type))); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rest-ahead-client/src/test/java/io/github/zskamljic/restahead/client/requests/parts/FieldPartTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client.requests.parts; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.IOException; 6 | import java.util.UUID; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | class FieldPartTest { 11 | private static final String FIELD_NAME = "name"; 12 | 13 | @Test 14 | void stringValueGeneratesCorrectStream() throws IOException { 15 | var field = new FieldPart(FIELD_NAME, "value"); 16 | 17 | var data = readStringFrom(field); 18 | 19 | assertEquals(""" 20 | --boundary\r 21 | Content-Disposition: form-data; name="name"\r 22 | \r 23 | value\r 24 | """, data); 25 | } 26 | 27 | @Test 28 | void uuidValueGeneratesCorrectStream() throws IOException { 29 | var uuid = UUID.randomUUID(); 30 | var field = new FieldPart(FIELD_NAME, uuid); 31 | 32 | var data = readStringFrom(field); 33 | 34 | assertEquals(""" 35 | --boundary\r 36 | Content-Disposition: form-data; name="name"\r 37 | \r 38 | %s\r 39 | """.formatted(uuid), data); 40 | } 41 | 42 | @Test 43 | void numberGeneratesCorrectStream() throws IOException { 44 | var field = new FieldPart(FIELD_NAME, 5); 45 | 46 | var data = readStringFrom(field); 47 | 48 | assertEquals(""" 49 | --boundary\r 50 | Content-Disposition: form-data; name="name"\r 51 | \r 52 | %s\r 53 | """.formatted(5), data); 54 | } 55 | 56 | @Test 57 | void charactersCorrectStream() throws IOException { 58 | var field = new FieldPart(FIELD_NAME, 'a'); 59 | 60 | var data = readStringFrom(field); 61 | 62 | assertEquals(""" 63 | --boundary\r 64 | Content-Disposition: form-data; name="name"\r 65 | \r 66 | %s\r 67 | """.formatted('a'), data); 68 | } 69 | 70 | private String readStringFrom(FieldPart field) throws IOException { 71 | return new String(field.inputStream("boundary").readAllBytes()); 72 | } 73 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/client/requests/MultiPartRequest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client.requests; 2 | 3 | import io.github.zskamljic.restahead.client.requests.parts.MultiPart; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.SequenceInputStream; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | /** 14 | * This class can be used to generate multipart request bodies. 15 | */ 16 | public class MultiPartRequest { 17 | private final List parts = new ArrayList<>(); 18 | 19 | private MultiPartRequest() { 20 | } 21 | 22 | /** 23 | * Starts building a new instance. 24 | * 25 | * @return reference to the builder 26 | */ 27 | public static MultiPartRequest builder() { 28 | return new MultiPartRequest(); 29 | } 30 | 31 | /** 32 | * Adds a new part of the request. 33 | * 34 | * @param part the part to add 35 | * @return reference to the builder 36 | */ 37 | public MultiPartRequest addPart(MultiPart part) { 38 | parts.add(part); 39 | return this; 40 | } 41 | 42 | /** 43 | * Generates a boundary string. 44 | * 45 | * @return the boundary string to use 46 | */ 47 | String generateBoundary() { 48 | return "----" + UUID.randomUUID(); 49 | } 50 | 51 | /** 52 | * Builds the specified parts into a target request, setting header and body. 53 | * 54 | * @param requestBuilder the builder for which to set the header and body 55 | */ 56 | public void buildInto(Request.Builder requestBuilder) { 57 | var boundary = generateBoundary(); 58 | requestBuilder.addHeader("Content-Type", "multipart/form-data; boundary=" + boundary); 59 | var streams = parts.stream() 60 | .map(part -> part.inputStream(boundary)) 61 | .toList(); 62 | 63 | var body = new SequenceInputStream( 64 | new SequenceInputStream(Collections.enumeration(streams)), 65 | new ByteArrayInputStream(("--" + boundary + "--").getBytes(StandardCharsets.US_ASCII)) 66 | ); 67 | requestBuilder.setBody(body); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/main/java/io/github/zskamljic/restahead/modeling/validation/HeaderValidator.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.modeling.validation; 2 | 3 | import io.github.zskamljic.restahead.modeling.declaration.RequestParameterSpec; 4 | 5 | import javax.annotation.processing.Messager; 6 | import javax.lang.model.element.VariableElement; 7 | import javax.lang.model.util.Elements; 8 | import javax.lang.model.util.Types; 9 | import javax.tools.Diagnostic; 10 | import java.util.Optional; 11 | import java.util.function.Predicate; 12 | import java.util.regex.Pattern; 13 | 14 | /** 15 | * Validates the headers and creates corresponding {@link RequestParameterSpec} 16 | */ 17 | public class HeaderValidator extends CommonParameterValidator { 18 | 19 | // Valid HTTP header characters as per RFC2616, page 31 20 | private static final Predicate HEADER_REGEX = Pattern.compile("[!-'*+\\-./\\dA-Z^_`a-z|~]+") 21 | .asMatchPredicate() 22 | .negate(); 23 | 24 | /** 25 | * Create a new instance. 26 | * 27 | * @param messager the messager where errors will be reported to 28 | * @param elements the Elements object used to fetch type info 29 | * @param types an instance of the Types utility 30 | */ 31 | public HeaderValidator(Messager messager, Elements elements, Types types) { 32 | super(messager, elements, types); 33 | } 34 | 35 | /** 36 | * Get a header spec if the variable is a valid request parameter 37 | * 38 | * @param value the header name 39 | * @param parameter the parameter to generate the header from 40 | * @return Optional.empty in case of errors, HeaderSpec otherwise 41 | */ 42 | public Optional getHeaderSpec(String value, VariableElement parameter) { 43 | if (value.isEmpty()) { 44 | messager.printMessage(Diagnostic.Kind.ERROR, "Generating header names from parameter names not yet supported", parameter); 45 | return Optional.empty(); 46 | } 47 | 48 | if (HEADER_REGEX.test(value)) { 49 | messager.printMessage(Diagnostic.Kind.ERROR, "Header contains illegal characters", parameter); 50 | return Optional.empty(); 51 | } 52 | 53 | return extractSpec(parameter, value); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/parameters/FormOnClass$Impl.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.adapter.DefaultAdapters; 4 | import io.github.zskamljic.restahead.client.Client; 5 | import io.github.zskamljic.restahead.client.requests.Request; 6 | import io.github.zskamljic.restahead.client.requests.Verb; 7 | import io.github.zskamljic.restahead.exceptions.RestException; 8 | import java.io.ByteArrayInputStream; 9 | import java.io.InputStream; 10 | import java.lang.InterruptedException; 11 | import java.lang.Override; 12 | import java.lang.String; 13 | import java.net.URLEncoder; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.concurrent.ExecutionException; 16 | import javax.annotation.processing.Generated; 17 | 18 | @Generated("Generated by RestAhead") 19 | public final class FormOnClass$Impl implements FormOnClass { 20 | private final String baseUrl; 21 | 22 | private final Client client; 23 | 24 | private final DefaultAdapters defaultAdapters; 25 | 26 | public FormOnClass$Impl(String baseUrl, Client client, DefaultAdapters defaultAdapters) { 27 | this.baseUrl = baseUrl; 28 | this.client = client; 29 | this.defaultAdapters = defaultAdapters; 30 | } 31 | 32 | @Override 33 | public final void post(FormOnClass.Sample body) { 34 | var httpRequestBuilder = new Request.Builder() 35 | .setVerb(Verb.POST) 36 | .setBaseUrl(baseUrl) 37 | .setPath(""); 38 | httpRequestBuilder.addHeader("Content-Type", "application/x-www-form-urlencoded"); 39 | httpRequestBuilder.setBody(formEncode(body)); 40 | var response = client.execute(httpRequestBuilder.build()); 41 | try { 42 | defaultAdapters.syncVoidAdapter(response); 43 | } catch (ExecutionException | InterruptedException exception) { 44 | throw RestException.getAppropriateException(exception); 45 | } 46 | } 47 | 48 | public static InputStream formEncode(FormOnClass.Sample value) { 49 | var stringValue = "first=" + URLEncoder.encode(String.valueOf(value.getFirst()), StandardCharsets.UTF_8) + 50 | "&smth=" + URLEncoder.encode(String.valueOf(value.getSecond()), StandardCharsets.UTF_8); 51 | return new ByteArrayInputStream(stringValue.getBytes()); 52 | } 53 | } -------------------------------------------------------------------------------- /rest-ahead-jackson-converter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | RestAhead 7 | io.github.zskamljic 8 | 0.5.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | rest-ahead-jackson-converter 13 | jar 14 | ${project.groupId}:${project.artifactId} 15 | Jackson converter for Rest Ahead clients. 16 | 17 | 18 | 19 | io.github.zskamljic 20 | rest-ahead-client 21 | 0.5.0-SNAPSHOT 22 | 23 | 24 | 25 | com.fasterxml.jackson.core 26 | jackson-databind 27 | 2.13.2.2 28 | 29 | 30 | 31 | org.junit.jupiter 32 | junit-jupiter-api 33 | ${junit.version} 34 | test 35 | 36 | 37 | org.junit.jupiter 38 | junit-jupiter-engine 39 | ${junit.version} 40 | test 41 | 42 | 43 | org.mockito 44 | mockito-junit-jupiter 45 | ${mockito.version} 46 | test 47 | 48 | 49 | 50 | 51 | 52 | 53 | org.apache.maven.plugins 54 | maven-surefire-plugin 55 | ${surefire.version} 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/response/FutureGenericResponse$Impl.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.client.Client; 4 | import io.github.zskamljic.restahead.client.requests.Request; 5 | import io.github.zskamljic.restahead.client.requests.Verb; 6 | import io.github.zskamljic.restahead.conversion.Converter; 7 | import io.github.zskamljic.restahead.conversion.GenericReference; 8 | import io.github.zskamljic.restahead.exceptions.RequestFailedException; 9 | import java.io.IOException; 10 | import java.lang.Object; 11 | import java.lang.Override; 12 | import java.lang.String; 13 | import java.util.Map; 14 | import java.util.concurrent.CompletableFuture; 15 | import java.util.concurrent.CompletionException; 16 | import java.util.concurrent.Future; 17 | import javax.annotation.processing.Generated; 18 | 19 | @Generated("Generated by RestAhead") 20 | public final class FutureGenericResponse$Impl implements FutureGenericResponse { 21 | private final String baseUrl; 22 | 23 | private final Client client; 24 | 25 | private final Converter converter; 26 | 27 | public FutureGenericResponse$Impl(String baseUrl, Client client, Converter converter) { 28 | this.baseUrl = baseUrl; 29 | this.client = client; 30 | this.converter = converter; 31 | } 32 | 33 | @Override 34 | public final Future> delete() { 35 | var httpRequestBuilder = new Request.Builder() 36 | .setVerb(Verb.DELETE) 37 | .setBaseUrl(baseUrl) 38 | .setPath("/delete"); 39 | var response = client.execute(httpRequestBuilder.build()); 40 | CompletableFuture> convertedResponse = response.thenApply(r -> { 41 | if (r.status() < 200 || r.status() >= 300) { 42 | throw new RequestFailedException(r.status(), r.body()); 43 | } 44 | try { 45 | var conversionTypeHolder = new GenericReference>(){}; 46 | Map deserializedResponse = converter.deserialize(r, conversionTypeHolder.getType()); 47 | return deserializedResponse; 48 | } catch (IOException exception) { 49 | throw new CompletionException(exception); 50 | } 51 | } ); 52 | return convertedResponse; 53 | } 54 | } -------------------------------------------------------------------------------- /rest-ahead-jackson-converter/src/main/java/io/github/zskamljic/restahead/JacksonConverter.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.github.zskamljic.restahead.client.responses.Response; 5 | import io.github.zskamljic.restahead.conversion.Converter; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.PipedInputStream; 10 | import java.io.PipedOutputStream; 11 | import java.lang.reflect.Type; 12 | 13 | /** 14 | * Simple Jackson converter to be used with RestAhead. 15 | */ 16 | public class JacksonConverter implements Converter { 17 | private final ObjectMapper objectMapper; 18 | 19 | /** 20 | * Construct a new instance, using the default settings for ObjectMapper 21 | */ 22 | public JacksonConverter() { 23 | this(new ObjectMapper()); 24 | } 25 | 26 | /** 27 | * Use the provided object mapper for this converter 28 | * 29 | * @param objectMapper the mapper to use 30 | */ 31 | public JacksonConverter(ObjectMapper objectMapper) { 32 | this.objectMapper = objectMapper; 33 | } 34 | 35 | /** 36 | * Deserialize the type from given response. 37 | * 38 | * @param response the response to deserialize from 39 | * @param type the type to deserialize to 40 | * @param the target type 41 | * @return the deserialized object 42 | * @throws IOException if an error occurred while deserializing the object 43 | */ 44 | @Override 45 | public T deserialize(Response response, Type type) throws IOException { 46 | var javaType = objectMapper.getTypeFactory().constructType(type); 47 | var reader = objectMapper.readerFor(javaType); 48 | return reader.readValue(response.body()); 49 | } 50 | 51 | /** 52 | * Serializes the provided object to JSON. 53 | * 54 | * @param object the object to serialize 55 | * @return {@link InputStream} containing the serialized object 56 | * @throws IOException if an exception occurs while mapping to json 57 | */ 58 | @Override 59 | public InputStream serialize(Object object) throws IOException { 60 | var pipedInput = new PipedInputStream(); 61 | try (var output = new PipedOutputStream(pipedInput)) { 62 | objectMapper.writeValue(output, object); 63 | } 64 | return pipedInput; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/headers/HeadersService$Impl.java: -------------------------------------------------------------------------------- 1 | import io.github.zskamljic.restahead.adapter.DefaultAdapters; 2 | import io.github.zskamljic.restahead.client.Client; 3 | import io.github.zskamljic.restahead.client.requests.Request; 4 | import io.github.zskamljic.restahead.client.requests.Verb; 5 | import io.github.zskamljic.restahead.exceptions.RestException; 6 | import java.lang.InterruptedException; 7 | import java.lang.Override; 8 | import java.lang.String; 9 | import java.util.concurrent.ExecutionException; 10 | import javax.annotation.processing.Generated; 11 | 12 | @Generated("Generated by RestAhead") 13 | public final class HeadersService$Impl implements HeadersService { 14 | private final String baseUrl; 15 | 16 | private final Client client; 17 | 18 | private final DefaultAdapters defaultAdapters; 19 | 20 | public HeadersService$Impl(String baseUrl, Client client, DefaultAdapters defaultAdapters) { 21 | this.baseUrl = baseUrl; 22 | this.client = client; 23 | this.defaultAdapters = defaultAdapters; 24 | } 25 | 26 | @Override 27 | public final void performGet() { 28 | var httpRequestBuilder = new Request.Builder() 29 | .setVerb(Verb.GET) 30 | .setBaseUrl(baseUrl) 31 | .setPath(""); 32 | httpRequestBuilder.addHeader("Authorization", "none"); 33 | var response = client.execute(httpRequestBuilder.build()); 34 | try { 35 | defaultAdapters.syncVoidAdapter(response); 36 | } catch (ExecutionException | InterruptedException exception) { 37 | throw RestException.getAppropriateException(exception); 38 | } 39 | } 40 | 41 | @Override 42 | public final void performGetMultipleValues() { 43 | var httpRequestBuilder = new Request.Builder() 44 | .setVerb(Verb.GET) 45 | .setBaseUrl(baseUrl) 46 | .setPath(""); 47 | httpRequestBuilder.addHeader("Authorization", "none"); 48 | httpRequestBuilder.addHeader("Authorization", "some"); 49 | httpRequestBuilder.addHeader("Test", "value"); 50 | var response = client.execute(httpRequestBuilder.build()); 51 | try { 52 | defaultAdapters.syncVoidAdapter(response); 53 | } catch (ExecutionException | InterruptedException exception) { 54 | throw RestException.getAppropriateException(exception); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /rest-ahead-spring/src/main/java/io/github/zskamljic/restahead/spring/RestAheadService.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.spring; 2 | 3 | import io.github.zskamljic.restahead.client.Client; 4 | import io.github.zskamljic.restahead.conversion.Converter; 5 | import io.github.zskamljic.restahead.intercepting.Interceptor; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.ElementType; 9 | import java.lang.annotation.Inherited; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.lang.annotation.Target; 13 | 14 | /** 15 | * Annotation for declaring that annotated interface is a RestAhead compatible service. The service will be made into 16 | * an injectable bean. 17 | */ 18 | @Target(ElementType.TYPE) 19 | @Retention(RetentionPolicy.RUNTIME) 20 | @Documented 21 | @Inherited 22 | public @interface RestAheadService { 23 | /** 24 | * @return the base url of the service when it's instantiated 25 | */ 26 | String url(); 27 | 28 | /** 29 | * Converter will be automatically instantiated. The class specified needs to be a bean or has to have 30 | * a public no-args constructor. 31 | * 32 | * @return the Converter class to use with this service. Can be left empty if no converter is required. 33 | */ 34 | Class converter() default Converter.class; 35 | 36 | /** 37 | * Client will be automatically instantiated. The class specified needs to be a bean or has to have 38 | * a public no-args constructor. 39 | * 40 | * @return the Client to use with this service. Can be left empty for default client. 41 | */ 42 | Class client() default Client.class; 43 | 44 | /** 45 | * Interceptors will be automatically instantiated. The class specified needs to be a bean or has to have 46 | * a public no-args constructor. 47 | * This will only be honored if a client is provided. 48 | * 49 | * @return the Interceptor classes to use with the client. 50 | */ 51 | Class[] interceptors() default {}; 52 | 53 | /** 54 | * Adapters will be automatically instantiated. The class specified needs to be a bean or has to have 55 | * a public no-args constructor. 56 | * 57 | * @return the Adapter classes to use with this service. Can be left empty if no adapters are required. 58 | */ 59 | Class[] adapters() default {}; 60 | } 61 | -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/client/requests/parts/FilePart.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client.requests.parts; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.Optional; 11 | 12 | /** 13 | * Represents a multipart file 14 | */ 15 | public final class FilePart extends MultiPart { 16 | private final InputStream inputStream; 17 | private final String fileName; 18 | private final String contentType; 19 | 20 | public FilePart(String fieldName, String fileName, InputStream inputStream) { 21 | this(fieldName, fileName, null, inputStream); 22 | } 23 | 24 | public FilePart(String fieldName, String fileName, String contentType, InputStream inputStream) { 25 | super(fieldName); 26 | this.fileName = fileName; 27 | this.contentType = Optional.ofNullable(contentType).orElse("application/octet-stream"); 28 | this.inputStream = inputStream; 29 | } 30 | 31 | public FilePart(String fieldName, Path path) throws IOException { 32 | this(fieldName, path.getFileName().toString(), path); 33 | } 34 | 35 | public FilePart(String fieldName, String fileName, Path path) throws IOException { 36 | this(fieldName, fileName, Files.probeContentType(path), Files.newInputStream(path)); 37 | } 38 | 39 | public FilePart(String fieldName, File file) throws IOException { 40 | this(fieldName, file.getName(), file); 41 | } 42 | 43 | public FilePart(String fieldName, String fileName, File file) throws IOException { 44 | this(fieldName, fileName, Files.probeContentType(file.toPath()), new FileInputStream(file)); 45 | } 46 | 47 | public FilePart(String fieldName, String fileName, byte[] body) { 48 | this(fieldName, fileName, new ByteArrayInputStream(body)); 49 | } 50 | 51 | public FilePart(String fieldName, String fileName, String contentType, byte[] body) { 52 | this(fieldName, fileName, contentType, new ByteArrayInputStream(body)); 53 | } 54 | 55 | @Override 56 | public String contentDisposition() { 57 | return super.contentDisposition() + "; filename=\"" + fileName + "\"" + SEPARATOR + 58 | "Content-Type: " + contentType; 59 | } 60 | 61 | @Override 62 | public InputStream bodyContent() { 63 | return inputStream; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rest-ahead-processor/src/test/java/io/github/zskamljic/restahead/generation/methods/PathValidatorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.generation.methods; 2 | 3 | import io.github.zskamljic.restahead.client.requests.Verb; 4 | import io.github.zskamljic.restahead.modeling.declaration.ParameterDeclaration; 5 | import io.github.zskamljic.restahead.modeling.validation.PathValidator; 6 | import io.github.zskamljic.restahead.request.BasicRequestLine; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.CsvSource; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | 14 | import javax.annotation.processing.Messager; 15 | import javax.lang.model.element.ExecutableElement; 16 | import javax.lang.model.element.TypeElement; 17 | import javax.lang.model.type.TypeMirror; 18 | import javax.lang.model.util.Elements; 19 | import javax.lang.model.util.Types; 20 | import java.util.List; 21 | import java.util.Optional; 22 | 23 | import static org.junit.jupiter.api.Assertions.assertEquals; 24 | import static org.mockito.ArgumentMatchers.any; 25 | import static org.mockito.Mockito.doReturn; 26 | import static org.mockito.Mockito.mock; 27 | 28 | @ExtendWith(MockitoExtension.class) 29 | class PathValidatorTest { 30 | @Mock 31 | private Messager messager; 32 | 33 | @Mock 34 | private ExecutableElement function; 35 | 36 | @Mock 37 | private Elements elements; 38 | 39 | @Mock 40 | private Types types; 41 | 42 | private PathValidator pathValidator; 43 | 44 | @BeforeEach 45 | void setUp() { 46 | var typeMock = mock(TypeElement.class); 47 | doReturn(mock(TypeMirror.class)).when(typeMock).asType(); 48 | doReturn(typeMock).when(elements).getTypeElement(any()); 49 | pathValidator = new PathValidator(messager, elements, types); 50 | } 51 | 52 | @ParameterizedTest 53 | @CsvSource({ 54 | "false,", 55 | "false,''", 56 | "false,/abcdef1234-._~", 57 | "false,/%20%31", 58 | "false,/a+b()", 59 | "false,/%20a+b()abc", 60 | "false,/a+b()/%20%31/abc~", 61 | "true,/p ath;" 62 | }) 63 | void pathReturnsCorrectForString(boolean invalid, String path) { 64 | var result = pathValidator.extractRequestData( 65 | function, 66 | new BasicRequestLine(Verb.GET, path), 67 | new ParameterDeclaration(List.of(), List.of(), List.of(), Optional.empty()) 68 | ); 69 | 70 | assertEquals(invalid, result.isEmpty()); 71 | } 72 | } -------------------------------------------------------------------------------- /demo/src/test/java/io/github/zskamljic/restahead/demo/spring/ConfigCombinationsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo.spring; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.junit.jupiter.SpringExtension; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertFalse; 11 | import static org.junit.jupiter.api.Assertions.assertNotNull; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | @SpringBootTest 15 | @ExtendWith(SpringExtension.class) 16 | class ConfigCombinationsTest { 17 | @Autowired 18 | private ConfigCombinations.ClientOnlyService clientOnlyService; 19 | 20 | @Autowired 21 | private ConfigCombinations.ClientAndInterceptorService clientAndInterceptorService; 22 | 23 | @Autowired 24 | private ConfigCombinations.InterceptorService interceptorService; 25 | 26 | @Autowired 27 | private ConfigCombinations.AdapterService adapterService; 28 | 29 | @Autowired 30 | private ConfigCombinations.PlaceholderService placeholderService; 31 | 32 | @AfterEach 33 | void tearDown() { 34 | DummyClient.requests.clear(); 35 | } 36 | 37 | @Test 38 | void clientOnlyServiceHasClient() { 39 | clientOnlyService.get(); 40 | var requests = DummyClient.requests; 41 | 42 | assertFalse(requests.isEmpty()); 43 | var request = requests.get(0); 44 | assertTrue(((DummyClient) request.client()).getInterceptors().isEmpty()); 45 | } 46 | 47 | @Test 48 | void clientAndInterceptorService() { 49 | clientAndInterceptorService.get(); 50 | var requests = DummyClient.requests; 51 | 52 | assertFalse(requests.isEmpty()); 53 | var request = requests.get(0); 54 | assertTrue(((DummyClient) request.client()).getInterceptors().get(0) instanceof ConfigCombinations.PassThroughInterceptor); 55 | } 56 | 57 | @Test 58 | void interceptorService() { 59 | interceptorService.get(); 60 | var requests = DummyClient.requests; 61 | 62 | assertTrue(requests.isEmpty()); 63 | } 64 | 65 | @Test 66 | void adapterService() { 67 | adapterService.get().get(); 68 | var requests = DummyClient.requests; 69 | 70 | assertTrue(requests.isEmpty()); 71 | } 72 | 73 | @Test 74 | void placeholderService() { 75 | var response = placeholderService.get(); 76 | 77 | assertNotNull(response); 78 | } 79 | } -------------------------------------------------------------------------------- /rest-ahead-client/src/main/java/io/github/zskamljic/restahead/client/JavaHttpClient.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.client; 2 | 3 | import io.github.zskamljic.restahead.client.requests.Request; 4 | import io.github.zskamljic.restahead.client.responses.Response; 5 | 6 | import java.net.http.HttpClient; 7 | import java.net.http.HttpRequest; 8 | import java.net.http.HttpResponse; 9 | import java.util.concurrent.CompletableFuture; 10 | 11 | /** 12 | * Implementation of {@link Client} using {@link HttpClient} present in JDK. 13 | */ 14 | public class JavaHttpClient extends Client { 15 | private final HttpClient httpClient; 16 | 17 | /** 18 | * Construct an instance using default client. 19 | */ 20 | public JavaHttpClient() { 21 | httpClient = HttpClient.newHttpClient(); 22 | } 23 | 24 | /** 25 | * Use the specified client for requests. 26 | * 27 | * @param client the client to use 28 | */ 29 | public JavaHttpClient(HttpClient client) { 30 | httpClient = client; 31 | } 32 | 33 | @Override 34 | public CompletableFuture performRequest(Request request) { 35 | var requestBuilder = HttpRequest.newBuilder() 36 | .uri(request.uri()); 37 | switch (request.getVerb()) { 38 | case DELETE -> requestBuilder.DELETE(); 39 | case GET -> requestBuilder.GET(); 40 | case HEAD -> requestBuilder.method("HEAD", HttpRequest.BodyPublishers.noBody()); 41 | case OPTIONS -> requestBuilder.method("OPTIONS", HttpRequest.BodyPublishers.noBody()); 42 | case PATCH -> requestBuilder.method("PATCH", selectBodyPublisher(request)); 43 | case POST -> requestBuilder.POST(selectBodyPublisher(request)); 44 | case PUT -> requestBuilder.PUT(selectBodyPublisher(request)); 45 | } 46 | request.getHeaders().forEach((name, values) -> values.forEach(value -> requestBuilder.header(name, value))); 47 | var httpRequest = requestBuilder.build(); 48 | return httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofInputStream()) 49 | .thenApply(response -> new Response(response.statusCode(), response.headers().map(), response.body())); 50 | } 51 | 52 | /** 53 | * Creates a body publisher for the given request. 54 | * 55 | * @param request the request with body, from which to get the body 56 | * @return the appropriate body publisher 57 | */ 58 | private HttpRequest.BodyPublisher selectBodyPublisher(Request request) { 59 | return request.getBody() 60 | .map(input -> HttpRequest.BodyPublishers.ofInputStream(() -> input)) 61 | .orElse(HttpRequest.BodyPublishers.noBody()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test-report-aggregator/src/test/resources/response/ServiceWithUnknownResponse$Impl.java: -------------------------------------------------------------------------------- 1 | package io.github.zskamljic.restahead.demo; 2 | 3 | import io.github.zskamljic.restahead.adapter.DefaultAdapters; 4 | import io.github.zskamljic.restahead.client.Client; 5 | import io.github.zskamljic.restahead.client.requests.Request; 6 | import io.github.zskamljic.restahead.client.requests.Verb; 7 | import io.github.zskamljic.restahead.conversion.Converter; 8 | import io.github.zskamljic.restahead.exceptions.RequestFailedException; 9 | import io.github.zskamljic.restahead.exceptions.RestException; 10 | import java.io.IOException; 11 | import java.lang.InterruptedException; 12 | import java.lang.Override; 13 | import java.lang.String; 14 | import java.util.concurrent.CompletableFuture; 15 | import java.util.concurrent.CompletionException; 16 | import java.util.concurrent.ExecutionException; 17 | import javax.annotation.processing.Generated; 18 | 19 | @Generated("Generated by RestAhead") 20 | public final class ServiceWithUnknownResponse$Impl implements ServiceWithUnknownResponse { 21 | private final String baseUrl; 22 | 23 | private final Client client; 24 | 25 | private final Converter converter; 26 | 27 | private final DefaultAdapters defaultAdapters; 28 | 29 | public ServiceWithUnknownResponse$Impl(String baseUrl, Client client, Converter converter, 30 | DefaultAdapters defaultAdapters) { 31 | this.baseUrl = baseUrl; 32 | this.client = client; 33 | this.converter = converter; 34 | this.defaultAdapters = defaultAdapters; 35 | } 36 | 37 | @Override 38 | public final ServiceWithUnknownResponse.TestResponse delete() { 39 | var httpRequestBuilder = new Request.Builder() 40 | .setVerb(Verb.DELETE) 41 | .setBaseUrl(baseUrl) 42 | .setPath("/delete"); 43 | var response = client.execute(httpRequestBuilder.build()); 44 | CompletableFuture convertedResponse = response.thenApply(r -> { 45 | if (r.status() < 200 || r.status() >= 300) { 46 | throw new RequestFailedException(r.status(), r.body()); 47 | } 48 | try { 49 | ServiceWithUnknownResponse.TestResponse deserializedResponse = converter.deserialize(r, ServiceWithUnknownResponse.TestResponse.class); 50 | return deserializedResponse; 51 | } catch (IOException exception) { 52 | throw new CompletionException(exception); 53 | } 54 | } ); 55 | try { 56 | return defaultAdapters.syncAdapter(convertedResponse); 57 | } catch (ExecutionException | InterruptedException exception) { 58 | throw RestException.getAppropriateException(exception); 59 | } 60 | } 61 | } --------------------------------------------------------------------------------