如何为状态机或有限自动机实现RESTful资源

我是一个Rails和REST新手,我试图找出如何最好地公开由具有状态机的域对象支持的资源(换句话说,是一个有限的自动机)。

我已经看到了许多用于使模型类成为状态机的gem,例如aasm,过渡,工作流,但它们都没有记录它们如何在面向资源的控制器中实际使用的示例。 它们似乎都暗示状态转换是由“事件”触发的,这实际上是一种方法调用。 我对这意味着的一些问题是:

  1. 更新操作(PUT方法)不合适,因为PUT被认为是幂等的。 唯一可行的是,如果州是作为代表的一部分发送的。 这与“事件”不一致。 它是否正确?
  2. 由于事件不是幂等的,因此必须使用POST。 但是,到哪个资源? 每个可能的事件都有子资源吗? 或者,是否有一个(/ updatestate)将触发事件和事件的任何参数作为其表示?
  3. 由于资源的状态是由另一个资源可能触发的事件修改的,因此create action是否应该接受对state属性(或依赖于状态机的任何其他属性)的更改?
  4. [更新的问题]在UI中公开转换的好方法是什么? 由于事件不是状态,因此似乎允许更新状态属性(以及依赖于状态转换的任何其他属性)是没有意义的。 这是否意味着在更新操作中应忽略这些属性?

  • 更新操作(PUT方法)不合适,因为PUT被认为是幂等的。 唯一可行的是,如果州是作为代表的一部分发送的。 这与“事件”不一致。 它是否正确?

正确。

  • 由于事件不是幂等的,因此必须使用POST。 但是,到哪个资源? 每个可能的事件都有子资源吗? 或者,是否有一个(/ updatestate)将触发事件和事件的任何参数作为其表示?

你可以两种方式做到这一点。 您可以在同一个应用程序中同时支持,事件类型的变化由传入文档或接收资源确定。 就个人而言,我更愿意通过不同的文档类型来实现,但这只是我的意见。 如果您确实使用了多个资源路由,请确保它们是可发现的(即,通过在获取其父资源时返回到文档中描述的每个资源的链接)。

  • 由于资源的状态是由另一个资源可能触发的事件修改的,因此create action是否应该接受对state属性(或依赖于状态机的任何其他属性)的更改?

由你决定; 没有必要密切关注创作的任何特定属性的真正原因。 (您可以通过说状态在创建后立即更改为状态机的正确初始状态来合理化。)在我已经完成的状态机中,无论​​如何都是通过POST创建的(并且是一个不同的 – 相当复杂 – 所以整个事情都没有实际意义,但如果你允许多个初始状态,那么在创建文档中采用“这是我首选的起始状态”提示是有意义的。 要清楚,仅仅因为用户想要它并不意味着你必须这样做; 是否要在拒绝他们的建议时向用户投诉是您的电话。

  • 项目清单

[股票回答。]

这里的派对有点晚了,但我正在为自己研究这个问题并发现我目前用来管理我的状态机的gem( pluginaweek的state_machine )有一些方法可以很好地处理这个问题。

当与ActiveRecord一起使用时(我也假设其他持久层),它提供了一个#state_event=方法,该方法接受您要触发的事件的字符串表示。 请参阅此处的文档

 # For example, vehicle = Vehicle.create # => # 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.state # => "idling" 

这允许您只需在资源的编辑表单中添加state_event字段,并像更新任何其他属性一样轻松获取状态转换。

现在我们显然仍然使用PUT来使用这种方法触发事件,这不是RESTful。 然而,gem确实提供了一个有趣的例子 ,至少“感觉”非常RESTful,尽管它使用相同的非RESTful方法。

正如您在此处和此处所见,gem的内省function允许您在表单中显示您要触发的事件该事件的结果状态的名称。

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

使用后一种技术,您可以将模型状态的简单基于表单的更新转换为任何有效的下一状态,而无需编写任何额外的代码。 它在技术上并不是RESTful,但它允许您在UI中以这种方式轻松呈现它。

这种技术的清洁性与尝试将基于事件的状态机转换为简单的RESTful资源的固有冲突相结合,足以让我满意,所以希望它也为您提供一些见解。

这里的派对迟到了,而且远离专家,因为我有类似的查询,但……

把活动变成资源怎么样?

而不是……

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

你会…

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

使用Order和OrderEvent或回调之间的发布/订阅侦听器,尝试在Order上触发该事件并记录转换消息。 它还为您提供了所有状态变化事件的便捷审计。

在Shopify从Willem Bergen窃取的想法

我错过了什么吗? 对不起,我自己也在努力理解这一点。

如果您的资源具有某种状态属性,则可以使用称为micro-PUT的技术来更新其状态。

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

您可以将资源状态建模为集合,并在这些集合之间移动资源。

 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 

您始终可以使用新的PATCH方法

 PATCH /Customer/1 Content-Type: application/x-www-form-urlencoded Status=Closed => 200 OK