Skip to content

open-jumpco/kfsm-spring-rest

Repository files navigation

KFSM Turnstile Sample for Spring HATEOAS

88x31

A simple application to demonstrate implementing KFSM with the classic Turnstile FSM.

./gradlew bootRun

Description

When designing a FSM careful consideration of how the state is externalised is important. It is advisable that the type used to represent the state in the definition also be the type persisted by the domain entity or at least providing a consistent bi-directional conversion.

In the Turnstile example we are only concerned with 2 states so a Boolean is an acceptible standin.

The FSM definition uses TurnstileContext to represent the operations and determine the current state.

The domain class TurnstileData is a representation of the state of the Turnstile for external use and is return from the event.

data class TurnstileData(
    val id: Long,
    val locked: Boolean,
    val message: String
) {
    val currentState: TurnstileState
        get() = if (locked) LOCKED else UNLOCKED
}

interface TurnstileContext {
    val currentState: TurnstileState
    fun alarm(): TurnstileData?
    fun lock(): TurnstileData?
    fun unlock(): TurnstileData?
    fun returnCoin(): TurnstileData?
}

We extend the TurnstileContext to create a persistent context using Spring Data JDBC

@ResponseStatus(CONFLICT)
class TurnstileAlarmException(message: String) : Exception(message)

@ResponseStatus(NOT_FOUND)
class TurnstileInfoNotFound(message: String) : Exception(message)

@Table("TURNSTILE_INFO")
@Entity
data class TurnstileEntity(
    @Id var id: Long? = null,
    @Column("LOCKED_B")
    val locked: Boolean = true,
    @Column("MESSAGE_S")
    val message: String = ""
) {
    fun update(locked: Boolean? = null, message: String? = null) =
        copy(locked = locked ?: this.locked, message = message ?: "")

    fun toInfo() = TurnstileData(id!!, locked, message)
}

interface TurnstileRepository : PagingAndSortingRepository<TurnstileEntity, Long>

class TurnstilePersistentContext(private val turnstileRepository: TurnstileRepository, id: Long) : TurnstileContext {
    val turnstileInfo: TurnstileEntity

    init {
        turnstileInfo =
            turnstileRepository.findById(id).orElseThrow { TurnstileInfoNotFound("TurnstileEntity $id not found") }
    }

    override val currentState: TurnstileState
        get() = if (turnstileInfo.locked) LOCKED else UNLOCKED

    override fun alarm(): TurnstileData? {
        throw TurnstileAlarmException("Alarm")
    }

    override fun lock(): TurnstileData? {
        return turnstileRepository.save(turnstileInfo.update(locked = true)).toInfo()
    }

    override fun unlock(): TurnstileData? {
        return turnstileRepository.save(turnstileInfo.update(locked = false)).toInfo()
    }

    override fun returnCoin(): TurnstileData? {
        return turnstileRepository.save(turnstileInfo.update(message = "Coin returned")).toInfo()
    }
}

UI

A Web UI is available at kfsm-hateoas-client

This is embedded in src/main/resources/static

Operations

This section describes the various operations.

We can obtain a list of top level API operations.

ApiList

http http://localhost:8080/api

Should return:

{
    "_links": {
        "create": {
            "href": "http://localhost:8080/api/turnstile/"
        },
        "list": {
            "href": "http://localhost:8080/api/turnstile/"
        },
        "self": {
            "href": "http://localhost:8080/api?requestedVersion=1"
        }
    },
    "apiName": "Turnstile",
    "apiVersion": 1
}

Only list and create are available at the top-level. All other operations apply to specific turnstile resources, and those resources contain the embedded links for performing the options.

We start by creating some turnstile entities:

Create

http POST http://localhost:8080/api/turnstile/

Should return:

{
    "_links": {
        "coin": {
            "href": "http://localhost:8080/api/turnstile/1/coin"
        },
        "self": {
            "href": "http://localhost:8080/api/turnstile/1"
        }
    },
    "currentState": "LOCKED",
    "id": 1,
    "locked": true,
    "message": ""
}

After calling is 5 times we can list the turnstiles

List

http http://localhost:8080/api/turnstile/

Should return:

{
  "_embedded": {
    "turnstiles": [
      {
        "id": 1,
        "locked": true,
        "message": "",
        "currentState": "LOCKED",
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/turnstile/1"
          },
          "delete": {
            "href": "http://localhost:8080/api/turnstile/1"
          },
          "coin": {
            "href": "http://localhost:8080/api/turnstile/1/coin"
          }
        }
      },
      {
        "id": 2,
        "locked": true,
        "message": "",
        "currentState": "LOCKED",
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/turnstile/2"
          },
          "delete": {
            "href": "http://localhost:8080/api/turnstile/2"
          },
          "coin": {
            "href": "http://localhost:8080/api/turnstile/2/coin"
          }
        }
      },
      {
        "id": 3,
        "locked": true,
        "message": "",
        "currentState": "LOCKED",
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/turnstile/3"
          },
          "delete": {
            "href": "http://localhost:8080/api/turnstile/3"
          },
          "coin": {
            "href": "http://localhost:8080/api/turnstile/3/coin"
          }
        }
      },
      {
        "id": 4,
        "locked": true,
        "message": "",
        "currentState": "LOCKED",
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/turnstile/4"
          },
          "delete": {
            "href": "http://localhost:8080/api/turnstile/4"
          },
          "coin": {
            "href": "http://localhost:8080/api/turnstile/4/coin"
          }
        }
      },
      {
        "id": 5,
        "locked": true,
        "message": "",
        "currentState": "LOCKED",
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/turnstile/5"
          },
          "delete": {
            "href": "http://localhost:8080/api/turnstile/5"
          },
          "coin": {
            "href": "http://localhost:8080/api/turnstile/5/coin"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/api/turnstile/?page=0&size=10"
    }
  },
  "page": {
    "size": 10,
    "totalElements": 5,
    "totalPages": 1,
    "number": 0
  }
}

Read

This uses the self link from the resource.

http http://localhost:8080/api/turnstile/1

Returns:

{
    "_links": {
        "coin": {
            "href": "http://localhost:8080/api/turnstile/1/coin"
        },
        "self": {
            "href": "http://localhost:8080/api/turnstile/1"
        }
    },
    "id": 1,
    "locked": true,
    "message": ""
}

Coin

http POST http://localhost:8080/api/turnstile/1/coin

Should return:

{
    "_links": {
        "coin": {
            "href": "http://localhost:8080/api/turnstile/1/coin"
        },
        "pass": {
            "href": "http://localhost:8080/api/turnstile/1/pass"
        },
        "self": {
            "href": "http://localhost:8080/api/turnstile/1"
        }
    },
    "id": 1,
    "locked": false,
    "message": ""
}

Pass

http POST http://localhost:8080/api/turnstile/1/pass

Should return:

{
    "_links": {
        "coin": {
            "href": "http://localhost:8080/api/turnstile/1/coin"
        },
        "self": {
            "href": "http://localhost:8080/api/turnstile/1"
        }
    },
    "id": 1,
    "locked": true,
    "message": ""
}

Invalid pass event

http POST http://localhost:8080/api/turnstile/1/pass

The system throws TurnstileAlarmException which results in 409 - Conflict

{
    "error": "Conflict",
    "message": "Alarm",
    "path": "/1/pass",
    "status": 409,
    "timestamp": "2020-01-30T21:06:05.491+0000"
}

Coin when unlocked

http POST http://localhost:8080/api/turnstile/1/coin

Should return:

{
    "_links": {
        "coin": {
            "href": "http://localhost:8080/api/turnstile/1/coin"
        },
        "pass": {
            "href": "http://localhost:8080/api/turnstile/1/pass"
        },
        "self": {
            "href": "http://localhost:8080/api/turnstile/1"
        }
    },
    "id": 1,
    "locked": false,
    "message": "Coin returned"
}

Generated State Table

Generated State Diagram

turnstile

Generated Simple State Diagram

turnstile simple

To learn more about visualization visit kfsm-viz and kfsm-viz-plugin

Class Diagrams

FSM Classes

turnstile classes

Service Classes

turnstile service

Controller Classes

turnstile controller

Releases

No releases published

Packages

No packages published

Languages