How to implement a RESTful resource for a state machine or finite automata

前端 未结 4 509
误落风尘
误落风尘 2021-01-01 10:33

I\'m a Rails and REST newbie and I\'m trying to figure how best to expose a resource that is backed by a domain object that has a state machine (in other words is a finite a

相关标签:
4条回答
  • 2021-01-01 10:38

    A little late to the party here, but I was researching this exact issue for myself and found that the gem I'm currently using to manage my state machines (state_machine by pluginaweek) has some methods that deal with this issue quite nicely.

    When used with ActiveRecord (and I'm assuming other persistence layers as well), it provides a #state_event= method that accepts a string representation of the event you would like to fire. See documentation here.

    # For example,
    
    vehicle = Vehicle.create          # => #<Vehicle id: 1, name: nil, state: "parked">
    vehicle.state_event               # => nil
    vehicle.state_event = 'invalid'
    vehicle.valid?                    # => false
    vehicle.errors.full_messages      # => ["State event is invalid"]
    
    vehicle.state_event = 'ignite'
    vehicle.valid?                    # => true
    vehicle.save                      # => true
    vehicle.state                     # => "idling"
    vehicle.state_event               # => nil
    
    # Note that this can also be done on a mass-assignment basis:
    
    vehicle = Vehicle.create(:state_event => 'ignite')  # => #<Vehicle id: 1, name: nil, state: "idling">
    vehicle.state                                       # => "idling"
    

    This allows you to simply add a state_event field in your resource's edit forms and get state transitions as easily as updating any other attribute.

    Now we're obviously still using PUT to trigger events using this method, which isn't RESTful. The gem does, however, provide an interesting example that at least "feels" quite RESTful, despite it using the same non-RESTful method under the covers.

    As you can see here and here, the gem's introspection capabilities allow you to present in your forms either the event you would like to fire or the name of that event's resulting state.

    <div class="field">
      <%= f.label :state %><br />
      <%= f.collection_select :state_event, @user.state_transitions, :event, :human_to_name, :include_blank => @user.human_state_name %>
    </div>
    
    <div class="field">
      <%= f.label :access_state %><br />
      <%= f.collection_select :access_state_event, @user.access_state_transitions, :event, :human_event, :include_blank => "don't change" %>
    </div>
    

    Using the latter technique, you get simple form-based updating of the model's state to any valid next state without having to write any extra code. It's not technically RESTful, but it allows you to easily present it that way in the UI.

    The cleanliness of this technique combined with the inherent conflicts in trying to cast an event-based state machine into a simple RESTful resource was enough to satisfy me, so hopefully it provides some insight to you as well.

    0 讨论(0)
  • 2021-01-01 10:43

    Bit late to the party here and far from an expert as I have a similar query but...

    How about making the event a resource?

    So instead of...

    PUT /order/53?state_event="pay" #Order.update_attributes({state_event: "pay})
    

    You would...

    POST /order/53/pay     #OrderEvent.create(event_name: :pay)
    POST /order/53/cancel  #OrderEvent.create(event_name: :cancel)
    

    With a pub/sub listener between Order and OrderEvent or callback that attempts to fire that event on Order and records the transition messages. It also gives you a handy audit of all state change events.

    Idea stolen from Willem Bergen at Shopify

    Am I missing something? Sorry, struggling to understand this myself.

    0 讨论(0)
  • 2021-01-01 10:50

    If your resource has some kind of status attribute, you can use a technique called micro-PUT to update it's status.

    PUT /Customer/1/Status
    Content-Type: text/plain
    
    Closed
    
    => 200 OK
    Content-Location: /Customer/1
    

    You can model resource states as collections and move resources between those collections.

    GET /Customer/1
    =>
    Content-Type: application/vnd.acme.customer+xml
    200 OK
    
    
    POST /ClosedCustomers
    Content-Type: application/vnd.acme.customer+xml
    =>
    200 OK
    
    POST /OpenCustomers
    Content-Type: application/vnd.acme.customer+xml
    =>
    200 OK
    

    You could always use the new PATCH method

    PATCH /Customer/1
    Content-Type: application/x-www-form-urlencoded
    Status=Closed
    =>
    200 OK
    
    0 讨论(0)
  • 2021-01-01 11:02
    • The update action (PUT method) is not appropriate because PUT is suppose to be idempotent. The only this would be possible is if the state was sent as part of the representation. This is inconsistet with an "event". Is this correct?

    Correct.

    • Since, events aren't idempotent, then the a POST must be used. But, to which resource? Is there a subresource for each possible event? Or, is there one (/updatestate) that takes as its representation the event to trigger and any parameters to the event?

    You can do it both ways. You can support both in the same application, with variation in event types being determined by either the incoming document or the receiving resource. Personally, I would prefer to do it by differing document types, but that's just my opinion. If you do go the multiple resources route, make sure they're discoverable (i.e., by having links to each of them described in the document returned when you GET their parent resource).

    • Since the state of the resource is modified by an event triggered potentially by another resource, should the create action accept changes to the state attribute (or any other attributes that are dependent on the state machine)?

    Up to you; there's no real reason why you have to pay close attention to any particular attribute on creation. (You could rationalize this by saying that the state changes to a proper initial state for the state machine immediately after creation.) In the state machines I've done, the creation was by a POST anyway (and of a different – rather complex – document) so the whole thing was moot, but if you allow multiple initial states then it makes sense to take a “this is my preferred starting state” hint in the creation document. To be clear, just because the user wants it doesn't mean you have to do it; whether you want to complain to the user when you reject a suggestion of theirs is your call.

    • List item

    [Stock answer.]

    0 讨论(0)
提交回复
热议问题