How to implement a RESTful resource for a state ma

2019-02-07 18:27发布

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 automata).

I've seen a number of gems for making a model class a state machine, such as aasm, transitions, workflow, but none of them document examples of how they are actually used in a resource oriented controller. They all seem to imply that state transitions are triggered by an "event" , which is really a method call. Some questions I have with what this implies are:

  1. 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?
  2. 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?
  3. 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)?
  4. [Updated question] What is a good way to expose the transitions in the UI? Since events aren't states, it would seem that it doesn't make sense to allow the state attribute (and any other attribute that is dependent on state transitions) to be updated. Does that mean that these attributes should be ignored in the update action?

4条回答
Juvenile、少年°
2楼-- · 2019-02-07 18:40
  • 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.]

查看更多
干净又极端
3楼-- · 2019-02-07 18:48

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
查看更多
做自己的国王
4楼-- · 2019-02-07 18:49

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.

查看更多
迷人小祖宗
5楼-- · 2019-02-07 18:51

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.

查看更多
登录 后发表回答