A CRUD address service with a mutex-locked repository

The complete source of webServices_addresses.ek9, compiled by the EK9 compiler. Shown on webServices.html.

#!ek9
defines module site.webServices.addresses

  defines type
    //Just for strong typing - no constraints
    AddressId as String

  defines record
    Address
      id <- AddressId()
      street <- String()
      street2 <- String()
      city <- String()
      state <- String()
      zipcode <- String()

      Address()
        ->
          id as AddressId
          street as String
          street2 as String
          city as String
          state as String
          zipcode as String

        this.id :=: id
        this.street :=: street
        this.street2 :=: street2
        this.city :=: city
        this.state :=: state
        this.zipcode :=: zipcode

      Address()
        -> from as Address
        this :=: from

      Address()
        -> from as Optional of Address
        if from?
          this :=: from.get()

      //Copy
      operator :=:
        -> from as Address
        id :=: from.id
        street :=: from.street
        street2 :=: from.street2
        city :=: from.city
        state :=: from.state
        zipcode :=: from.zipcode

      //Merge only if incoming address parts are set
      operator :~:
        -> address as Address
        if address.street?
          street :=: address.street
        if address.street2?
          street2 :=: address.street2
        if address.city?
          city :=: address.city
        if address.state?
          state :=: address.state
        if address.zipcode?
          zipcode :=: address.zipcode

      override operator ? as pure
        //street2 is optional and can be omitted
        <- rtn as Boolean: id? and street? and city? and state? and zipcode?

  defines text for "en"
    AddressToOutputFormat

      toJSON()
        -> address as Address
        `{
"address":
    {
    "id": "${address.id}",
    "street": "${address.street}",
    ${optionalJSON("street2", address.street2)}
    "city": "${address.city}",
    "state": "${address.state}",
    "zipcode": "${address.zipcode}"
    }
}`

  defines function

    //Used for specific operations to be applied to a set of addresses
    addressOperation() as abstract
      ->
        addresses as AddressAccess
        address as Address

    //deals with wrapping the operation in the calls to deal with mutex lock.
    safeOperation()
      ->
        lockedAddressSet as MutexLock of AddressSet
        address as Address
        operation as addressOperation

      accessKey <- (address, operation) extends MutexKey of AddressSet as function
          operation(lockedItem, address)

      require lockedAddressSet.enter(accessKey)

    addressFromJson()
      -> addressInJSONFormat as String
      <- rtn as Address: Address()

      addressParts <- addressDictionaryFromJSON(addressInJSONFormat)
      rtn.id: AddressId(addressParts.getOrDefault("id", ""))
      rtn.street: String(addressParts.getOrDefault("street", ""))
      rtn.street2: String(addressParts.getOrDefault("street2", ""))
      rtn.city: String(addressParts.getOrDefault("city", ""))
      rtn.state: String(addressParts.getOrDefault("state", ""))
      rtn.zipcode: String(addressParts.getOrDefault("zipcode", ""))

    <?-
      Until we have a more general JSON parser
      a bespoke parser is needed for this demonstration
    -?>
    addressDictionaryFromJSON()
      -> json as String
      <- rtn <- Dict() of (String, String)

      //Just pull out the address bits from within {}'s using a regex
      extractAddressPartsEx <- /\{\s+"address":\s+\{\s+([^}]*?)\s+\}\s+\}/
      //Now break into lines based on commas
      addressItems <- json.split(extractAddressPartsEx).getOrDefault(0, "").split(/,/)
      //Map to a dictionary and return.
      rtn: cat addressItems | map with toDictEntry | collect as Dict of (String, String)

    toDictEntry()
      -> line as String
      <- rtn as DictEntry of (String, String)?
      keyValues <- line.trim().split(/:/)
      rtn: DictEntry(keyValues.getOrDefault(0, "").trim().trim('"'), keyValues.getOrDefault(length keyValues - 1, "").trim().trim('"'))

    copyAddress()
      -> from as Address
      <- to as Address: Address(from)

    wrapInBrackets()
      -> mainValue as String
      <- rtn as String: `[ ${mainValue} ]`

    commaSeparated()
      ->
        firstPart String
        secondPart String
      <-
        rtn as String: firstPart? and secondPart? <- `${firstPart},${secondPart}` : String()

    addressToJSON()
      -> address as Address
      <- addressAsString as String: AddressToOutputFormat("en").toJSON(address)

    addressListToJSON()
      -> addresses as List of Address
      <- listAsString as String: cat addresses
        | map with addressToJSON
        | join with commaSeparated
        | map with wrapInBrackets
        | collect as String


    optionalJSON() as pure
      ->
        name as String
        mainValue as String
      <-
        rtn as String: mainValue? <- `"${name}": "${mainValue}",` else String()

  defines trait

    AddressAccess

      hash()
        <- rtn as String: String()

      hashOfAddress()
        -> id as AddressId
        <- rtn as String?
        require id?
        rtn: String()

      byId()
        -> id as AddressId
        <- rtn as Address?

      listAll()
        <- rtn as List of Address: List() of Address

      operator +=
        -> address as Address
        require address?

      operator -=
        -> address as Address
        require address?

      //merge with an existing address
      operator :~:
        -> address as Address
        require address?

      //replace an existing address
      operator :^:
        -> address as Address
        require address?

      operator contains as pure
        -> addressId as AddressId
        <- rtn as Boolean: Boolean()

  defines class

    AddressSet with trait of AddressAccess
      hash as String: Digest().SHA256(GUID())
      addresses <- Dict() of (AddressId, Address)
      hashes <- Dict() of (AddressId, String)

      override hash()
        <- rtn as String: this.hash

      override hashOfAddress()
        -> id as AddressId
        <- rtn as String: String(hashes.getOrDefault(id, ""))

      override byId()
        -> id as AddressId
        <- rtn as Address: Address(addresses.getOrDefault(id, Address()))

      override listAll()
        <- rtn <- List() of Address

        iter <- addresses.values()
        cat iter | map with copyAddress > rtn

      private includeAddress()
        -> address as Address

        copy <- Address(address)
        addresses += DictEntry(copy.id, copy)
        hashes += DictEntry(copy.id, Digest().SHA256(addressToJSON(copy)))
        updateHash()

      private updateHash()
        hash :=: Digest().SHA256(GUID())

      override operator +=
        -> address as Address
        require address?

        if this not contains address
          includeAddress(address)

      override operator -=
        -> address as Address
        require address.id?

        addresses -= address.id
        hashes -= address.id
        updateHash()

      override operator :~:
        -> address as Address
        require address.id?
        //We don't assert whole address because it can be partial

        currentAddress <- addresses.getOrDefault(address.id, Address())
        if currentAddress?
          //make a new copy and then merge the two.
          updatedAddress <- Address(currentAddress)
          updatedAddress :~: address
          includeAddress(updatedAddress)

      override operator :^:
        -> address as Address
        require address?

        if this contains address
          includeAddress(address)

      operator contains as pure
        -> address as Address
        <- rtn as Boolean: this contains address.id

      override operator contains as pure
        -> addressId as AddressId
        <- rtn as Boolean: addresses contains addressId

      default operator ?

    //Example of wrapping shared data set in a mutex lock.
    LockableAddressSet with trait of AddressAccess
      lockedAddressSet as MutexLock of AddressSet: MutexLock(AddressSet())

      override hash()
        <- rtn as String: String()
        accessKey <- (rtn) is MutexKey of AddressSet as function
          rtn :=: lockedItem.hash()

        require lockedAddressSet.enter(accessKey)

      override hashOfAddress()
        -> id as AddressId
        <- rtn as String: String()
        accessKey <- (id, rtn) is MutexKey of AddressSet as function
          rtn :=: lockedItem.hashOfAddress(id)

        require lockedAddressSet.enter(accessKey)

      override byId()
        -> id as AddressId
        <- rtn as Address: Address()
        accessKey <- (id, rtn) is MutexKey of AddressSet as function
          rtn :=: lockedItem.byId(id)

        require lockedAddressSet.enter(accessKey)

      override listAll()
        <- rtn <- List() of Address
        accessKey <- (rtn) is MutexKey of AddressSet as function
          rtn += lockedItem.listAll()

        require lockedAddressSet.enter(accessKey)

      override operator +=
        -> address as Address

        //You can inline this simple dynamic function if you wish
        //Also used named parameters
        safeOperation(
          lockedAddressSet: lockedAddressSet,
          address: address,
          operation: () is addressOperation (addresses += address)
          )

      override operator -=
        -> address as Address

        //Or you can inline all on one line.
        safeOperation(lockedAddressSet, address, () is addressOperation (addresses -= address))

      override operator :~:
        -> address as Address

        //Or define a dynamic function and pass in as delegate (my preferred way)
        operation <- () is addressOperation
          addresses :~: address
        safeOperation(lockedAddressSet, address, operation)

      override operator :^:
        -> address as Address
        operation <- () is addressOperation
          addresses :^: address
        safeOperation(lockedAddressSet, address, operation)

      override operator contains as pure
        -> addressId as AddressId
        <- rtn as Boolean: false
        accessKey <- (addressId, rtn) is MutexKey of AddressSet as function
          rtn :=: lockedItem contains addressId

        require lockedAddressSet.enter(accessKey)

      default operator ?

  defines function

    plainNonCacheableHTTPResponse()
      <- rtn as HTTPResponse: () with trait of HTTPResponse
        override cacheControl() as pure
          <- rtn as String: "no-store,max-age=0"
        override contentType() as pure
          <- rtn as String: "text/plain"
        override contentLanguage() as pure
          <- rtn as String: "en"
        default operator ?

    cacheableHTTPResponse()
      <- rtn as HTTPResponse: () with trait of HTTPResponse
        override cacheControl() as pure
          <- rtn as String: "public,max-age=5,must-revalidate"
        override contentType() as pure
          <- rtn as String: "application/json"
        override contentLanguage() as pure
          <- rtn as String: "en"
        default operator ?

  defines class

    ByETagHTTPResponse with trait of HTTPResponse by delegate
      repository as Repository!

      delegate as HTTPResponse?
      addressId as AddressId?
      provideContentLocation as Boolean?

      status <- Integer()
      etagOfAddress <- String()

      default private ByETagHTTPResponse()

      ByETagHTTPResponse()
        ->
          addressId as AddressId
          delegate as HTTPResponse
          provideContentLocation as Boolean
        require addressId? and delegate? and provideContentLocation?

        this.addressId: addressId
        this.provideContentLocation: provideContentLocation
        this.delegate: delegate

      ByETagHTTPResponse()
        ->
          addressId as AddressId
          delegate as HTTPResponse
        this(addressId, delegate, true)

      override etag()
        <- rtn as String: String()
        //Only call if un-set.
        etagOfAddress :=? repository.addresses().hashOfAddress(addressId)
        rtn := etagOfAddress
        status :=? ~etagOfAddress? <- 404 else 200

      status()
        -> newStatus as Integer
        status :=: newStatus
      override status() as pure
        <- rtn as Integer: status
      override contentLocation() as pure
        <- rtn as String: String()
        httpBadRequest <- 400
        rtn: status < httpBadRequest and provideContentLocation <- `/addresses/${addressId}` else String()
      default operator ?
  <?-
    REST web service.
  -?>
  defines service
    //The Name of the and the uri it is mapped to
    Addresses for :/addresses open

      //It is still possible to use dynamic variable capture, like this but see the later code.
      //It used named variables.
      byId() as GET for :/{address-id}
        -> id as String :=: PATH "address-id" //required because different name
        <- response as HTTPResponse?

        addressId <- AddressId(id)
        delegate <- ByETagHTTPResponse(addressId, cacheableHTTPResponse(), false)
        response: (addressId, delegate) with trait of HTTPResponse by delegate
          repository as Repository!

          override content()
            <- rtn as Bytes: Bytes()
            httpNotFound <- 404
            if delegate.status() <> httpNotFound
              rtn: Bytes().withUtf8(addressToJSON(repository.addresses().byId(addressId)))
          default operator ?

      //A POST
      operator += for :/
        -> request as HTTPRequest :=: REQUEST
        <- response as HTTPResponse: (
            request: request,
            nonCacheable: plainNonCacheableHTTPResponse()
          ) with trait of HTTPResponse by nonCacheable

            repository as Repository!
            address as Address: Address()
            status as Integer: 201

            override content()
              <- rtn as Bytes: Bytes()
              address: addressFromJson(request.content().toText("utf-8"))
              //But the server sets the ID!
              if address.id?
                status := 422 //unprocessable entity
                rtn: Bytes().withUtf8("Do not supply ID in Address, server will set this")
              else
                address.id: AddressId(GUID())
                if ~address?
                  status := 422 //unprocessable entity
                else if repository.addresses() contains address.id
                  status := 409 //conflict
                else
                  repository.addresses() += address

            override contentLocation() as pure
              <- rtn as String: String()
              httpCreated <- 201
              rtn: status == httpCreated <- `/addresses/${address.id}` else String()
            override status() as pure
              <- rtn as Integer: status
            default operator ?

      //A DELETE
      operator -= for :/{id}
        -> id as String //Assume PATH
        <- response as HTTPResponse: (
            addressId: AddressId(id),
            delegate: ByETagHTTPResponse(
              AddressId(id),
              plainNonCacheableHTTPResponse(),
              false
            )
          ) with trait of HTTPResponse by delegate

            repository as Repository!

            override content()
              <- rtn as Bytes: Bytes()
              httpNotFound <- 404
              //Only if the etag was found can we delete it!
              if delegate.status() <> httpNotFound
                repository.addresses() -= repository.addresses().byId(addressId)
                delegate.status(204)
            default operator ?

      //A PATCH which is a merge

      //Note with dynamic variable capture, you can still do it all one one line if you wish.
      operator :~: for :/{id}
        ->
          id as String //Assume PATH
          incomingContent as String :=: CONTENT
        <-
          response as HTTPResponse: (
              addressId: AddressId(id),
              incomingContent: incomingContent,
              delegate: ByETagHTTPResponse(AddressId(id), plainNonCacheableHTTPResponse())
            ) trait HTTPResponse by delegate
            repository as Repository!

            override content()
              <- rtn as Bytes: Bytes()
              httpNotFound <- 404
              if delegate.status() <> httpNotFound
                address <- addressFromJson(incomingContent)
                if ~address.id?
                  delegate.status(422) //unprocessable entity
                else if address.id <> addressId
                  delegate.status(400) //the id on the url is not the same as the id in the body content
                else
                  repository.addresses() :~: address
                  delegate.status(204)
            default operator ?

      //A PUT which is a replace for an existing address
      //Now the dynamic variable capture allowed multiple lines and various formatting within the ()
      operator :^: for :/{id}
        ->
          id as String //Assume PATH
          content as String :=: CONTENT
        <-
          response as HTTPResponse: (
                addressId: AddressId(id),
                incomingContent: content,
                delegate: ByETagHTTPResponse(AddressId(id), plainNonCacheableHTTPResponse())
              ) trait HTTPResponse by delegate

            repository as Repository!

            override content()
              <- rtn as Bytes: Bytes()
              httpNotFound <- 404
              if delegate.status() <> httpNotFound
                address <- addressFromJson(incomingContent)
                if ~address?
                  delegate.status(422) //unprocessable entity
                else if address.id <> addressId
                  delegate.status(400) //the id on the url is not the same as the mainValue in the body content
                else
                  repository.addresses() :^: address
                  delegate.status(204)
            default operator ?

      //Note it is now possible to use named parameters in dynamic variable capture
      //So now, we can do a simpler one liner, define the name of the property and use an expression
      //to set it up, then use the 'by' on a trait to delegate as much or as little as we want to it.
      listAll() for :/
        <- response as HTTPResponse: (
            cacheable: cacheableHTTPResponse()
          ) with trait of HTTPResponse by cacheable

          repository as Repository!

          override etag()
            <- rtn as String: repository.addresses().hash()
          override content()
            <- rtn as Bytes: Bytes().withUtf8(addressListToJSON(repository.addresses().listAll()))
          default operator ?

  defines component

    Repository as abstract

      addresses() as pure abstract
        <- rtn as AddressAccess?

      default operator ?

    InMemoryRepository extends Repository
      addresses as AddressAccess: LockableAddressSet()

      InMemoryRepository()
        addresses += Address(id: AddressId(GUID()), street: "121 Admin Rd.", street2: String(), city: "Concord", state: "NH", zipcode: "03301")
        addresses += Address(id: AddressId(GUID()), street: "67 Paperwork Ave", street2: String(), city: "Manchester", state: "NH", zipcode: "03101")
        addresses += Address(id: AddressId(GUID()), street: "15 Rose St", street2: "Apt. B-1", city: "Concord", state: "NH", zipcode: "03301")
        addresses += Address(id: AddressId(GUID()), street: "39 Sole St.", street2: String(), city: "Concord", state: "NH", zipcode: "03301")
        addresses += Address(id: AddressId(GUID()), street: "99 Mountain Rd.", street2: String(), city: "Concord", state: "NH", zipcode: "03301")

      override addresses() as pure
        <- rtn as AddressAccess: addresses

      default operator ?

  defines application

    //SERVING wiring: this application registers the REST service, so any program using
    //it MUST create an HTTP instance. EK9 enforces that both ways — a program wiring this
    //application without HTTP is E12020 (the service could never receive a request), and
    //an HTTP program wired to an application with no services is E12021.
    AccessPoint
      register InMemoryRepository() as Repository
      register Addresses()

    //COMPONENT wiring only: no service is registered here, so a plain (non-serving)
    //program can use it. Splitting the two is the point — conflating them is what
    //leaves a REST service registered and permanently unreachable.
    RepositoryOnly
      register InMemoryRepository() as Repository

  defines program

    //Uses the component-only application: it exercises the repository and exits, so it
    //must NOT be wired to the service-registering AccessPoint.
    TestAddressOutput with application of RepositoryOnly
      //Expect injection
      repository as Repository!

      stdout <- Stdout()

      address1 <- Address(id: AddressId(GUID()), street: "121 Admin Rd.", street2: String(), city: "Concord", state: "NH", zipcode: "03301")
      address2 <- Address(id: AddressId(GUID()), street: "15 Rose St", street2: "Apt. B-1", city: "Concord", state: "NH", zipcode: "03301")

      addresses <- [address1, address2]

      iter <- addresses.iterator()
      //convert contents in iterator to a list
      theAddresses <- cat iter | collect as List of Address
      require theAddresses?

      stdout.println("Address as JSON")
      stdout.println(addressToJSON(address1))

      jsonAddress <- addressFromJson(addressToJSON(address1))

      stdout.println(`Rebuilt address is [${addressToJSON(jsonAddress)}]`)

      stdout.println("Now arrays")

      stdout.println(addressListToJSON(addresses))

      stdout.println("Done basics now all addresses")

      allAddresses <- repository.addresses().listAll()

      stdout.println(`Hash of all addresses is [${repository.addresses().hash()}]`)

      stdout.println(addressListToJSON(allAddresses))

      stdout.println("Done!")

Download webServices_addresses.ek9