ruby-on-rails - tutorial - ruby restful
Cómo implementar un recurso RESTful para una máquina de estado o autómatas finitos (4)
Soy un novato de Rails y REST y estoy tratando de encontrar la mejor manera de exponer un recurso respaldado por un objeto de dominio que tenga una máquina de estados (en otras palabras, es un autómata finito).
He visto varias gemas para convertir a una clase modelo en una máquina de estado, como aasm, transiciones, flujo de trabajo, pero ninguna de ellas documenta ejemplos de cómo se usan realmente en un controlador orientado a recursos. Todos parecen implicar que las transiciones de estado son activadas por un "evento", que es realmente una llamada de método. Algunas preguntas que tengo con lo que esto implica son:
- La acción de actualización (método PUT) no es apropiada porque se supone que PUT es idempotente. Lo único que esto sería posible es si el estado fue enviado como parte de la representación. Esto es inconsistet con un "evento". ¿Es esto correcto?
- Como los eventos no son idempotentes, entonces se debe usar un POST. Pero, ¿a qué recurso? ¿Hay un sub-recurso para cada evento posible? O, ¿hay uno (/ updatestate) que toma como representación el evento que se desencadena y algún parámetro para el evento?
- Dado que el estado del recurso se modifica por un evento desencadenado potencialmente por otro recurso, ¿debería la acción de creación aceptar cambios en el atributo de estado (o en cualquier otro atributo que dependa de la máquina de estado)?
- [Pregunta actualizada] ¿Cuál es una buena manera de exponer las transiciones en la interfaz de usuario? Como los eventos no son estados, parece que no tiene sentido permitir que el atributo de estado (y cualquier otro atributo que dependa de las transiciones de estado) se actualice. ¿Eso significa que estos atributos deben ignorarse en la acción de actualización?
- La acción de actualización (método PUT) no es apropiada porque se supone que PUT es idempotente. Lo único que esto sería posible es si el estado fue enviado como parte de la representación. Esto es inconsistet con un "evento". ¿Es esto correcto?
Correcto.
- Como los eventos no son idempotentes, entonces se debe usar un POST. Pero, ¿a qué recurso? ¿Hay un sub-recurso para cada evento posible? O, ¿hay uno (/ updatestate) que toma como representación el evento que se desencadena y algún parámetro para el evento?
Puedes hacerlo de ambas maneras. Puede admitir ambas aplicaciones en la misma aplicación, ya que la variación en los tipos de eventos está determinada por el documento entrante o el recurso receptor. Personalmente, preferiría hacerlo por diferentes tipos de documentos, pero esa es solo mi opinión. Si sigue la ruta de los múltiples recursos, asegúrese de que sean detectables (es decir, haciendo que se devuelvan los enlaces a cada uno de ellos en el documento cuando OBTENGA su recurso principal).
- Dado que el estado del recurso se modifica por un evento desencadenado potencialmente por otro recurso, ¿debería la acción de creación aceptar cambios en el atributo de estado (o en cualquier otro atributo que dependa de la máquina de estado)?
Depende de usted; no hay ninguna razón real por la que tenga que prestar mucha atención a cualquier atributo particular en la creación. (Podría racionalizar esto diciendo que el estado cambia a un estado inicial adecuado para la máquina de estado inmediatamente después de la creación.) En las máquinas de estado que he hecho, la creación fue por POST de todos modos (y de una forma diferente, bastante compleja) documento) por lo que todo era discutible, pero si permite varios estados iniciales, entonces tiene sentido tomar una sugerencia de "este es mi estado de inicio preferido" en el documento de creación. Para que quede claro, solo porque el usuario lo quiera no significa que tengas que hacerlo; Si desea quejarse al usuario cuando rechaza una sugerencia de ellos, es su decisión.
- Elemento de lista
[Respuesta de archivo.]
Poco tarde a la fiesta aquí y lejos de un experto ya que tengo una consulta similar pero ...
¿Qué tal hacer del evento un recurso?
Así que en lugar de ...
PUT /order/53?state_event="pay" #Order.update_attributes({state_event: "pay})
Lo harías...
POST /order/53/pay #OrderEvent.create(event_name: :pay)
POST /order/53/cancel #OrderEvent.create(event_name: :cancel)
Con un pub / sub escucha entre Order y OrderEvent o callback que intenta activar ese evento en Order y registra los mensajes de transición. También le brinda una auditoría útil de todos los eventos de cambio de estado.
Idea robada de Willem Bergen en Shopify
¿Me estoy perdiendo de algo? Lo siento, luchando para entender esto yo mismo.
Si su recurso tiene algún tipo de atributo de estado, puede utilizar una técnica llamada micro-PUT para actualizar su estado.
PUT /Customer/1/Status
Content-Type: text/plain
Closed
=> 200 OK
Content-Location: /Customer/1
Puede modelar estados de recursos como colecciones y mover recursos entre esas colecciones.
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
Siempre se puede utilizar el nuevo método PATCH.
PATCH /Customer/1
Content-Type: application/x-www-form-urlencoded
Status=Closed
=>
200 OK
Un poco tarde para la fiesta aquí, pero estaba investigando este problema exacto y descubrí que la gema que estoy usando actualmente para administrar las máquinas de mi estado ( state_machine by pluginaweek ) tiene algunos métodos que tratan este problema bastante bien.
Cuando se usa con ActiveRecord (y también #state_event=
otras capas de persistencia), proporciona un método #state_event=
que acepta una representación de cadena del evento que le gustaría #state_event=
. Vea la documentación 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"
Esto le permite simplemente agregar un campo state_event
en los formularios de edición de su recurso y obtener transiciones de estado tan fácilmente como actualizar cualquier otro atributo.
Ahora, obviamente, seguimos utilizando PUT para desencadenar eventos utilizando este método, que no es REST. La gema, sin embargo, proporciona un ejemplo interesante que al menos "se siente" bastante RESTful, a pesar de que utiliza el mismo método no RESTful debajo de las coberturas.
Como puede ver here y here , las capacidades de introspección de la gema le permiten presentar en sus formularios el evento que desea activar o el nombre del estado resultante de ese evento.
<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>
Usando la última técnica, obtiene una actualización simple basada en formulario del estado del modelo a cualquier estado próximo válido sin tener que escribir ningún código adicional. No es técnicamente REST, pero te permite presentarlo fácilmente de esa manera en la interfaz de usuario.
La limpieza de esta técnica combinada con los conflictos inherentes al tratar de convertir una máquina de estados basada en eventos en un recurso RESTful simple fue suficiente para satisfacerme, así que espero que también le brinde una idea.