From d1245f06e3974071e2df2b3e036c89c48a5de186 Mon Sep 17 00:00:00 2001 From: Andy Lowry Date: Tue, 18 Sep 2018 19:13:23 -0400 Subject: [PATCH 1/2] [#2] Improved edge semantics Edges are now simple objects in their own right, rather than just appearing is entires in a map keyed by edge label. Each edge is either a fixed-(string-)value edge, a regex edge, or an integer edge. Labels are interpreted as: * ":"" - fixed label , even if it would otherwise look special * "*" - regex edge with regex ".*" * "re: " - regex edge * "#" integer edge When a move is requested, edges are considered in the order they were added to the machine, and the first matching edge is used. Results for moves are cached so the same move in the future won't require an edge scan. V2 and V3 state machines have had their "*" edges tightened with regexes as needed. A vendor-extension in the paths objectof the v3 walk test model was previously misconstrued as a path object and caused the test to fail. That test now passes, so things appear to be working, but more extensive tests of different edge types should be added to the state machine unit tests. Also, javadocs need to be updated on the StateMachine class. --- .../kaizen/normalizer/util/StateMachine.java | 243 +++++++++++++----- .../kaizen/normalizer/v2/V2StateMachine.java | 8 +- .../kaizen/normalizer/v3/V3StateMachine.java | 20 +- .../normalizer/test/StateMachineTest.java | 2 +- .../test/resources/models/StateWalkV3.yaml | 1 + 5 files changed, 190 insertions(+), 84 deletions(-) diff --git a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java index be7044a..ecd8240 100644 --- a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java +++ b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java @@ -6,7 +6,7 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; +import java.util.regex.Pattern; /** * Class for defining and moving through a state machine. @@ -26,13 +26,19 @@ * Each transit defines a path from a start state to an end state along one or * more labeled edges. *

- * Labels are strings. There are two wildcard labels: + * Labels are strings. There are three wildcard labels: * *

    *
  • "*" - matches any string + *
  • "re: ..." - Java regular expression; whatever follows re: + * is trimmed and then parsed as a Java regular expression. The label will match + * strings that are matched by the regex. *
  • "#" - matches any integer *
- * + * Note: in the unlikely event that you need any of the above to be treated as a + * literal value instead of a wildcard, precede your desired value with a colon; + * if you want a label to start with a colon, start with two colons. + *

* Once the transits ave been defined, a Tracker can be created and used to move * through the state machine. *

@@ -43,6 +49,10 @@ * edge, a "*" edge, if present, will be used instead. If an int value is * provided, a "#" edge will be followed, if present. *

+ * When processing a move, the available edges leading out of the current state + * are considered in the order they were added to the machine (via transits or + * out-edge copying). The first matching edge is used. + *

* The tracker records the "path" - the sequence of move values - and the * corresponding history of states. When a move value does not match any edges, * the tracker records the value in the path and adds a null value to the state @@ -61,7 +71,9 @@ public class StateMachine> { private Map> namedStates = new HashMap<>(); - private Map, Map>> graph = new IdentityHashMap<>(); + private Map, List>> graph = new IdentityHashMap<>(); + private Map, Map>> graphCache = new IdentityHashMap<>(); + private Map, State> graphIntCache = new IdentityHashMap<>(); private E anonymousValue = null; private E offRoadValue = null; private Class stateClass; @@ -89,6 +101,39 @@ public StateMachine(Class stateClass, E anonymousValue, E offRoadValue) { this.offRoadValue = offRoadValue; } + public State getMoveTarget(State start, String value) { + if (graphCache.containsKey(start)) { + if (graphCache.get(start).containsKey(value)) { + return graphCache.get(start).get(value); + } + } + for (Edge edge : getOutEdges(start)) { + if (edge.matches(value)) { + cacheMove(start, value, edge.getTarget()); + return edge.getTarget(); + } + } + return null; + } + + public State getMoveTarget(State start, int value) { + if (!graphIntCache.containsKey(start)) { + for (Edge edge : getOutEdges(start)) { + if (edge.matches(value)) { + graphIntCache.put(start, edge.getTarget()); + } + } + } + return graphIntCache.get(start); + } + + public void cacheMove(State from, String value, State to) { + if (!graphCache.containsKey(from)) { + graphCache.put(from, new HashMap>()); + } + graphCache.get(from).put(value, to); + } + /** * Define a new transit for the machine, consisting of a named start stated, a * sequence of edge labels, and a named end state. @@ -149,18 +194,49 @@ public E getOffRoadValue() { private void installTransit(TransitDef transit) { State current = transit.getStartState(); - List edges = transit.getEdges(); - int size = edges.size(); + List moves = transit.getMoves(); + int size = moves.size(); for (int i = 0; i < size - 1; i++) { - current = installEdge(current, edges.get(i)); + current = installEdge(current, moves.get(i)); } - installEdge(current, edges.get(size - 1), transit.getEndState()); + installEdge(current, moves.get(size - 1), transit.getEndState()); + } + + private State installEdge(State start, String label) { + return installEdge(start, label, null); + } + + private State installEdge(State start, String label, State end) { + List> existingEdges = getOutEdges(start); + for (Edge existingEdge : existingEdges) { + if (existingEdge.getLabel().equals(label)) { + State target = existingEdge.getTarget(); + if (end == null || end == target) { + return target; + } else { + throw new IllegalArgumentException("Multiple edges with label '" + label + "' from same state"); + } + } + } + // no existing edge with identical label... create new edge, either to provided + // end state or to a new anonymous state + State target = end != null ? end : new State(anonymousValue); + existingEdges.add(new Edge(label, target)); + return target; + } + + private List> getOutEdges(State state) { + if (!graph.containsKey(state)) { + graph.put(state, new ArrayList>()); + } + return graph.get(state); } /** * Copy the outgoing edges from one named state to another. * - * Existing edges with the same labels are overwritten. + * Existing edges for the to-state are not affected, but precede the copied + * edges when it comes to matching priority. * * @param from * @param to @@ -168,39 +244,12 @@ private void installTransit(TransitDef transit) { public void copyOutEdges(E from, E to) { State fromState = getState(from); State toState = getState(to); - Map> edges = graph.get(fromState); + List> edges = graph.get(fromState); if (edges != null) { - for (Entry> edge : edges.entrySet()) { - installEdge(toState, edge.getKey(), edge.getValue()); - } - } - } - - private State installEdge(State from, String edge) { - return installEdge(from, edge, null); - } - - private State installEdge(State from, String edge, State to) { - if (!graph.containsKey(from)) { - graph.put(from, new HashMap<>()); - } - State target = graph.get(from).get(edge); - if (target != null) { - // edge already exists - must match to-state if provided - if (to != null && to != target) { - throw new IllegalArgumentException(); + for (Edge edge : edges) { + installEdge(toState, edge.getLabel(), edge.getTarget()); } - } else { - // edge not in graph - use given to-state if provided - target = to; - if (target == null) { - // else allocate a new state as target - target = new State(anonymousValue); - } - // install this as a new edge in the graph - graph.get(from).put(edge, target); } - return target; } public State getState(E name) { @@ -215,8 +264,70 @@ public State getState(String name) { return getState(Enum.valueOf(stateClass, name)); } - private Map> getOutEdges(State state) { - return graph.get(state); + public static class Edge> { + private String label; + private State target; + private EdgeType type; + private Pattern pattern = null; + private String value = null; + + public Edge(String label, State target) { + this.label = label; + this.target = target; + if (label.equals("#")) { + this.type = EdgeType.INTEGER; + } else if (label.equals("*")) { + this.pattern = Pattern.compile(".*"); + this.type = EdgeType.REGEX; + } else if (label.startsWith("re:")) { + this.pattern = Pattern.compile(label.substring(3).trim()); + this.type = EdgeType.REGEX; + } else { + this.value = label.startsWith(":") ? label.substring(1) : label; + this.type = EdgeType.FIXED_STRING; + } + } + + public String getLabel() { + return label; + } + + public EdgeType getType() { + return type; + } + + public String getFixedValue() { + return value; + } + + public Pattern getRegex() { + return pattern; + } + + public boolean matches(String s) { + switch (type) { + case FIXED_STRING: + return value.equals(s); + case REGEX: + return pattern.matcher(s).matches(); + case INTEGER: + return false; + default: + return false; + } + } + + public boolean matches(int i) { + return type == EdgeType.INTEGER; + } + + public State getTarget() { + return target; + } + + public enum EdgeType { + FIXED_STRING, REGEX, INTEGER + }; } /** @@ -231,7 +342,7 @@ private Map> getOutEdges(State state) { */ public class TransitDef { State startState = null; - List edges = new ArrayList<>(); + List moves = new ArrayList<>(); State endState = null; /** @@ -241,7 +352,7 @@ public class TransitDef { * @return */ public TransitDef from(E start) { - if (startState != null || !edges.isEmpty() || endState != null) { + if (startState != null || !moves.isEmpty() || endState != null) { throw new IllegalStateException(); } this.startState = getState(start); @@ -251,14 +362,14 @@ public TransitDef from(E start) { /** * Provide labels for the edges that will be used/created for the transit * - * @param edges + * @param moves * @return */ - public TransitDef via(String... edges) { + public TransitDef via(String... moves) { if (startState == null || endState != null) { throw new IllegalStateException(); } - this.edges.addAll(Arrays.asList(edges)); + this.moves.addAll(Arrays.asList(moves)); return this; } @@ -268,7 +379,7 @@ public TransitDef via(String... edges) { * @param end */ public void to(E end) { - if (startState == null || edges.isEmpty() || endState != null) { + if (startState == null || moves.isEmpty() || endState != null) { throw new IllegalStateException(); } this.endState = getState(end); @@ -279,8 +390,8 @@ public State getStartState() { return startState; } - public List getEdges() { - return edges; + public List getMoves() { + return moves; } public State getEndState() { @@ -333,27 +444,27 @@ public State getCurrentState() { /** * Move to a new state based on a string value * - * @param edgeValue + * @param value * value to match against available edges. An edge labeled "*" will * be considered if no non-wild edge matches. * @return new current state, or null if current state was already null, or if * there was no matching edge */ - public State move(String edgeValue) { - return moveTo(peek(edgeValue, true), edgeValue); + public State move(String value) { + return moveTo(peek(value), value); } /** * Move to a new state based on an int value * - * @param edgeValue + * @param value * integer value. Only an edge labeled "#" can match. N.B. An edge * labeled with Integer.toString(edgeValue) will NOT match. * @return new current state, or null if current state was already null, or if * there was no matching edge */ - public State move(int edgeValue) { - return moveTo(peek("#", false), edgeValue); + public State move(int value) { + return moveTo(peek(value), value); } private State moveTo(State newState, Object edgeValue) { @@ -370,31 +481,25 @@ private State moveTo(State newState, Object edgeValue) { * Determine the state that would be current after a move to the given value, * but don't actually perform the move. * - * @param edgeValue - * @param wildOk - * whether to consider an edge labeled "*" + * @param value + * * @return the state that would become current, or null if the current state is * already null or if there is no matching edge */ - public State peek(String edgeValue, boolean wildOk) { - Map> edges = machine.getOutEdges(currentState); - State result = edges != null ? edges.get(edgeValue) : null; - if (result == null && wildOk) { - result = edges != null ? edges.get("*") : null; - } - return result; + public State peek(String value) { + return machine.getMoveTarget(currentState, value); } /** - * Determine the state that would be current after a mvoe to the given value, + * Determine the state that would be current after a move to the given value, * but don't actually perform the move. * - * @param edgeValue + * @param value * @return the state that would become current, or null if the current state is * already null or if there is no matching edge */ - public State peek(int edgeValue) { - return peek("#", false); + public State peek(int value) { + return machine.getMoveTarget(currentState, value); } /** diff --git a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/v2/V2StateMachine.java b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/v2/V2StateMachine.java index 07eaa70..0a878ce 100644 --- a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/v2/V2StateMachine.java +++ b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/v2/V2StateMachine.java @@ -24,10 +24,10 @@ public V2StateMachine() { // Set up all state transitions to use while traversing a Swagger model spec // ways to get to object definitions - transit().from(MODEL).via("paths", "*").to(PATH); - transit().from(MODEL).via("definitions", "*").to(SCHEMA_DEF); - transit().from(MODEL).via("responses", "*").to(RESPONSE_DEF); - transit().from(MODEL).via("parameters", "*").to(PARAMETER_DEF); + transit().from(MODEL).via("paths", "re: /.*").to(PATH); + transit().from(MODEL).via("definitions", "re: (?!x-)[a-zA-Z0-9._-]+").to(SCHEMA_DEF); + transit().from(MODEL).via("responses", "re: (?!x-)[a-zA-Z0-9._-]+").to(RESPONSE_DEF); + transit().from(MODEL).via("parameters", "re: (?!x-)[a-zA-Z0-9._-]+").to(PARAMETER_DEF); // ways to get to a schema object transit().from(PARAMETER).via("schema").to(SCHEMA); // valid if `in == "body"` diff --git a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/v3/V3StateMachine.java b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/v3/V3StateMachine.java index f438567..5cdbb1f 100644 --- a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/v3/V3StateMachine.java +++ b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/v3/V3StateMachine.java @@ -36,16 +36,16 @@ public V3StateMachine() { // Set up all state transitions to use while traversing OpenAPI v3 model spec // ways to get to component definitions - transit().from(MODEL).via("paths", "*").to(PATH); - transit().from(MODEL).via("components", "schemas", "*").to(SCHEMA_DEF); - transit().from(MODEL).via("components", "responses", "*").to(RESPONSE_DEF); - transit().from(MODEL).via("components", "parameters", "*").to(PARAMETER_DEF); - transit().from(MODEL).via("components", "examples", "*").to(EXAMPLE_DEF); - transit().from(MODEL).via("components", "requestBodies", "*").to(REQUEST_BODY_DEF); - transit().from(MODEL).via("components", "headers", "*").to(HEADER_DEF); - transit().from(MODEL).via("components", "securitySchemes", "*").to(SECURITY_SCHEME_DEF); - transit().from(MODEL).via("components", "links", "*").to(LINK_DEF); - transit().from(MODEL).via("components", "callbacks", "*").to(CALLBACK_DEF); + transit().from(MODEL).via("paths", "re: /.*").to(PATH); + transit().from(MODEL).via("components", "schemas", "re: (?!x-)[A-Za-z0-9._-]+").to(SCHEMA_DEF); + transit().from(MODEL).via("components", "responses", "re: (?!x-)[A-Za-z0-9._-]+").to(RESPONSE_DEF); + transit().from(MODEL).via("components", "parameters", "re: (?!x-)[A-Za-z0-9._-]+").to(PARAMETER_DEF); + transit().from(MODEL).via("components", "examples", "re: (?!x-)[A-Za-z0-9._-]+").to(EXAMPLE_DEF); + transit().from(MODEL).via("components", "requestBodies", "re: (?!x-)[A-Za-z0-9._-]+").to(REQUEST_BODY_DEF); + transit().from(MODEL).via("components", "headers", "re: (?!x-)[A-Za-z0-9._-]+").to(HEADER_DEF); + transit().from(MODEL).via("components", "securitySchemes", "re: (?!x-)[A-Za-z0-9._-]+").to(SECURITY_SCHEME_DEF); + transit().from(MODEL).via("components", "links", "re: (?!x-)[A-Za-z0-9._-]+").to(LINK_DEF); + transit().from(MODEL).via("components", "callbacks", "re: (?!x-)[A-Za-z0-9._-]+").to(CALLBACK_DEF); // ways to get to a schema object transit().from(PARAMETER).via("schema").to(SCHEMA); diff --git a/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/StateMachineTest.java b/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/StateMachineTest.java index ec76365..f6145b1 100644 --- a/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/StateMachineTest.java +++ b/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/StateMachineTest.java @@ -31,10 +31,10 @@ private void defineTransits() { //                 ⇙ ⇖ //                 int // - machine.transit().from(A).via("x", "*", "y").to(B); machine.transit().from(A).via("x", "shortcut").to(C); machine.transit().from(B).via("#").to(B); machine.transit().from(B).via("done").to(C); + machine.transit().from(A).via("x", "*", "y").to(B); } @Test diff --git a/kaizen-openapi-normalizer/src/test/resources/models/StateWalkV3.yaml b/kaizen-openapi-normalizer/src/test/resources/models/StateWalkV3.yaml index a26511a..35f2223 100644 --- a/kaizen-openapi-normalizer/src/test/resources/models/StateWalkV3.yaml +++ b/kaizen-openapi-normalizer/src/test/resources/models/StateWalkV3.yaml @@ -8,6 +8,7 @@ info: servers: - url: https://api.beamup.com/v1 paths: + x-extension: Caught you! Fix the state machine NOW. /products: get: summary: Product Types From eba1162c79b1461c1da3778f1636d8b777a86730 Mon Sep 17 00:00:00 2001 From: Andy Lowry Date: Tue, 18 Sep 2018 20:55:18 -0400 Subject: [PATCH 2/2] [#2] Enhance state machine tests and update javadocs New regex-based edge lables are now exercised by the state machine unit tests. --- .../kaizen/normalizer/util/StateMachine.java | 263 ++++++++++++++++-- .../normalizer/test/StateMachineTest.java | 20 +- 2 files changed, 248 insertions(+), 35 deletions(-) diff --git a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java index ecd8240..c8e3647 100644 --- a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java +++ b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java @@ -33,7 +33,8 @@ *

  • "re: ..." - Java regular expression; whatever follows re: * is trimmed and then parsed as a Java regular expression. The label will match * strings that are matched by the regex. - *
  • "#" - matches any integer + *
  • "#" - matches any integer (i.e. a move using an integer + * value, not a string that looks like an integer) * * Note: in the unlikely event that you need any of the above to be treated as a * literal value instead of a wildcard, precede your desired value with a colon; @@ -44,14 +45,10 @@ *

    * The tracker is instantiated with an enum identifying the start state. * Thereafter, the tracker can be "moved" by providing a string or integer - * value. If a matching edge exists from the current state, its destination - * becomes the new current state. If a string value does not correspond to an - * edge, a "*" edge, if present, will be used instead. If an int value is - * provided, a "#" edge will be followed, if present. - *

    - * When processing a move, the available edges leading out of the current state - * are considered in the order they were added to the machine (via transits or - * out-edge copying). The first matching edge is used. + * value. The first matching edge originating in the current state, if any, + * determines the new current state. Edge ordering is determined by the order of + * the {@link #transit()} and/or {@link #copyOutEdges(Enum, Enum)} operations + * performed when defining the machine. *

    * The tracker records the "path" - the sequence of move values - and the * corresponding history of states. When a move value does not match any edges, @@ -61,7 +58,8 @@ *

    * The tracker can also back up, removing items from the tails of the path and * the state history. Backing up when off-road removes the nulls caused by - * non-matching moves. + * non-matching moves. The tracker can also be reset to a given state, which + * clears the path and history as a side-effect * * @author Andy Lowry * @@ -81,6 +79,9 @@ public class StateMachine> { /** * Create a new state machine instance, with no special values for anonymous and * off-road states. + * + * @param stateClass + * class of the state enum type */ public StateMachine(Class stateClass) { this(stateClass, null, null); @@ -90,6 +91,8 @@ public StateMachine(Class stateClass) { * Create a new state machine with special enum values for anonymous and * off-road states. * + * @param stateClass + * class of the state enum type * @param anonymousValue * value to be used for anonymous states, or null for none * @param offRoadValue @@ -101,6 +104,16 @@ public StateMachine(Class stateClass, E anonymousValue, E offRoadValue) { this.offRoadValue = offRoadValue; } + /** + * Determine the state that the given string move value should move to, by + * considering the given state's out edges + * + * @param start + * start state for the presumed move + * @param value + * value for the move + * @return end state of the presumed move + */ public State getMoveTarget(State start, String value) { if (graphCache.containsKey(start)) { if (graphCache.get(start).containsKey(value)) { @@ -116,6 +129,16 @@ public State getMoveTarget(State start, String value) { return null; } + /** + * Determine the state that the given integer move value should move to, by + * considering the given state's out edges + * + * @param start + * start state for the presumed move + * @param value + * value for the move + * @return end state of the presumed move + */ public State getMoveTarget(State start, int value) { if (!graphIntCache.containsKey(start)) { for (Edge edge : getOutEdges(start)) { @@ -127,7 +150,7 @@ public State getMoveTarget(State start, int value) { return graphIntCache.get(start); } - public void cacheMove(State from, String value, State to) { + private void cacheMove(State from, String value, State to) { if (!graphCache.containsKey(from)) { graphCache.put(from, new HashMap>()); } @@ -239,7 +262,9 @@ private List> getOutEdges(State state) { * edges when it comes to matching priority. * * @param from + * state whose edges are to be copied * @param to + * state that will receive the copies */ public void copyOutEdges(E from, E to) { State fromState = getState(from); @@ -252,6 +277,15 @@ public void copyOutEdges(E from, E to) { } } + /** + * Return the named state for the given name value. + *

    + * This method always returns the same state object for the same name value. + * + * @param name + * one of the values of the enum used for name states + * @return unique state value for the given name value + */ public State getState(E name) { if (!namedStates.containsKey(name)) { State state = new State(name); @@ -260,10 +294,26 @@ public State getState(E name) { return namedStates.get(name); } + /** + * Return the named state for the given enum name value + *

    + * This is just like {@link #getState(Enum)}, but the enum value is specified by + * its name. + * + * @param name + * @return + */ public State getState(String name) { return getState(Enum.valueOf(stateClass, name)); } + /** + * An edge, characterized by a label and a target state. + * + * @author Andy Lowry + * + * @param + */ public static class Edge> { private String label; private State target; @@ -271,6 +321,31 @@ public static class Edge> { private Pattern pattern = null; private String value = null; + /** + * Create a new edge. + *

    + * Label values are interpreted as follows: + *

    + *
    *
    + *
    Wildcard - equivalent to "re: .*"
    + *
    re: regex
    + *
    Regex - everything following ":re" is interpreted as a Java + * regular expression.
    + *
    #
    + *
    Any integer value (not a string that looks like an integer)
    + *
    :anything
    + *
    A fixed-string label defined by the string following the initial + * colon
    + *
    anything-else
    + *
    A fixed-string label
    + * + *

    + * + * @param label + * the label string + * @param target + * the target state + */ public Edge(String label, State target) { this.label = label; this.target = target; @@ -288,22 +363,56 @@ public Edge(String label, State target) { } } + /** + * Return the label used to define this edge (not any of its interpretations) + * + * @return + */ public String getLabel() { return label; } + /** + * Return the type of this edge: one of FIXED_STRING, + * REGEX, and INTEGER. + * + * @return + */ public EdgeType getType() { return type; } + /** + * Return the fixed value determined from the label, if the edge type is + * FIXED_VALUE + * + * @return the fixed value, or null if this is not a fixed value edge + */ public String getFixedValue() { return value; } + /** + * Return the pattern determined from the label, if the edge type is + * REGEX. + * + * @return the regex pattern, or null if this is not a regex edge + */ public Pattern getRegex() { return pattern; } + /** + * Determine whether this edge matches a given string value + * + * @param s + * the string value + * @return true if this edge matches + */ + /** + * @param s + * @return + */ public boolean matches(String s) { switch (type) { case FIXED_STRING: @@ -317,10 +426,22 @@ public boolean matches(String s) { } } + /** + * Determine whether this edge matches a given integer value. + * + * @param i + * the integer value + * @return true if this edge matches (i.e. its any integer-type edge) + */ public boolean matches(int i) { return type == EdgeType.INTEGER; } + /** + * Get the target state of this edge. + * + * @return + */ public State getTarget() { return target; } @@ -406,36 +527,50 @@ public State getEndState() { * @author Andy Lowry * */ - /** - * @author Andy Lowry - * - */ public static class Tracker> { private StateMachine machine; private State currentState; private List> crumbs = new ArrayList<>(); private List path = new ArrayList<>(); private E offRoadValue = null; + private State initialStartState; + /** + * Create a tracker for a given state machine with a given start state + * + * @param machine + * the state machine + * @param start + * the start state enum value + */ private Tracker(StateMachine machine, E start) { this(machine, machine.getState(start)); } + /** + * Create a tracker for a given state machine with a given start state + * + * @param machine + * the state machine + * @param start + * the start state value + */ private Tracker(StateMachine machine, State start) { this.machine = machine; this.currentState = start; this.offRoadValue = machine.getOffRoadValue(); + this.initialStartState = start; } /** - * Get the machine's current state + * Get the tracker's current state * * If the tracker is off-road, null will be returned unless an off-road enum * value has been set for the machine. In that case, a new State instance will * be created and returned, using that value. * - * @return the current state, or null if the current path includes a unmatched - * move. + * @return the current state, or null-or-offroad-state if the current path + * includes a unmatched move. */ public State getCurrentState() { return currentState; @@ -445,23 +580,24 @@ public State getCurrentState() { * Move to a new state based on a string value * * @param value - * value to match against available edges. An edge labeled "*" will - * be considered if no non-wild edge matches. + * value to match against available edges * @return new current state, or null if current state was already null, or if - * there was no matching edge + * there was no matching edge (or the anonymous or off-road state + * instead of null, if they have been provided for the machine) */ public State move(String value) { return moveTo(peek(value), value); } /** - * Move to a new state based on an int value + * Move to a new state based on an integer value * * @param value * integer value. Only an edge labeled "#" can match. N.B. An edge - * labeled with Integer.toString(edgeValue) will NOT match. + * that matches Integer.toString(value) will NOT match. * @return new current state, or null if current state was already null, or if - * there was no matching edge + * there was no matching edge (or the anonymous or off-road state + * instead of null, if they have been provided for the machine) */ public State move(int value) { return moveTo(peek(value), value); @@ -484,7 +620,9 @@ private State moveTo(State newState, Object edgeValue) { * @param value * * @return the state that would become current, or null if the current state is - * already null or if there is no matching edge + * already null or if there is no matching edge (or anonymous or + * off-road state instead of null, if they have been provided for the + * machine) */ public State peek(String value) { return machine.getMoveTarget(currentState, value); @@ -496,7 +634,9 @@ public State peek(String value) { * * @param value * @return the state that would become current, or null if the current state is - * already null or if there is no matching edge + * already null or if there is no matching edge (or anonymous or + * off-road state instead of null, if they have been provided for the + * machine) */ public State peek(int value) { return machine.getMoveTarget(currentState, value); @@ -507,7 +647,9 @@ public State peek(int value) { * * The final entries on the current path and the state history are removed. * - * @return the new current state - may be null if the machine is still off-road + * @return the new current state - may be null if the tracker is still off-road + * or the new state is anonymous - or the anonymous or off-road state + * instead of null, if they have been provided for the machine) */ public State backup() { return backup(1); @@ -519,6 +661,9 @@ public State backup() { * @param n * number of moves to back out of * @return the new current state - may be null if the machine is still off-road + * or if the new state is an anonymous state - or the anonymous or + * off-road value instead of null if they have been provided for the + * machine) */ public State backup(int n) { while (n-- > 0) { @@ -532,22 +677,81 @@ public State backup(int n) { return currentState; } + /** + * Reset the tracker to the given state. + *

    + * This clears the path and state history. + * + * @param state + * the state to become the new current state + */ + public void reset(State state) { + this.currentState = state; + this.crumbs.clear(); + this.path.clear(); + } + + /** + * Reset the tracker to the given state. + *

    + * This clears the path and state history. + * + * @param state + * the enum value for the state to become the new current state + */ + public void reset(E stateValue) { + reset(machine.getState(stateValue)); + } + + /** + * Reset the tracker to the start state that was specified at its creation. + */ + public void reset() { + reset(this.initialStartState); + } + + /** + * Get the list of values that led from the start state to the current state. + * + * @return list of string and integer values used in moves + */ public List getPath() { return new ArrayList<>(path); } } + /** + * A state in a state machine. + *

    + * Encompasses a value from the underlying enum type. + *

    + * The encompassed value may be null in the case of an anonymous or off-road + * state, if the state machine has not been configured with enum values to use + * for such states. + * + * @author Andy Lowry + * + * @param + * the underlying enum type + */ public static class State> { private E value = null; + /** + * Create a new state for the given enum value + * + * @param name + * enum value, or null for anonymous or off-road state + */ private State(E name) { this.value = name; } /** - * Get this state's name + * Get this state's enum value * - * @return the enum naming this state, or null if this is an anonymous state + * @return the enum naming this state. This may be null for an anonymous or + * off-road state. */ public E getValue() { return value; @@ -558,5 +762,4 @@ public String toString() { return String.format("State[%s]", getValue()); } } - } diff --git a/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/StateMachineTest.java b/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/StateMachineTest.java index f6145b1..6446df7 100644 --- a/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/StateMachineTest.java +++ b/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/StateMachineTest.java @@ -27,14 +27,15 @@ public void createMachine() { private void defineTransits() { //         ⇗ "shortcut"⇒ ⇒ ⇒ ⇒ ⇘ - // Ⓐ ⇒ "x" ⇒ "*" ⇒ Ⓑ ⇒ "done" ⇒ Ⓒ - //                 ⇙ ⇖ - //                 int + // Ⓐ ⇒ "x*" ⇒ "*" ⇒ Ⓑ ⇒ "done" ⇒ Ⓒ + //   ⇘ ⇒  "y*" ⇒ ⇗  ⇙ ⇖ + //                  int // - machine.transit().from(A).via("x", "shortcut").to(C); + machine.transit().from(A).via("re: x.*", "shortcut").to(C); machine.transit().from(B).via("#").to(B); machine.transit().from(B).via("done").to(C); - machine.transit().from(A).via("x", "*", "y").to(B); + machine.transit().from(A).via("re: x.*", "*", "y").to(B); + machine.transit().from(A).via("re: y.*").to(B); } @Test @@ -45,6 +46,9 @@ public void testSimpleMoves() { private Tracker performSimpleMoves() { Tracker tracker = machine.tracker(A); checkState(tracker, A); + checkMove(tracker, "yellow", B); + checkPath(tracker, "yellow"); + checkReset(tracker, A); checkMove(tracker, "x", null); checkPath(tracker, "x"); checkMove(tracker, "hello", null); @@ -162,6 +166,12 @@ private void checkPath(Tracker tracker, Object... expectedPath) { assertEquals(Arrays.asList(expectedPath), tracker.getPath()); } + private void checkReset(Tracker tracker, S value) { + tracker.reset(value); + checkState(tracker, value); + checkPath(tracker); + } + public static enum S { A, B, C, ANON, OFF_ROAD; }