Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
[!WARNING] Tiny State is currently in early development. Use at your own risk!
Tiny State is a library to add a state machine to any Ruby class. It has a few design goals:
Tiny State is published on Rubygems, so just add it to your Gemfile:
gem "tiny_state"
This example implements a state machine with three states: new
, published
and rejected
. It has two events: publish
and reject
. The publish
event can only be triggered from the new
and rejected
states, and the reject
event can only be triggered from the new
state, so you can't reject a post that is already published.
class Post
# include the TinyState module in your class.
include TinyState
# set up a state attribute, usually this will come from something like your Rails model.
attr_accessor :state
def initialize(state: :initial)
@state = state
end
# define the state machine with the `tiny_state` method.
tiny_state do
state :new # define one state at a time
state [:approved, :published] # or multiple at once
# define each event with the class that handles it.
event :publish, PublishPost
event :reject, RejectPost
end
end
# define the event classes that handle the transitions, they need to inherit from `TinyState::Event`.
class PublishPost < TinyState::Event
# define the transitions this event allows. It can transition from multiple states to a single state.
transitions from: %i[new rejected], to: :published
end
class RejectPost < TinyState::Event
transitions from: :new, to: :rejected
end
tiny_state
takes a block and only defines the possible states and events. The state
methods defines possible states, and the event
method defines possible events. The event class is passed as a second argument.
Note that the state
attribute itself is not defined by Tiny State. You need to define it yourself in your class or model. There is also no default value set by Tiny State.
This will then add the following event methods to a Post
instance.
#publish?
and #publish!
#reject?
and #reject!
The #question_mark?
methods will return true
or false
depending on whether the transition is allowed. These methods are also used internally to check if we can transition the state.
The #bang!
methods will raise an exception if the transition is not allowed, if it is they will change the state
attribute on the instance to the new value. This will not trigger any database update or other side effects, that is left up to you to implement.
You can define states with the state
method, eiter individually or multiple states at once. States are deduplicated automatically.
class Post
include TinyState
tiny_state do
state :new
state [:approved, :rejected]
end
end
You can define multiple state machines on a single class, each with their own attribute. If you redefine a state machine, the previous one is overwritten.
You define events with the event
method. Each event is defined with a class that handles that event.
class Post
include TinyState
tiny_state do
# ...
event :publish, PublishPost
end
end
This class should inherit from TinyState::Event
. The Event class takes one configuration that defines which transitions it allows. This is checked before transitioning the state. If the transition is not allowed, an TinyState::InvalidTransitionError
exception is raised.
You can define multiple transitions with the transitions
method. This takes a from
and to
keyword argument. The from
argument can be a single state or an array of states, and the to
argument is the (single) state the event transitions to.
class PublishPost < TinyState::Event
transitions from: [:new, :rejected], to: :published
end
To be able to peek inside the state machines, the #tiny_state_machines
and #tiny_state_machine
methods are added to the instance of your class.
#tiny_state_machines
returns a hash with the field as the key and a TinyState::Machine
instance as the value, which contains the defined states and events.
As a shortcut the singular #tiny_state_machine
is also defined, which returns the machine for the first state machine you define or the machine for a specific field if you give it an argument.
post = Post.new # from the example above
post.tiny_state_machine.states
# => #<TinyState::StateMachine:0x00000000000ff0 :state, states: [:initial, :approved, :rejected], events: [:approve, :reject]>
post.tiny_state_machine(:state)
# => #<TinyState::StateMachine:0x00000000000ff0 :state, states: [:initial, :approved, :rejected], events: [:approve, :reject]>
The TinyState::StateMachine
instance has the following methods:
#attribute
returns the attribute the state machine is defined on.#states
returns an array of the defined states.#events
returns a hash with the event names as keys and the event classes as values.post = Post.new # from the example above
post.tiny_state_machine.states
# => [:new, :published, :rejected]
post.tiny_state_machine.events
# => [:publish, :reject]
post.tiny_state_machine.attribute
# => :state
By default Tiny State uses the state
field on your resource. If you want to use a different field, you can supply that as an option when defining the state machine:
class Post
# ...
tiny_state :status do
# ...
end
end
If you want to extend the logic that allows a transition, you can do so by overriding the #transition?
method in the event class. Be sure to always call super
to ensure the transition state checks defined in the event itself are also performed.
class PublishPost < TinyState::Event
# ...
def transition?
# Add your own logic here, for example:
super && some_other_condition?
end
private
def some_other_condition?
# ...
end
end
If you want to implement side effects before or after a transition, you can do so by overriding the #transition!
method in the event class. Be sure to always call super
to ensure the transition is allowed and the state
is changed.
class PublishPost < TinyState::Event
# ...
def transition!
super
# Add your own logic here, for example:
log_publication!
end
private
def log_publication!
# ...
end
end
If you don't call super
in your transition!
method, it won't check if the transition is allowed and won't change the state
attribute. If you are fine with that, you can still call change_state!
to change the state at the moment you want.
Note: if you plan to have any asynchronous side effects like sending emails or queueing jobs, you should make sure they are fired after the resource is saved and the transaction is committed. This is because the state change is not persisted until the transaction is committed. I recommend to use the after_commit_everywhere gem for this.
The object itself is always referenceable through resource
. This can be used if you want to access the object in the event class.
class PublishPost < TinyState::Event
# ...
def transition!
# ...
resource.published_at = Time.now
# ...
end
end
If you want to use a different name for the resource, for example to match the type of resource you pass in, you can alias the method or define a simple one line method that refers to resource
.
class PublishPost < TinyState::Event
# ...
alias_method :post, :resource
# or
def post = resource
end
Bug reports and pull requests are welcome on GitHub at https://github.com/djfpaagman/tiny_state.
FAQs
Unknown package
We found that tiny_state demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.