├── .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 extends WiremockConfigFactory> factory() default WiremockConfigFactory.DefaultWiremockConfigFactory.class;
98 |
99 | /**
100 | * @return class which defines on how to customize server after start
101 | */
102 | Class extends WiremockCustomizer> 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 |
--------------------------------------------------------------------------------