├── .gitignore ├── service ├── src │ ├── main │ │ └── java │ │ │ └── ru │ │ │ └── lanwen │ │ │ └── heisenbug │ │ │ ├── beans │ │ │ ├── SchemaVersion.java │ │ │ ├── EticketMeta.java │ │ │ ├── Eticket.java │ │ │ ├── Airline.java │ │ │ ├── Region.java │ │ │ ├── Flight.java │ │ │ └── Airport.java │ │ │ └── TicketApi.java │ └── test │ │ ├── resources │ │ └── logback-test.xml │ │ └── java │ │ └── ru │ │ └── lanwen │ │ └── heisenbug │ │ └── EticketResourceTest.java └── pom.xml ├── lib ├── src │ └── main │ │ ├── java │ │ └── ru │ │ │ └── lanwen │ │ │ └── heisenbug │ │ │ ├── wiremock │ │ │ ├── WiremockCustomizer.java │ │ │ └── WiremockConfigFactory.java │ │ │ ├── WiremockAddressResolver.java │ │ │ ├── app │ │ │ └── TicketEndpoint.java │ │ │ └── WiremockResolver.java │ │ └── resources │ │ └── ticket.json └── pom.xml ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | #IDEA Files 2 | .idea 3 | target 4 | *.iml 5 | *.ipr 6 | *.exec 7 | 8 | #Eclipse files 9 | *.settings 10 | *.classpath 11 | *.project 12 | *.pydevproject 13 | BenchmarkList 14 | CompilerHints 15 | db-fetcher.log* 16 | yt.properties 17 | 18 | #logs 19 | *.log 20 | *.log.* 21 | -------------------------------------------------------------------------------- /service/src/main/java/ru/lanwen/heisenbug/beans/SchemaVersion.java: -------------------------------------------------------------------------------- 1 | 2 | package ru.lanwen.heisenbug.beans; 3 | 4 | public enum SchemaVersion { 5 | 6 | V_1("V1"); 7 | private final String value; 8 | 9 | SchemaVersion(String v) { 10 | value = v; 11 | } 12 | 13 | public String value() { 14 | return value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /service/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/src/main/java/ru/lanwen/heisenbug/wiremock/WiremockCustomizer.java: -------------------------------------------------------------------------------- 1 | package ru.lanwen.heisenbug.wiremock; 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer; 4 | import ru.lanwen.heisenbug.WiremockResolver; 5 | 6 | 7 | /** 8 | * Helps to create reusable customizer for injected wiremock server 9 | * 10 | * @author lanwen (Merkushev Kirill) 11 | * @see WiremockResolver.Server 12 | */ 13 | public interface WiremockCustomizer { 14 | 15 | void customize(final WireMockServer server); 16 | 17 | class NoopWiremockCustomizer implements WiremockCustomizer { 18 | @Override 19 | public void customize(final WireMockServer server) { 20 | // noop 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/main/java/ru/lanwen/heisenbug/wiremock/WiremockConfigFactory.java: -------------------------------------------------------------------------------- 1 | package ru.lanwen.heisenbug.wiremock; 2 | 3 | import com.github.tomakehurst.wiremock.common.Slf4jNotifier; 4 | import com.github.tomakehurst.wiremock.core.WireMockConfiguration; 5 | import ru.lanwen.heisenbug.WiremockResolver; 6 | 7 | /** 8 | * You can create custom config to init wiremock server in test. 9 | * 10 | * @author lanwen (Merkushev Kirill) 11 | * @see WiremockResolver.Server 12 | */ 13 | public interface WiremockConfigFactory { 14 | 15 | /** 16 | * Create config to be used by injected to test method wiremock 17 | * 18 | * @return config for wiremock 19 | */ 20 | WireMockConfiguration create(); 21 | 22 | /** 23 | * By default creates config with dynamic port only and notifier. 24 | */ 25 | class DefaultWiremockConfigFactory implements WiremockConfigFactory { 26 | 27 | @Override 28 | public WireMockConfiguration create() { 29 | return WireMockConfiguration.options().dynamicPort().notifier(new Slf4jNotifier(true)); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /service/src/main/java/ru/lanwen/heisenbug/beans/EticketMeta.java: -------------------------------------------------------------------------------- 1 | 2 | package ru.lanwen.heisenbug.beans; 3 | 4 | import java.io.Serializable; 5 | 6 | public class EticketMeta implements Serializable { 7 | 8 | private final static long serialVersionUID = 271283517L; 9 | protected SchemaVersion schemaVersion; 10 | 11 | public SchemaVersion getSchemaVersion() { 12 | return schemaVersion; 13 | } 14 | 15 | public void setSchemaVersion(SchemaVersion value) { 16 | this.schemaVersion = value; 17 | } 18 | 19 | @Override 20 | public boolean equals(Object o) { 21 | if (this == o) return true; 22 | if (o == null || getClass() != o.getClass()) return false; 23 | 24 | EticketMeta that = (EticketMeta) o; 25 | 26 | return schemaVersion == that.schemaVersion; 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return schemaVersion != null ? schemaVersion.hashCode() : 0; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "EticketMeta{" + 37 | "schemaVersion=" + schemaVersion + 38 | '}'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/main/resources/ticket.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "schema_version": "V1" 4 | }, 5 | "flights": [ 6 | { 7 | "number": "S7 232", 8 | "airline": { 9 | "name": "S7 Airlines", 10 | "iata": "S7", 11 | "icao": null 12 | }, 13 | "departure": { 14 | "scheduled": "2017-04-29T10:25:00+03:00", 15 | "iata": "VOZ", 16 | "name": "Чертовицкое", 17 | "city": { 18 | "name": "Воронеж", 19 | "latitude": 51.661535, 20 | "longitude": 39.200287 21 | }, 22 | "country": { 23 | "name": "Россия", 24 | "latitude": 61.698653, 25 | "longitude": 99.505405 26 | }, 27 | "tz": "Europe/Moscow" 28 | }, 29 | "arrival": { 30 | "scheduled": "2017-04-29T11:40:00+03:00", 31 | "iata": "DME", 32 | "name": "Домодедово", 33 | "city": { 34 | "name": "Москва", 35 | "latitude": 55.75396, 36 | "longitude": 37.620393 37 | }, 38 | "country": { 39 | "name": "Россия", 40 | "latitude": 61.698653, 41 | "longitude": 99.505405 42 | }, 43 | "tz": null 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/main/java/ru/lanwen/heisenbug/WiremockAddressResolver.java: -------------------------------------------------------------------------------- 1 | package ru.lanwen.heisenbug; 2 | 3 | import org.junit.jupiter.api.extension.ExtensionContext; 4 | import org.junit.jupiter.api.extension.ParameterContext; 5 | import org.junit.jupiter.api.extension.ParameterResolver; 6 | 7 | import java.lang.annotation.ElementType; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.lang.annotation.Target; 11 | 12 | import static ru.lanwen.heisenbug.WiremockResolver.WIREMOCK_PORT; 13 | 14 | /** 15 | * @author lanwen (Merkushev Kirill) 16 | */ 17 | public class WiremockAddressResolver implements ParameterResolver { 18 | @Override 19 | public boolean supports(ParameterContext parameterContext, ExtensionContext extensionContext) { 20 | return parameterContext.getParameter().isAnnotationPresent(Uri.class) 21 | && String.class.isAssignableFrom(parameterContext.getParameter().getType()); 22 | } 23 | 24 | @Override 25 | public Object resolve(ParameterContext parameterContext, ExtensionContext context) { 26 | ExtensionContext.Store store = context.getStore(ExtensionContext.Namespace.create(WiremockResolver.class)); 27 | 28 | return "http://localhost:" + store.get(WIREMOCK_PORT); 29 | } 30 | 31 | /** 32 | * To target host:port injection 33 | */ 34 | @Target({ElementType.PARAMETER}) 35 | @Retention(RetentionPolicy.RUNTIME) 36 | public @interface Uri { 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /service/src/main/java/ru/lanwen/heisenbug/beans/Eticket.java: -------------------------------------------------------------------------------- 1 | 2 | package ru.lanwen.heisenbug.beans; 3 | 4 | import java.io.Serializable; 5 | import java.util.List; 6 | 7 | public class Eticket implements Serializable { 8 | 9 | private final static long serialVersionUID = 271283517L; 10 | protected EticketMeta meta; 11 | protected List flights; 12 | 13 | public EticketMeta getMeta() { 14 | return meta; 15 | } 16 | 17 | public void setMeta(EticketMeta value) { 18 | this.meta = value; 19 | } 20 | 21 | public List getFlights() { 22 | return this.flights; 23 | } 24 | 25 | public void setFlights(List flights) { 26 | this.flights = flights; 27 | } 28 | 29 | @Override 30 | public boolean equals(Object o) { 31 | if (this == o) return true; 32 | if (o == null || getClass() != o.getClass()) return false; 33 | 34 | Eticket eticket = (Eticket) o; 35 | 36 | if (meta != null ? !meta.equals(eticket.meta) : eticket.meta != null) return false; 37 | return flights != null ? flights.equals(eticket.flights) : eticket.flights == null; 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | int result = meta != null ? meta.hashCode() : 0; 43 | result = 31 * result + (flights != null ? flights.hashCode() : 0); 44 | return result; 45 | } 46 | 47 | 48 | @Override 49 | public String toString() { 50 | return "Eticket{" + 51 | "meta=" + meta + 52 | ", flights=" + flights + 53 | '}'; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /service/src/main/java/ru/lanwen/heisenbug/TicketApi.java: -------------------------------------------------------------------------------- 1 | package ru.lanwen.heisenbug; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; 5 | import feign.Feign; 6 | import feign.Logger; 7 | import feign.Param; 8 | import feign.RequestLine; 9 | import feign.Response; 10 | import feign.jackson.JacksonDecoder; 11 | import feign.jackson.JacksonEncoder; 12 | import feign.slf4j.Slf4jLogger; 13 | import ru.lanwen.heisenbug.beans.Eticket; 14 | 15 | import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; 16 | 17 | /** 18 | * Blackbox API 19 | * 20 | * @author lanwen (Merkushev Kirill) 21 | */ 22 | public interface TicketApi { 23 | 24 | @RequestLine("GET /ticket/{id}") 25 | Eticket get(@Param("id") String uid); // can't return both status and eticket 26 | 27 | @RequestLine("POST /ticket") 28 | Response create(Eticket uid); 29 | 30 | /** 31 | * Constructs ready-to use client 32 | * 33 | * @param uri base uri 34 | * @return instance of api class 35 | */ 36 | static TicketApi connect(String uri) { 37 | ObjectMapper mapper = new ObjectMapper() 38 | .registerModule(new JaxbAnnotationModule()) 39 | .disable(FAIL_ON_UNKNOWN_PROPERTIES); 40 | 41 | return Feign.builder() 42 | .decoder(new JacksonDecoder(mapper)) 43 | .encoder(new JacksonEncoder(mapper)) 44 | .logger(new Slf4jLogger(TicketApi.class)) 45 | .logLevel(Logger.Level.FULL) 46 | .target(TicketApi.class, uri); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | heisenbug 7 | ru.lanwen 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | service 13 | 14 | 15 | 16 | ch.qos.logback 17 | logback-classic 18 | test 19 | 20 | 21 | 22 | com.fasterxml.jackson.module 23 | jackson-module-jaxb-annotations 24 | 25 | 26 | 27 | io.github.openfeign 28 | feign-core 29 | 30 | 31 | 32 | io.github.openfeign 33 | feign-jackson 34 | 35 | 36 | 37 | io.github.openfeign 38 | feign-slf4j 39 | 40 | 41 | 42 | ru.lanwen 43 | lib 44 | test 45 | 46 | 47 | 48 | org.junit.jupiter 49 | junit-jupiter-engine 50 | test 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /service/src/main/java/ru/lanwen/heisenbug/beans/Airline.java: -------------------------------------------------------------------------------- 1 | 2 | package ru.lanwen.heisenbug.beans; 3 | 4 | import java.io.Serializable; 5 | 6 | public class Airline implements Serializable { 7 | 8 | private final static long serialVersionUID = 271283517L; 9 | protected String name; 10 | protected String iata; 11 | protected String icao; 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | public void setName(String value) { 18 | this.name = value; 19 | } 20 | 21 | public String getIata() { 22 | return iata; 23 | } 24 | 25 | public void setIata(String value) { 26 | this.iata = value; 27 | } 28 | 29 | public String getIcao() { 30 | return icao; 31 | } 32 | 33 | public void setIcao(String value) { 34 | this.icao = value; 35 | } 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) return true; 40 | if (o == null || getClass() != o.getClass()) return false; 41 | 42 | Airline airline = (Airline) o; 43 | 44 | if (name != null ? !name.equals(airline.name) : airline.name != null) return false; 45 | if (iata != null ? !iata.equals(airline.iata) : airline.iata != null) return false; 46 | return icao != null ? icao.equals(airline.icao) : airline.icao == null; 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | int result = name != null ? name.hashCode() : 0; 52 | result = 31 * result + (iata != null ? iata.hashCode() : 0); 53 | result = 31 * result + (icao != null ? icao.hashCode() : 0); 54 | return result; 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return "Airline{" + 60 | "name='" + name + '\'' + 61 | ", iata='" + iata + '\'' + 62 | ", icao='" + icao + '\'' + 63 | '}'; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | heisenbug 7 | ru.lanwen 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | lib 13 | 14 | 15 | 16 | org.projectlombok 17 | lombok 18 | provided 19 | 20 | 21 | 22 | com.github.tomakehurst 23 | wiremock-standalone 24 | compile 25 | 26 | 27 | 28 | org.hamcrest 29 | hamcrest-all 30 | compile 31 | 32 | 33 | 34 | org.junit.jupiter 35 | junit-jupiter-engine 36 | compile 37 | 38 | 39 | 40 | org.junit.vintage 41 | junit-vintage-engine 42 | compile 43 | 44 | 45 | 46 | org.junit.jupiter 47 | junit-jupiter-api 48 | compile 49 | 50 | 51 | 52 | commons-io 53 | commons-io 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /service/src/main/java/ru/lanwen/heisenbug/beans/Region.java: -------------------------------------------------------------------------------- 1 | 2 | package ru.lanwen.heisenbug.beans; 3 | 4 | import java.io.Serializable; 5 | 6 | public class Region implements Serializable { 7 | 8 | private final static long serialVersionUID = 271283517L; 9 | protected String name; 10 | protected double latitude; 11 | protected double longitude; 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | public void setName(String value) { 18 | this.name = value; 19 | } 20 | 21 | public double getLatitude() { 22 | return latitude; 23 | } 24 | 25 | public void setLatitude(double value) { 26 | this.latitude = value; 27 | } 28 | 29 | public double getLongitude() { 30 | return longitude; 31 | } 32 | 33 | public void setLongitude(double value) { 34 | this.longitude = value; 35 | } 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) return true; 40 | if (o == null || getClass() != o.getClass()) return false; 41 | 42 | Region region = (Region) o; 43 | 44 | if (Double.compare(region.latitude, latitude) != 0) return false; 45 | if (Double.compare(region.longitude, longitude) != 0) return false; 46 | return name != null ? name.equals(region.name) : region.name == null; 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | int result; 52 | long temp; 53 | result = name != null ? name.hashCode() : 0; 54 | temp = Double.doubleToLongBits(latitude); 55 | result = 31 * result + (int) (temp ^ (temp >>> 32)); 56 | temp = Double.doubleToLongBits(longitude); 57 | result = 31 * result + (int) (temp ^ (temp >>> 32)); 58 | return result; 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return "Region{" + 64 | "name='" + name + '\'' + 65 | ", latitude=" + latitude + 66 | ", longitude=" + longitude + 67 | '}'; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/main/java/ru/lanwen/heisenbug/app/TicketEndpoint.java: -------------------------------------------------------------------------------- 1 | package ru.lanwen.heisenbug.app; 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer; 4 | import org.apache.commons.io.IOUtils; 5 | import ru.lanwen.heisenbug.wiremock.WiremockCustomizer; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.UUID; 11 | 12 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 13 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 14 | import static com.github.tomakehurst.wiremock.client.WireMock.post; 15 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; 16 | 17 | /** 18 | * @author lanwen (Merkushev Kirill) 19 | */ 20 | public class TicketEndpoint implements WiremockCustomizer { 21 | 22 | public static final String X_TICKET_ID_HEADER = "X-Ticket-ID"; 23 | 24 | @Override 25 | public void customize(WireMockServer server) { 26 | String uuid = UUID.randomUUID().toString(); 27 | 28 | server.stubFor( 29 | post(urlPathEqualTo("/ticket")) 30 | .willReturn(aResponse() 31 | .withStatus(201) 32 | .withHeader( 33 | "Location", 34 | String.format("http://localhost:%s/ticket/%s", server.port(), uuid) 35 | ) 36 | .withHeader(X_TICKET_ID_HEADER, uuid)) 37 | ); 38 | 39 | server.stubFor( 40 | get(urlPathEqualTo("/ticket/" + uuid)) 41 | .willReturn(aResponse() 42 | .withStatus(200) 43 | .withHeader(X_TICKET_ID_HEADER, uuid) 44 | .withBody(cp("ticket.json")) 45 | ) 46 | ); 47 | } 48 | 49 | private static String cp(String path) { 50 | try (InputStream is = TicketEndpoint.class.getClassLoader().getResourceAsStream(path)) { 51 | return IOUtils.toString(is, StandardCharsets.UTF_8); 52 | } catch (IOException e) { 53 | throw new RuntimeException(e); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /service/src/main/java/ru/lanwen/heisenbug/beans/Flight.java: -------------------------------------------------------------------------------- 1 | 2 | package ru.lanwen.heisenbug.beans; 3 | 4 | import java.io.Serializable; 5 | 6 | public class Flight implements Serializable { 7 | 8 | private final static long serialVersionUID = 271283517L; 9 | protected String number; 10 | protected String status; 11 | protected Airline airline; 12 | protected Airport departure; 13 | protected Airport arrival; 14 | 15 | public String getNumber() { 16 | return number; 17 | } 18 | 19 | public void setNumber(String value) { 20 | this.number = value; 21 | } 22 | 23 | public String getStatus() { 24 | return status; 25 | } 26 | 27 | public void setStatus(String value) { 28 | this.status = value; 29 | } 30 | 31 | public Airline getAirline() { 32 | return airline; 33 | } 34 | 35 | public void setAirline(Airline value) { 36 | this.airline = value; 37 | } 38 | 39 | public Airport getDeparture() { 40 | return departure; 41 | } 42 | 43 | public void setDeparture(Airport value) { 44 | this.departure = value; 45 | } 46 | 47 | public Airport getArrival() { 48 | return arrival; 49 | } 50 | 51 | public void setArrival(Airport value) { 52 | this.arrival = value; 53 | } 54 | 55 | @Override 56 | public boolean equals(Object o) { 57 | if (this == o) return true; 58 | if (o == null || getClass() != o.getClass()) return false; 59 | 60 | Flight flight = (Flight) o; 61 | 62 | if (number != null ? !number.equals(flight.number) : flight.number != null) return false; 63 | if (status != null ? !status.equals(flight.status) : flight.status != null) return false; 64 | if (airline != null ? !airline.equals(flight.airline) : flight.airline != null) return false; 65 | if (departure != null ? !departure.equals(flight.departure) : flight.departure != null) return false; 66 | return arrival != null ? arrival.equals(flight.arrival) : flight.arrival == null; 67 | } 68 | 69 | @Override 70 | public int hashCode() { 71 | int result = number != null ? number.hashCode() : 0; 72 | result = 31 * result + (status != null ? status.hashCode() : 0); 73 | result = 31 * result + (airline != null ? airline.hashCode() : 0); 74 | result = 31 * result + (departure != null ? departure.hashCode() : 0); 75 | result = 31 * result + (arrival != null ? arrival.hashCode() : 0); 76 | return result; 77 | } 78 | 79 | @Override 80 | public String toString() { 81 | return "Flight{" + 82 | "number='" + number + '\'' + 83 | ", status='" + status + '\'' + 84 | ", airline=" + airline + 85 | ", departure=" + departure + 86 | ", arrival=" + arrival + 87 | '}'; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /service/src/main/java/ru/lanwen/heisenbug/beans/Airport.java: -------------------------------------------------------------------------------- 1 | 2 | package ru.lanwen.heisenbug.beans; 3 | 4 | import java.io.Serializable; 5 | import java.util.Date; 6 | 7 | public class Airport implements Serializable { 8 | 9 | private final static long serialVersionUID = 271283517L; 10 | 11 | protected String scheduled; 12 | protected String iata; 13 | protected String name; 14 | protected Region city; 15 | protected Region country; 16 | protected String tz; 17 | 18 | public String getScheduled() { 19 | return scheduled; 20 | } 21 | 22 | public void setScheduled(String value) { 23 | this.scheduled = value; 24 | } 25 | 26 | public String getIata() { 27 | return iata; 28 | } 29 | 30 | public void setIata(String value) { 31 | this.iata = value; 32 | } 33 | 34 | public String getName() { 35 | return name; 36 | } 37 | 38 | public void setName(String value) { 39 | this.name = value; 40 | } 41 | public Region getCity() { 42 | return city; 43 | } 44 | 45 | public void setCity(Region value) { 46 | this.city = value; 47 | } 48 | 49 | public Region getCountry() { 50 | return country; 51 | } 52 | 53 | public void setCountry(Region value) { 54 | this.country = value; 55 | } 56 | 57 | public String getTz() { 58 | return tz; 59 | } 60 | 61 | public void setTz(String value) { 62 | this.tz = value; 63 | } 64 | 65 | @Override 66 | public boolean equals(Object o) { 67 | if (this == o) return true; 68 | if (o == null || getClass() != o.getClass()) return false; 69 | 70 | Airport airport = (Airport) o; 71 | 72 | if (scheduled != null ? !scheduled.equals(airport.scheduled) : airport.scheduled != null) return false; 73 | if (iata != null ? !iata.equals(airport.iata) : airport.iata != null) return false; 74 | if (name != null ? !name.equals(airport.name) : airport.name != null) return false; 75 | if (city != null ? !city.equals(airport.city) : airport.city != null) return false; 76 | if (country != null ? !country.equals(airport.country) : airport.country != null) return false; 77 | return tz != null ? tz.equals(airport.tz) : airport.tz == null; 78 | } 79 | 80 | @Override 81 | public int hashCode() { 82 | int result = scheduled != null ? scheduled.hashCode() : 0; 83 | result = 31 * result + (iata != null ? iata.hashCode() : 0); 84 | result = 31 * result + (name != null ? name.hashCode() : 0); 85 | result = 31 * result + (city != null ? city.hashCode() : 0); 86 | result = 31 * result + (country != null ? country.hashCode() : 0); 87 | result = 31 * result + (tz != null ? tz.hashCode() : 0); 88 | return result; 89 | } 90 | 91 | @Override 92 | public String toString() { 93 | return "Airport{" + 94 | "scheduled=" + scheduled + 95 | ", iata='" + iata + '\'' + 96 | ", name='" + name + '\'' + 97 | ", city=" + city + 98 | ", country=" + country + 99 | ", tz='" + tz + '\'' + 100 | '}'; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/main/java/ru/lanwen/heisenbug/WiremockResolver.java: -------------------------------------------------------------------------------- 1 | package ru.lanwen.heisenbug; 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.junit.jupiter.api.extension.AfterEachCallback; 6 | import org.junit.jupiter.api.extension.ExtensionContext; 7 | import org.junit.jupiter.api.extension.ExtensionContext.Namespace; 8 | import org.junit.jupiter.api.extension.ParameterContext; 9 | import org.junit.jupiter.api.extension.ParameterResolutionException; 10 | import org.junit.jupiter.api.extension.ParameterResolver; 11 | import org.junit.jupiter.api.extension.TestExtensionContext; 12 | import ru.lanwen.heisenbug.wiremock.WiremockConfigFactory; 13 | import ru.lanwen.heisenbug.wiremock.WiremockCustomizer; 14 | 15 | import java.lang.annotation.ElementType; 16 | import java.lang.annotation.Retention; 17 | import java.lang.annotation.RetentionPolicy; 18 | import java.lang.annotation.Target; 19 | import java.util.Optional; 20 | 21 | import static java.lang.String.format; 22 | 23 | /** 24 | * @author lanwen (Merkushev Kirill) 25 | */ 26 | @Slf4j 27 | public class WiremockResolver implements ParameterResolver, AfterEachCallback { 28 | public static final String WIREMOCK_PORT = "wiremock.port"; 29 | 30 | private WireMockServer server; 31 | 32 | @Override 33 | public void afterEach(TestExtensionContext testExtensionContext) throws Exception { 34 | if (server == null || !server.isRunning()) { 35 | return; 36 | } 37 | 38 | server.resetRequests(); 39 | server.resetToDefaultMappings(); 40 | log.info("Stopping wiremock server on localhost:{}", server.port()); 41 | server.stop(); 42 | } 43 | 44 | @Override 45 | public boolean supports(ParameterContext parameterContext, ExtensionContext context) { 46 | return parameterContext.getParameter().isAnnotationPresent(Server.class); 47 | } 48 | 49 | @Override 50 | public Object resolve(ParameterContext parameterContext, ExtensionContext context) { 51 | if (Optional.ofNullable(server).map(WireMockServer::isRunning).orElse(false)) { 52 | throw new IllegalStateException("Can't inject more than one server"); 53 | } 54 | 55 | Server mockedServer = parameterContext.getParameter().getAnnotation(Server.class); 56 | 57 | 58 | try { 59 | server = new WireMockServer( 60 | mockedServer.factory().newInstance().create() 61 | ); 62 | } catch (ReflectiveOperationException e) { 63 | throw new ParameterResolutionException( 64 | format("Can't create config with given factory %s", mockedServer.factory()), 65 | e 66 | ); 67 | } 68 | 69 | server.start(); 70 | 71 | try { 72 | mockedServer.customizer().newInstance().customize(server); 73 | } catch (ReflectiveOperationException e) { 74 | throw new ParameterResolutionException( 75 | format("Can't customize server with given customizer %s", mockedServer.customizer()), 76 | e 77 | ); 78 | } 79 | 80 | ExtensionContext.Store store = context.getStore(Namespace.create(WiremockResolver.class)); 81 | store.put(WIREMOCK_PORT, server.port()); 82 | 83 | log.info("Started wiremock server on localhost:{}", server.port()); 84 | return server; 85 | } 86 | 87 | /** 88 | * Enables injection of wiremock server to test. 89 | * Helps to configure instance with {@link #factory} and {@link #customizer} methods 90 | */ 91 | @Target({ElementType.PARAMETER}) 92 | @Retention(RetentionPolicy.RUNTIME) 93 | public @interface Server { 94 | /** 95 | * @return class which defines on how to create config 96 | */ 97 | Class factory() default WiremockConfigFactory.DefaultWiremockConfigFactory.class; 98 | 99 | /** 100 | * @return class which defines on how to customize server after start 101 | */ 102 | Class customizer() default WiremockCustomizer.NoopWiremockCustomizer.class; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /service/src/test/java/ru/lanwen/heisenbug/EticketResourceTest.java: -------------------------------------------------------------------------------- 1 | package ru.lanwen.heisenbug; 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer; 4 | import feign.Response; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import ru.lanwen.heisenbug.WiremockAddressResolver.Uri; 8 | import ru.lanwen.heisenbug.WiremockResolver.Server; 9 | import ru.lanwen.heisenbug.app.TicketEndpoint; 10 | import ru.lanwen.heisenbug.beans.Airline; 11 | import ru.lanwen.heisenbug.beans.Airport; 12 | import ru.lanwen.heisenbug.beans.Eticket; 13 | import ru.lanwen.heisenbug.beans.EticketMeta; 14 | import ru.lanwen.heisenbug.beans.Flight; 15 | import ru.lanwen.heisenbug.beans.Region; 16 | 17 | import java.util.Collection; 18 | import java.util.Collections; 19 | 20 | import static org.hamcrest.Matchers.hasSize; 21 | import static org.hamcrest.Matchers.is; 22 | import static org.hamcrest.Matchers.notNullValue; 23 | import static org.hamcrest.Matchers.samePropertyValuesAs; 24 | import static org.junit.Assert.assertThat; 25 | import static org.junit.jupiter.api.Assertions.assertThrows; 26 | import static ru.lanwen.heisenbug.app.TicketEndpoint.X_TICKET_ID_HEADER; 27 | 28 | /** 29 | * @author lanwen (Merkushev Kirill) 30 | */ 31 | @ExtendWith({ 32 | WiremockResolver.class, 33 | WiremockAddressResolver.class 34 | }) 35 | public class EticketResourceTest { 36 | 37 | @Test 38 | void shouldCreateTicket(@Server(customizer = TicketEndpoint.class) WireMockServer server, @Uri String uri) { 39 | TicketApi api = TicketApi.connect(uri); 40 | 41 | Response response = api.create(new Eticket()); 42 | Collection ids = response.headers().get(X_TICKET_ID_HEADER); 43 | 44 | assertThat("ids", ids, hasSize(1)); 45 | 46 | Eticket ticket = api.get(ids.iterator().next()); 47 | 48 | assertThat(ticket, is(notNullValue())); 49 | } 50 | 51 | /** 52 | * Что бывает при использовании не тестовых клиентов 53 | */ 54 | @Test 55 | void shouldPassTicketAssertion(@Server(customizer = TicketEndpoint.class) WireMockServer server, @Uri String uri) { 56 | Eticket ticket = TicketApi.connect(uri).get("unknown"); 57 | 58 | // Ticket должен быть null! 59 | assertThrows( 60 | AssertionError.class, 61 | () -> assertThat(ticket, is(notNullValue())) 62 | ); 63 | } 64 | 65 | @Test 66 | void shouldSaveTicketProps(@Server(customizer = TicketEndpoint.class) WireMockServer server, @Uri String uri) { 67 | Eticket original = new Eticket(); 68 | original.setMeta(new EticketMeta()); 69 | Flight flight = new Flight(); 70 | Airline airline = new Airline(); 71 | airline.setIata("S7"); 72 | airline.setName("S7 Airlines"); 73 | flight.setAirline(airline); 74 | Airport dep = new Airport(); 75 | dep.setScheduled("2017-04-29T10:25:00+03:00"); 76 | dep.setIata("VOZ"); 77 | dep.setName("Чертовицкое"); 78 | Region depCity = new Region(); 79 | depCity.setName("Воронеж"); 80 | depCity.setLatitude(51.661535); 81 | depCity.setLongitude(39.200287); 82 | dep.setCity(depCity); 83 | Region countryDep = new Region(); 84 | countryDep.setName("Россия"); 85 | countryDep.setLongitude(99.505405); 86 | countryDep.setLatitude(61.698653); 87 | dep.setCountry(countryDep); 88 | dep.setTz("Europe/Moscow"); 89 | flight.setDeparture(dep); 90 | Airport arr = new Airport(); 91 | arr.setName("Домодедово"); 92 | arr.setIata("DME"); 93 | arr.setScheduled("2017-04-29T11:40:00+03:00"); 94 | Region cityArr = new Region(); 95 | cityArr.setName("Москва"); 96 | cityArr.setLatitude(55.75396); 97 | cityArr.setLongitude(37.620393); 98 | arr.setCity(cityArr); 99 | Region countryArr = new Region(); 100 | countryArr.setName("Россия"); 101 | countryArr.setLatitude(61.698653); 102 | countryArr.setLongitude(99.505405); 103 | arr.setCountry(countryArr); 104 | flight.setArrival(arr); 105 | flight.setNumber("S7 232"); 106 | original.setFlights(Collections.singletonList(flight)); 107 | 108 | TicketApi api = TicketApi.connect(uri); 109 | 110 | String id = api.create(original).headers().get(X_TICKET_ID_HEADER).iterator().next(); 111 | Eticket ticket = api.get(id); 112 | 113 | assertThat(ticket, samePropertyValuesAs(original)); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | ru.lanwen 8 | heisenbug 9 | 1.0-SNAPSHOT 10 | pom 11 | 12 | 13 | 14 | lib 15 | service 16 | 17 | 18 | 19 | 0.9.5 20 | 5.0.0-M4 21 | 4.12.0-M4 22 | 1.0.0-M4 23 | 3.0.1 24 | 1.16.14 25 | 1.1.11 26 | 1.7.24 27 | 2.8.7 28 | 9.4.0 29 | 30 | 31 | 32 | 33 | org.slf4j 34 | slf4j-api 35 | ${slf4j.version} 36 | 37 | 38 | org.junit.jupiter 39 | junit-jupiter-api 40 | test 41 | 42 | 43 | 44 | 45 | 46 | 47 | com.fasterxml.jackson 48 | jackson-bom 49 | ${jackson.version} 50 | import 51 | pom 52 | 53 | 54 | 55 | ru.lanwen 56 | lib 57 | ${project.version} 58 | 59 | 60 | 61 | org.projectlombok 62 | lombok 63 | ${lombok.version} 64 | 65 | 66 | 67 | ch.qos.logback 68 | logback-classic 69 | ${logback.version} 70 | 71 | 72 | 73 | commons-io 74 | commons-io 75 | 2.5 76 | 77 | 78 | 79 | org.junit.jupiter 80 | junit-jupiter-api 81 | ${junit.jupiter.version} 82 | 83 | 84 | 85 | org.junit.jupiter 86 | junit-jupiter-engine 87 | ${junit.jupiter.version} 88 | test 89 | 90 | 91 | 92 | org.junit.vintage 93 | junit-vintage-engine 94 | ${junit.vintage.version} 95 | test 96 | 97 | 98 | 99 | com.github.tomakehurst 100 | wiremock-standalone 101 | 2.5.1 102 | test 103 | 104 | 105 | 106 | ru.yandex.qatools.processors 107 | feature-matcher-generator 108 | 2.0.0 109 | test 110 | 111 | 112 | 113 | org.hamcrest 114 | hamcrest-all 115 | 1.3 116 | test 117 | 118 | 119 | 120 | io.rest-assured 121 | rest-assured 122 | ${rest-assured.version} 123 | test 124 | 125 | 126 | 127 | 128 | io.github.openfeign 129 | feign-core 130 | ${feign.version} 131 | 132 | 133 | 134 | io.github.openfeign 135 | feign-jackson 136 | ${feign.version} 137 | 138 | 139 | 140 | io.github.openfeign 141 | feign-slf4j 142 | ${feign.version} 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | maven-compiler-plugin 152 | 3.6.0 153 | 154 | 1.8 155 | 1.8 156 | 157 | 158 | 159 | 160 | 161 | maven-surefire-plugin 162 | 2.19 163 | 164 | 165 | org.junit.platform 166 | junit-platform-surefire-provider 167 | ${junit.platform.version} 168 | 169 | 170 | org.junit.jupiter 171 | junit-jupiter-engine 172 | ${junit.jupiter.version} 173 | 174 | 175 | org.junit.vintage 176 | junit-vintage-engine 177 | ${junit.vintage.version} 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heisenbug 2017 2 | 3 | Codegen полезняшки. Набор инструментов, которые пробовал в процессе работы над разными проектами. Некоторые сильно дублируют друг-друга. 4 | 5 | 6 | ## JAXB & плагины 7 | 8 | - The most advanced JAXB2 Maven Plugin for XML Schema compilation. 9 | Именно об этом плагине речь в докладе. Очень мощные возможности для быстрого наведения порядка в дата-классах 10 | https://github.com/highsource/maven-jaxb2-plugin 11 | 12 | - Jaxb плагин для вставки доп методов 13 | https://github.com/mklemm/jaxb-delegate-plugin 14 | 15 | - XJC / JAXB plugin for generation of Bean Validation Annotations (JSR-303) and replacing primitive types 16 | Позволяет сразу генерировать простые аннотации для валидации полей. 17 | https://github.com/krasa/krasa-jaxb-tools 18 | 19 | - Add arbitrary annotations to JAXB classes. 20 | Позволяет развешивать аннотации в любом месте 21 | https://github.com/highsource/jaxb2-annotate-plugin 22 | 23 | - JAXB @XmlElementWrapper Plugin 24 | Упрощает сгенерированные классы 25 | https://github.com/dmak/jaxb-xew-plugin 26 | 27 | - Наследование от кастомного класса 28 | http://docs.oracle.com/cd/E17802_01/webservices/webservices/docs/2.0/jaxb/vendorCustomizations.html 29 | 30 | ## Annotation processors 31 | 32 | - Reducing Boilerplate Code with Project Lombok 33 | Очень сильно позволяет уменьшить количество пустого кода. Несколько магический. 34 | https://projectlombok.org 35 | 36 | - An annotation processor for generating type-safe bean mappers http://mapstruct.org/ 37 | Чтобы одни бины превращать в другие без рефлексии и ухищрений 38 | https://github.com/mapstruct/mapstruct 39 | 40 | - Annotation processor to create immutable objects and builders. 41 | https://github.com/immutables/immutables 42 | 43 | - Java Annotation Processor which allows to simplify development 44 | Местами дублирует возможности ломбока, но не переписывает результирующие классы 45 | https://github.com/vbauer/jackdaw 46 | 47 | - A Java Code Generator for Pojo Builders 48 | Генерирует билдеры для классов 49 | https://github.com/mkarneim/pojobuilder 50 | 51 | - Generates Hamcrest's Feature Matchers for every field annotated with selected annotation 52 | Генерирует матчеры для полей 53 | https://github.com/yandex-qatools/hamcrest-pojo-matcher-generator 54 | 55 | - A collection of source code generators for Java. 56 | Солянка разных 57 | https://github.com/google/auto 58 | 59 | ## Шаблонизаторы 60 | 61 | - Logic-less and semantic Mustache templates with Java 62 | Удобные, без логики, довольно куцые шаблоны. Очень легко воспринимаются. Отлично для старта. 63 | https://github.com/jknack/handlebars.java 64 | 65 | - Apache FreeMarker is a template engine: a Java library to generate text output 66 | Один из самых распространенных в java. Умеет очень много, довольно быстрый. 67 | Сильно полноценнее mustache, но и сложнее для освоения и восприятия. 68 | http://freemarker.org 69 | 70 | - StringTemplate is a java template engine (with ports for C#, Objective-C, JavaScript, Scala) for generating source code, web pages, emails, or any other formatted text output. 71 | Несколько маргинальная либа для шаблонизации, но много умеет и входит в состав ANTLR тулчейна (популярная либа для грамматик по обработке текста). Очень быстрая. 72 | http://www.stringtemplate.org 73 | 74 | - Velocity is a Java-based template engine. It permits anyone to use a simple yet powerful template language to reference objects defined in Java code. 75 | По возможностям близко к freemarker, но ооочень давно не обновляется и сильно устарела по api, удобству, скорости итд. Здесь чтобы просто быть. 76 | http://velocity.apache.org 77 | 78 | ## Для тестирования 79 | 80 | - **!** Testing tools for javac and annotation processors 81 | Самый удобный тул для тестирования кодогенераторов в вакууме 82 | https://github.com/google/compile-testing 83 | 84 | - Custom assertions generator 85 | По идее похож на генерацию матчеров. Только генерирует сразу ассерты. 86 | Не всегда удобно использовать с глубокой вложенностью 87 | https://github.com/joel-costigliola/assertj-assertions-generator 88 | 89 | - Rest-Assured RAML Codegen - Generates test http client, based on Rest-Assured with help of RAML spec 90 | Гибкий клиент для тестирования. rest-assured под капотом 91 | https://github.com/qameta/rarc 92 | 93 | 94 | ## Бины, протоколы 95 | 96 | - Protocol buffers are Google's language-neutral, platform-neutral, extensible 97 | mechanism for serializing structured data – think XML, but smaller, faster, and simpler. 98 | Не только бины, но и протокол сериализации. Не всегда прозрачно ложится на текущую инфраструктуру. 99 | Внедрить подход через JAXB или jsonschema сильно проще, принцип но такой же как у протобуфов. 100 | Нечеловекочитаемый результат сериализации. Очень экономно передает данные. 101 | Используется, например, большинством браузеров для синхронизации данных с облаком. 102 | https://developers.google.com/protocol-buffers/ 103 | 104 | - Maven Plugin that executes the Protocol Buffers (protoc) compiler 105 | Позволяет процессить схемы во время билда и не коммитить сотни тысяч строк сгенерированного кода. 106 | https://github.com/xolstice/protobuf-maven-plugin 107 | 108 | - jsonschema2pojo generates Java types from JSON Schema (or example JSON) and can annotate those types for data-binding with Jackson 1.x, Jackson 2.x or Gson. 109 | Похож на JAXB, только работает с json и json-схемой. Очень гибкий и простой для внедрения. Есть мавен, гредл и cli виды. 110 | https://github.com/joelittlejohn/jsonschema2pojo 111 | 112 | - swagger-codegen contains a template-driven engine to generate documentation, API clients and server stubs 113 | Генерирует клиента по сваггер схеме. Код получается уродливый. Кастомизируется тяжело. 114 | Зато очень много языков из коробки. Отличный пример как *не надо* работать с результатами кодогенерации. 115 | Ребята коммитят всё прямо рядом с исходниками кодогенератора. Из-за этого в репозитории каша, а каждый PR это месиво из тысяч строк. 116 | Возможно из-за этого и сам код кодогенератора - месиво. 117 | https://github.com/swagger-api/swagger-codegen 118 | 119 | - The Apache Thrift software framework, for scalable cross-language services development, 120 | combines a software stack with a code generation engine to build services that work efficiently and seamlessly between C++, 121 | Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml and Delphi and other languages. 122 | Как и протобуфы дает еще и протокол сериализации. Помимо этого еще и сервер и клиентов. 123 | http://thrift.apache.org 124 | 125 | - Maven plugin for generating Java client RESTful code based on RAML protocol. 126 | Несложные клиенты по raml схеме. Уступает swagger-генератору по поддержаным возможностям 127 | https://github.com/aureliano/cgraml-maven-plugin 128 | 129 | 130 | ## Генерация сорцов, байткода 131 | 132 | - A Java API for generating .java source files. 133 | Лучшая на мой взгляд библиотека для императивного способа генерации сорцов 134 | https://github.com/square/javapoet 135 | 136 | ### Ближе к байткоду 137 | 138 | - cglib - Byte Code Generation Library is high level API to generate and transform Java byte code. 139 | https://github.com/cglib/cglib 140 | 141 | - Runtime code generation for the Java virtual machine. 142 | https://github.com/raphw/byte-buddy 143 | 144 | - Maven plugin that will apply Javassist bytecode transformations during build time. 145 | https://github.com/icon-Systemhaus-GmbH/javassist-maven-plugin 146 | 147 | 148 | ## Общее 149 | 150 | - Native bindings generator for JNA / BridJ / Node.js 151 | Парсит заголовочные файлы для генерации биндингов. 152 | https://github.com/nativelibs4java/JNAerator 153 | 154 | - Distributed code search and refactoring for Java 155 | https://github.com/Netflix-Skunkworks/rewrite 156 | 157 | - Среди java-awesome подборки 158 | https://github.com/akullpp/awesome-java#code-generators 159 | 160 | 161 | ## Golang 162 | 163 | - GO codegen 164 | Аналог javapoet, только на go 165 | https://github.com/dave/jennifer 166 | 167 | - json-schemas generator based on Go types 168 | Похож на jsonschema2pojo 169 | https://github.com/mcuadros/go-jsonschema-generator 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | --------------------------------------------------------------------------------