sunny / actor Goto Github PK
View Code? Open in Web Editor NEWComposable Ruby service objects
License: MIT License
Composable Ruby service objects
License: MIT License
Hi, it seems that setting an hash as a default value for an input results in a ServiceActor::ArgumentError
.
Here's an example, slightly changed from the example in the readme:
class BuildGreeting < Actor
input :name
# Here's the change: the original example used a string as a default.
# input :adjective, default: "wonderful"
input :adjective, default: { text: "wonderful" }
input :length_of_time, default: -> { %w[day week month].sample }
input :article,
default: lambda { |context|
puts context.adjective[:text]
puts context.adjective[:text].match?(/^[aeiou]/)
/^[aeiou]/.match?(context.adjective[:text]) ? "an" : "a"
}
output :greeting
def call
self.greeting = "Have #{article} #{adjective[:text]} #{length_of_time}, #{name}!"
end
end
actor = BuildGreeting.call(name: "Jim")
puts actor.greeting # => I'd expect "Have a wonderful week, Jim!", but I get a `ServiceActor::ArgumentError`
Wrapping the default value in a lambda does work, but I wouldn't expect to need such a workaround:
# This works, but it's kinda ugly
input :adjective, default: -> { return { text: "wonderful" } }
Thanks and keep up the great work!
Given the following actor
class TestActor < Actor
input :value, type: Integer
output :value_result, type: Integer, allow_nil: true
end
Evaluating TestActor.call(value: 1)
leads to
ServiceActor::ArgumentError:
The "value_result" input on "TestActor" is missing
(note the "input" while value_result
is defined as output)
It does not happen if you define output without allow_nil: true
Would'nt it be great to be able to specify a collection of admissible values for certain inputs ?
For instance, you could have:
class BuildGreeting < Actor
input :name
input :adjective, default: 'wonderful', in: ['brillant', 'wonderful', 'smart', 'dazzling', 'blazing']
input :length_of_time, default: -> { ['day', 'week', 'month'].sample }
output :greeting
def call
self.greeting = "Have a #{adjective} #{length_of_time} #{name}!"
end
end
Or, if you defined your GREAT_ADJECTIVES_LIST
anywhere else in the code,
class BuildGreeting < Actor
input :name
input :adjective, default: 'wonderful', in: GREAT_ADJECTIVES_LIST
input :length_of_time, default: -> { ['day', 'week', 'month'].sample }
output :greeting
def call
self.greeting = "Have a #{adjective} #{length_of_time} #{name}!"
end
end
Hi there,
First of all, thank you for your work. I find it really cool and looks like a good solution to improve basic service objects.
My question here is: why can't we pass directly constants to check the type of inputs and should pass it as string instead ?
like this:
class GreetUser < Actor
input :user, type: User
def call
puts "Hello #{user.name}!"
end
end
I didn't find any reason in the code of the gem or I am ignorant about a deeper problem.
All I found about this is in lib/actor/type_checkable.rb
:
types = Array(type_definition).map { |name| Object.const_get(name) }
which confirms to me we could pass directly constant instead of String since you "constantize" it anyway.
looking forward to your reply,
thanks !
Pagehey
First of all, thanks for the great work! π―
In interactor with the interactor-contracts gem, it's possible to validate a hash like this:
expects do
required(:user).filled(type?: User)
required(:params).schema do
required(:email).filled(:str?)
required(:first_name).filled(:str?)
optional(:middle_name)
end
end
Is there a similar way? Or a recommended way of achieving the same thing?
After upgrading to 3.9.0, I noticed something odd is happening. I managed to simplify it to the following MRE:
# frozen_string_literal: true
# some other actor defined somewhere
Another = Class.new(ApplicationActor) do
output :outcome
def call
p "ENTER"
self.outcome = nil
end
end
class TestActor < ApplicationActor
play Another
end
Invoking it shows Another#call
was executed twice:
[3] pry(main)> TestActor.result;
"ENTER"
"ENTER"
And on 3.8.1:
[2] pry(main)> TestActor.result;
"ENTER"
I think this is a critical bug -- I was extremely lucky to notice it before deploying to production. In my case it was doubled external API calls..
In a project I'm working on, we've taken to using actors as a way to isolate and encapsulate complex bits of fetching logic. For example:
class GetVersionChanges < ApplicationActor
input :version
input :filters, default: {}
output :output
def call
# complicated database calls
# version_changes = ...
this.output = version_changes
end
end
GetVersionChanges.call(version:, filters:).output
Calling .call().output
for these gets a bit tiring as use of the pattern grows throughout the app. What I'd like to do is something like this:
class GetVersionChanges < ApplicationActor
input :version
input :filters, default: {}
def call
# complicated database calls
version_changes # the return value is picked up and returned by `output_of`
end
end
GetVersionChanges.output_of(version:, filters:)
In fact, I've monkey-patched the gem with the following to get this behavior in our application:
module ServiceActor::Core
module ClassMethods
# Get the return value of calling an actor with arguments.
#
# CreateUser.output_of(name: "Joe")
def output_of(result = nil, **arguments)
call(result, **arguments)[:_default_output]
end
end
def _call
result._default_output = call
end
end
It's rudimentary - "polluting" the result object with a _default_output
attribute - but it works. Let me know if 1) this feature is a good fit for the gem and 2) if this implementation is sufficient, or if something more sophisticated is desired. Thanks!
Hi, i am a user come from interactor
, i use this gem for all my service , until, i meet a issue, i want a guarantee to ensure pass correct name input into my service, so, i switch to use service_actor, one if the reason is, this gem not use 'activesupport'.
Following is my code:
I define my service like this:
class RetrieveStocks < Actor
input :sort_column
input :sort_direction
input :page, default: '1'
input :per, default: '20'
input :stock_name
def call
# ...
end
end
Then, invoke my service in my roda routers, like this:
sort_column, sort_direction, stock_name, page, per = r.params.values_at('sort_column', 'sort_direction', 'stock_name', 'page', 'per')
result = RetrieveStocks.(
sort_column: sort_column,
sort_direction: sort_direction,
page: page,
per: per,
stock_name: stock_name,
)
Following is my issue, because above page
, per
is come from request params,
so, them maybe string, or nil(when user not use paginate), when pass both
of them into service, if request not use paginate, will pass page and per as nil.
like following:
result = RetrieveStocks.(
sort_column: sort_column,
sort_direction: sort_direction,
page: nil,
per: nil,
)
For this case, what i want is, use page/per default value, but, it not work.
AFAIK, the only solution is, run compact!
before passed in.
result = RetrieveStocks.({
sort_column: sort_column,
sort_direction: sort_direction,
page: nil,
per: nil,
}.compact!)
But i consider this solution is not good, so, just curious, what is the expected way to do this?
Thank you.
EDIT:
I workaround this issue use following code.
params = r.params.slice('sort_column', 'sort_direction', 'stock_name', 'page', 'per')
result = RetrieveStocks.(params)
It works anyway, though, i still curious, how to handle this case when use with actor.
I struggle to figure out in which case DefaultCheck is used.
Seems like return if @result.key?(@input_key)
(lib/service_actor/checks/default_check.rb:54) is always evaluated to true
(both in specs and real-world scenarios).
Defaultable
(which runs first) sets the default is requiredNiCheck
(runs second) raise if nilDefaultCheck.check
is fired and has nothing to doAm I missing something?
Hey @sunny !!!!
Thanks for the great work \o/
@AnneSottise and I were wondering what were you thinking about adding aliases to inputs
and ouputs
, such as:
input :description, type: String, default: nil, as: :aliased_description
Wouldn't it be great ?
While doing:
bin/rake
The following warnings appear:
/Users/afuno/Projects/GitHub/afuno/actor/lib/service_actor/playable.rb:46: warning: method redefined; discarding old orig_name=
/Users/afuno/.rbenv/versions/2.7.4/lib/ruby/2.7.0/ostruct.rb:191: warning: previous definition of orig_name= was here
/Users/afuno/Projects/GitHub/afuno/actor/lib/service_actor/playable.rb:46: warning: method redefined; discarding old name=
/Users/afuno/.rbenv/versions/2.7.4/lib/ruby/2.7.0/ostruct.rb:191: warning: previous definition of name= was here
Hello.
After updating to version 3.8.0, a large number of project services in RSpec began to crash with this comment:
DEPRECATED: Invoking undefined methods on `ServiceActor::Result` will lead to runtime errors in the next major release of Actor.
In one of the examples, the service was mocked using allow
and the expected result was indicated:
allow(MyService).to(
receive(:result)
.and_return(
ServiceActor::Result.new(failure?: true, error: "Some error")
)
)
But at the same time, when actually checking the result of the service, the following data appears:
service_result.failure?
# => false
service_result.success?
# => true
Please tell me what I'm doing wrong and what happened?
How to pass in positional arguments, and no keyword arguments? And at the same time keep the input validation, i.e. input :user, type: User
. Is that possible to do here? And, preferably, painlessly. :)
First of all, thank you for the work in this lib! I've found it very useful and I'm giving it a go in a personal project to play around.
I've found a hurdle that I would appreciate if you can provide some tips on how to proceed, hopefully, it could help others as well.
I see play
as a way of dependency injection, although inside of an actor I would like to call another actor and perform some logic basic on inputs and outputs.
class ServiceClient < Actor
input :token, allow_nil: false
input :term, allow_nil: false
def call
Faraday.new('http://example.com/service').get("", query: term)
end
end
class Persistor < Actor
input :client, default: ServiceClient
def class
outcome = ServiceClient.result(token: 'token', term: 'actor')
puts outcome.status if outcome.success?
end
end
The problem is when I use input :client, default: ServiceClient
it tries to initialize the class and then failed because of the missing required arguments. If I move provider
as a private method it works.
Is there any way to achieve dependency injection?
When the if:
condition is used on play
, the lambda is evaluated once per actor, when it should only be evaluated once before playing the whole group.
class PrintThings < Actor
play -> _ { puts "one" },
-> _ { puts "two" },
-> _ { puts "three" },
if: -> _ { puts "should be evaluated only once"; true }
end
PrintThings.call
Current output of calling this actor:
should be evaluted only once
one
should be evaluted only once
two
should be evaluted only once
three
Expected:
should be evaluted only once
one
two
three
See following backtrace.
TypeError: exception class/object expected
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/service_actor-3.6.0/lib/service_actor/result.rb:22:in `raise'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/service_actor-3.6.0/lib/service_actor/result.rb:22:in `fail!'
/home/common/Stocks/marketbet_crawler/app/services/retrieve_jin10_message.rb:42:in `call'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/service_actor-3.6.0/lib/service_actor/core.rb:48:in `_call'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/service_actor-3.6.0/lib/service_actor/checkable.rb:13:in `_call'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/service_actor-3.6.0/lib/service_actor/defaultable.rb:53:in `_call'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/service_actor-3.6.0/lib/service_actor/failable.rb:33:in `_call'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/service_actor-3.6.0/lib/service_actor/core.rb:16:in `call'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/service_actor-3.6.0/lib/service_actor/core.rb:26:in `result'
app/routes/jin10/messages.rb:4:in `block (2 levels) in <class:App>'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/plugins/hash_paths.rb:120:in `block in hash_paths'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/request.rb:536:in `always'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/plugins/hash_paths.rb:120:in `hash_paths'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/plugins/hash_routes.rb:296:in `hash_routes'
/home/common/Stocks/marketbet_crawler/app/app.rb:147:in `block (3 levels) in <class:App>'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/plugins/_optimized_matching.rb:140:in `block in _is1'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/request.rb:536:in `always'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/plugins/_optimized_matching.rb:140:in `_is1'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/plugins/_optimized_matching.rb:25:in `is'
/home/common/Stocks/marketbet_crawler/app/app.rb:146:in `block (2 levels) in <class:App>'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/request.rb:536:in `always'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/plugins/_optimized_matching.rb:124:in `_verb'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/request.rb:104:in `get'
/home/common/Stocks/marketbet_crawler/app/app.rb:103:in `block in <class:App>'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda.rb:518:in `_roda_run_main_route'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/plugins/_before_hook.rb:27:in `_roda_run_main_route'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda.rb:496:in `block in _roda_handle_main_route'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda.rb:494:in `catch'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda.rb:494:in `_roda_handle_main_route'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda/plugins/error_handler.rb:88:in `_roda_handle_main_route'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/roda-3.64.0/lib/roda.rb:380:in `block in base_rack_app_callable'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/rack-test-2.0.2/lib/rack/test.rb:358:in `process_request'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/rack-test-2.0.2/lib/rack/test.rb:165:in `custom_request'
/home/common/.rvm/gems/ruby-3.2.1@marketbet_crawler/gems/rack-test-2.0.2/lib/rack/test.rb:114:in `get'
/home/common/.rvm/rubies/ruby-3.2.1/lib/ruby/3.2.0/forwardable.rb:240:in `get'
/home/common/Stocks/marketbet_crawler/test/routes/jin10/messages_test.rb:6:in `block (2 levels) in <top (required)>'
The source code which raise this error is here
Thanks.
There is this code in the application:
class ApplicationService
include ServiceActor::Base
end
class CurrencyService < ApplicationService
input :currency,
type: String,
inclusion: {
in: %w[USD EUR],
message: ->(value:, **) { "Incorrect currency: #{value}" }
}
end
When using it:
CurrencyService.call(currency: :GBP)
Gives an error:
ServiceActor::ArgumentError: Incorrect currency: GBP
That is, the check for the type of the input value is ignored.
My tests started to fail after upgrading to 3.8.x because changes on Result.
My code before 3.8.x:
allow(MyActor).to receive(:result).and_return(ServiceActor::Result.new(failure?: true))
result = MyActor.result
result.success? # => false
result.failure? # => true
After upgrading to 3.8.x:
allow(MyActor).to receive(:result).and_return(ServiceActor::Result.new(failure?: true))
result = MyActor.result
result.success? # => true
result.failure? # => false
I think what caused the problem was this line: c73d688#diff-6d7487eb2415132d6b0752596159f369f60fc82446b1a372ea0d5841fed7678cL25
Fixing things on my side is easy, I have to change all ServiceActor::Result.new(failure?: true, ...)
by ServiceActor::Result.new(failure: true, ...)
, but I'm reporting here because it could be a problem for more people.
after i read output section in README, i have a question.
do we need this? i found it works as interactor, following is a example
class RetrieveStocks < Actor
input :stock_name, default: nil
def call
stocks = Stock.where(:stocks[:name] => stock_name)
if stocks.empty?
fail!(message: "no stocks!")
else
# we can assign use a setter like this, why we need output?
result.stocks = stocks
end
end
end
params = r.params.slice('sort_column', 'sort_direction', 'stock_name', 'page', 'per')
result = RetrieveStocks.call(params)
if result.success?
# get assigned value, it works
@stocks = result.stocks
end
Thank you.
Hi, I'm using sunny/actor on my projects and I like it a lot, thanks for this awesome project.
I'm doing some experiments with pattern matching + Result but I noticed some issues, for example:
My expectation:
case CreateUser.result(attributes: user_params)
in {success: true, user:}
redirect_to user_path(user), notice: "success"
in {error: :invalid_record, user:}
render "new", locals: { user: user }
in {error: :other_error}
# do something else
end
But it can't be achieve because:
case CreateUser.result(attributes: user_params)
To have acess to succes/failure key, I need to use .to_h
on result
in {success: true, user:}
result.to_h
doesn't return success
key, and returns failure?
key only if failure?
is true
So I have to write something like:
result = CreateUser.result(attributes: user_params)
case result.to_h
in {failure?: true, error: :invalid_record, user:}
render "new", locals: { user: user }
in {error: :other_error}
# do something else
in {user:}
redirect_to, user_path(user), notice: "success"
end
I'm not an expert on Pattern Matching, so I'm not sure if I'm on the right way. So I tried adding a #deconstruct_keys
to ServiceActor::Result and it worked fine:
def deconstruct_keys(keys)
to_h.merge(success: success?, failure: failure?)
end
What do you think? If you are interested on this, I can try opening a PR with a first take on this.
This is a feature idea for using Ruby symbols as play
arguments.
Sometimes, we need to interlace small actions with larger ones inside a play. For that, instead of giving it actors we can give it lambda actions.
For example :
class PlaceOrder < ApplicationActor
input :order
input :log, default: true
play -> actor do
actor.order.currency ||= Current.currency
actor.order.save! if actor.order.changes.any?
end,
Orders::Create,
Orders::Notify,
-> actor { Logger.log("Order #{actor.order.id} created") if actor.log }
end
In some cases it could be nice to be able to give it instance methods as well:
class PlaceOrder < ApplicationActor
input :order
input :log, default: true
play :set_current_currency,
Orders::Create,
Orders::Notify,
:log_order_creation
private
def set_current_currency
order.currency ||= Current.currency
order.save! if order.changes.any?
end
def log_order_creation
Logger.log("Order #{order.id} created") if log
end
end
It's not pleasant to type full keyword arguments all the time for actors with only one or two inputs. So I figured out a way to run actor with positional arguments:
class ApplicationActor < Actor
class << self
attr_reader :defined_run_args
# Define positional args for Actor.run()
# Should be called after all inputs defined
#
# class HiActor < ApplicationActor
# input :planet
# input :name
#
# has_run_args :name, :planet
#
# def call
# puts "Hi, #{name} from #{planet}"
# end
# end
#
# HiActor.run!('Dan', 'Earth')
#
def has_run_args(*args)
if args.size != inputs.keys.size
raise ServiceActor::ArgumentError, 'Run args must match inputs'
end
const_set('RUN_ARGS', args.freeze)
@defined_run_args = true
end
def run!(*args)
raise ServiceActor::ArgumentError, 'Actor run args not defined!' unless defined_run_args
args_h = self::RUN_ARGS.map.with_index { |key, idx| [key, args[idx]] }.to_h
call(args_h)
end
end
end
Just a small run!
method built on top of call
.
Is positional arguments support on the roadmap of Actor?
Hey @sunny !!
Thanks for the amazing work :D
I have an idea, regarding inputs
and outputs
.
What do you think about accepting a new notation when expecting an array of similar objects, such as:
inputs :orders, type: [Orders]
(Similar to the GraphQL notation)
This way it will be easier to type arrays.
We got tones of tests crashing with 3.8.0 update. It's be great to have some release notes for the new version with a summary of what has changes, some deprecations and reasoning behind them and maybe some migration paths described as well.
As reported by @Buzzkill-McSquare, it would be better for checks to raise an error if the :message
option isnβt provided. (Or make sure it uses the default.)
This should probably wait for #97 to be merged.
Sometimes we instantiate objects manually in the actor, from a main input. To do that, most of the time we override the call method in the players, or create attr_readers
from the main input.
What do you think about adding a declarative syntax to avoid being forced to override call
in such use cases ?
An actor looking like this
module Projects
class Confirm < Actor
input :project, type: Project
def call
result.owner = project.owner
result.rewards = project.rewards
super
end
play ConfirmOwner, ConfirmRewards, ConfirmProject
end
end
Could be simplified like this for instance :
module Projects
class Confirm < Actor
input :project, type: Project
declare :owner, :rewards, on: :project
play ConfirmOwner, ConfirmRewards, ConfirmProject
end
end
Ruby MRI 3.3 throws performance-related warnings:
gems/service_actor-3.7.0/lib/service_actor/result.rb:11: warning: OpenStruct use is discouraged for performance reasons
Can we refactor ServiceActor::Result
so that it doesn't inherit from OpenStruct
?
Hey :)
I wonder if there is a way to execute some callback code before / after / around the call method of an actor.
My use case is the following:
ApplicationActor
(which simply has include ServiceActor::Base
)I tried to add to ApplicationActor
the following:
class << self
def call(**args)
ActiveSupport::Notifications.instrument("ApplicationActor", extra: self) do
super
end
end
end
but it gets executed only on the call of the "main" actor.
Any idea?
If I have an input
that has both the default
and in
attribute, and I call the actor without that input, I would expect the default to be used. Instead I get a ServiceActor::ArgumentError
because my lack of input doesn't satisfy the in:
property.
class MyActor < Actor
input :word, default: "one", in: ["one", "two", "three"]
...
end
When calling this actor like MyActor.call
, I would expect the word
variable to be set to "one"
.
The gem requires Ruby >= 2.4
Line 35 in 0c8717e
but that versions EOLed 3 years ago, I'd suggest to bump required_ruby_version
to >= 3.0
or at least `>= 2.7'
Hi.
Have a question.
Where create type class?
What is the content of a type class?
I'd love to be able to use other input values in the Proc
form for default:
input :foo, type: [String], allow_nil: false
input :bar, type: [String], default: ->(inputs) { "Default Bar: #{inputs[:foo]}" }
You probably don't want the Proc
to be able to modify inputs
, so we should pass a defensive copy:
def default_for_normal_mode_with(result, key, default)
if default.is_a?(Proc)
args = []
args.push(result.dup) if default.arity != 0
default = default.call(*args)
end
result[key] = default
end
If you're interested in this, I'm happy to open a pull request.
Hey,
Thank you very much for this small yet precious gem. I really like how simple it is and working with it.
I had this idea for quite some time. Only recently i was able to pull it off.
So, i had an actor that we either creating or updating a record. I had a output :new_record
on the actor. Moreover, i could access it using ActorClass.result.new_record
.
However, i wanted it to give the impression that this accessor(new_record
) is a boolean output. In accordance with Ruby's best practices for naming boolean methods and postfixing them with a ?
character. I added some logic to support that. I would like your opinion. And, maybe shipping this behaviour by default in this gem.
For starters, i have my own ApplicationActor
that inherits from Actor
. Which looks like this
class ApplicationActor
include ServiceActor::Base
def self.call(options = nil, **arguments)
result = super options, **arguments
result.each_pair do |key, value|
if outputs.dig(key, :boolean)
result.define_singleton_method "#{key}?", -> { value }
end
end
end
end
I'm adding some extra behaviour to Actor.call
static method. Simply calling it. Then for each key in the result struct i determine if it has the :boolean
argument.
To tell Application actor to generate these methods. It can be like so
class AnyActor < ApplicationActor
output :new_record, boolean: true
end
Am i doing anything wrong here?.
Thank you for your work on this gem and hope this helps someone trying to do the same.
Face Γ vos pff ?
There seems to be something wrong with zeitwerk
in release 3.4.0. The application started crashing instantly after updating the gem version from 3.3.0 to 3.4.0.
Failure/Error: require File.expand_path("../config/environment", __dir__)
NameError:
uninitialized constant Actor::ServiceActor
include ServiceActor::Base
^^^^^^^^^^^^
# /usr/local/bundle/gems/service_actor-3.4.0/lib/service_actor.rb:7:in `<class:Actor>'
# /usr/local/bundle/gems/service_actor-3.4.0/lib/service_actor.rb:6:in `<main>'
The application code, especially that associated with Actor, has not changed.
Now I'm trying to figure out what's wrong.
I'm trying to fake a service actor result in a Rails request spec. The test I would like to write looks like this:
context 'when search fails (e.g. because of HTTP 503 on Google side)' do
let(:params) { { query: 'Darkstorm Galaxy' } }
before do
allow(Google::AutocompletePlaces).to receive(:result).and_return(
instance_double(ServiceActor::Result, success?: false, error: 'lol')
)
end
it 'returns suggestions' do
get api_authenticated_address_search_suggestions_path, params: params, headers: auth_headers
expect(response).to have_http_status(:service_unavailable)
expect(response.parsed_body).to include('application_errors' => be_a(String))
end
end
But this doesn't work because error
is derived from method_missing
. And I shouldn't use double
because of RSpec best practices enforced by rubocop.
What's your take on that?
This article by @pepawel gives an interesting way to remove the Zeitwerk dependency in production and replace it with a script that builds a loader.
It sounds like an interesting idea, so if anybody wants to tackle this, it should be rather straightforward by following the article and make the ServiceActor gem a bit more lightweight and easy to install πͺΆ
See also this discussion.
The new Playable.alias_input
is documented in the README but is not available in 3.4.1, the latest release.
The docs also reference both alias_input
and input_alias
. The former exists on main
. The latter doesn't exist anywhere.
Hello. Is it possible to add support for ruby 2.3?
I need to use fail_on but my project uses ruby 2.3
Uploading to ruby 2.4 is in the backlog. Apparently we will do it in a few months.
My alternative is to fork and upload a gem momentarily.
I can also fork and use github on the Gemfile but I don't like the idea.
β― bundle install ξ² 100% β ββ―
Bundler 2.4.0.dev is running, but your lockfile was generated with 2.1.4. Installing Bundler 2.1.4 and restarting using that version.
Fetching gem metadata from https://rubygems.org/.
Fetching bundler 2.1.4
Installing bundler 2.1.4
Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. Please call `DidYouMean.correct_error(error_name, spell_checker)' instead.
Fetching gem metadata from https://rubygems.org/.........
Using rake 13.0.6
Using ast 2.4.2
Using bundler 2.1.4
Using parallel 1.22.1
Using parser 3.1.2.0
Using rainbow 3.1.1
Fetching regexp_parser 2.3.1
Installing regexp_parser 2.3.1
Using rexml 3.2.5
Fetching rubocop-ast 1.17.0
Installing rubocop-ast 1.17.0
Using ruby-progressbar 1.11.0
Using unicode-display_width 2.1.0
Using rubocop 1.28.2
Fetching code-scanning-rubocop 0.5.0
Installing code-scanning-rubocop 0.5.0
Using coderay 1.1.3
Fetching diff-lcs 1.4.4
Installing diff-lcs 1.4.4
Fetching interactor 3.1.2
Installing interactor 3.1.2
Using method_source 1.0.0
Using pry 0.14.1
Fetching rspec-support 3.10.3
Installing rspec-support 3.10.3
Fetching rspec-core 3.10.1
Installing rspec-core 3.10.1
Fetching rspec-expectations 3.10.1
Installing rspec-expectations 3.10.1
Fetching rspec-mocks 3.10.2
Installing rspec-mocks 3.10.2
Fetching rspec 3.10.0
Installing rspec 3.10.0
Fetching rubocop-performance 1.12.0
Installing rubocop-performance 1.12.0
Fetching rubocop-rspec 2.6.0
Installing rubocop-rspec 2.6.0
Using service_actor 3.1.3 from source at `.`
Bundle complete! 9 Gemfile dependencies, 26 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
Similarly, when running bin/rake
β― bin/rake ξ² 100% β ξ³ 144 Mbps WiFi ββ―
Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. Please call `DidYouMean.correct_error(error_name, spell_checker)' instead.
/Users/pboling/.asdf/installs/ruby/3.1.2/bin/ruby -I/Users/pboling/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/lib:/Users/pboling/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-support-3.10.3/lib /Users/pboling/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.10.1/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb
Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. Please call `DidYouMean.correct_error(error_name, spell_checker)' instead.
Randomized with seed 5086
..................................................................................
Finished in 0.02617 seconds (files took 0.16946 seconds to load)
82 examples, 0 failures
Following is a example:
class InhibitMethodRedefinedWarning < Actor
input :page, default: 1, type: [Integer, String]
def call
result.page = page
end
private
def page
result.page.to_i
end
end
when i run test, get many warn message like this:
/home/zw963/Stocks/marketbet_crawler/app/services/retrieve_insider_history.rb:97: warning: method redefined; discarding old days
/home/zw963/others/.rvm/gems/ruby-3.0.2@marketbet_crawler/gems/service_actor-3.1.2/lib/service_actor/attributable.rb:26: warning: previous definition of days was here
If this gem expect user override input same name method this way, it should do some hack for avoid produce those warning.
the offical recommand way for this is to add a alias. as following code, (i copy from sequel gem).
you can check discuss here.
https://groups.google.com/g/sequel-talk/c/fwa1Pwye_CY/m/DANKiBGOBQAJ
And this hack should fix many warnings when run rspec --warning
for gem spec.
First, great work with the gem!
I was wondering: are these attributes supported at the same time?
Given the scenario that I have an interactor that looks like this:
class MyInteractor < Actor
# weekdays is an Array of strings
input :weekdays, type: Array, must: {
be_valid: ->(weekdays) { weekdays.all?(SomeValidation) }
}, default: DefaultWeekdays, allow_nil: true
end
In this scenario, weekdays
is optional. If this input is not sent, it defaults to something else. The issue I am having at the moment is that it can also be nil
and that would also trigger the usage of the defaults.
I get a ServiceActor::ArgumentError
, because in the must:
validation weekdays is nil
-- fixing it implies in using weekdays.nil? || ...
to allow this optional scenario.
Is that expected or am I missing something here?
Again, thanks for the great work π
#138 made Result
class inherit from BasicObject
. One thing I missied is that now it's impossible to call tap
/then
on service result:
SomeActor.call.tap { do_something(_1) }
SomeActor.call.then { _1.some_output = ... }
I think we can add these methods because collisions here are unlikely to happen and it seems reasonable in general
I use Actors to run complex business logic, often from console or a rake task. I'd love to be able to integrate prompts without needing to think about how to integrate.
It would only be a few lines of code either way, but it seems like a helpful enough ability to provide it to everyone.
https://github.com/piotrmurach/tty-prompt
One downside is adding a new dependency to this gem. If that's too big an ask, then I can make a plugem called service_actor-promptable
Thank you for this nice gem! π
In past I used the Interactor gem quite a lot, I recently switched to Actor and I find it a nice improvement.
As in the subject: perhaps it could be nice to suggest some testing strategies for actors.
For example, with RSpec I recently wrote something like this:
RSpec.describe ParseFileName, type: :actor do
describe ".result" do
subject(:result) { described_class.result(file_name:) }
context "with a valid file_name" do
let(:file_name) { "sample_file_name_20221229010101.csv" }
it { expect(result).to be_a_success }
it "parses the file_name and returns an Hash" do
expected_hash = {
prefix: :sample,
name: "file_name",
timestamp: Time.zone.strptime("20221229010101", "%Y%m%d%H%M%S")
}
expect(result.formatted_attributes).to eq expected_hash
end
end
context "with an invalid file_name" do
let(:file_name) { "wrong_name.csv" }
it { expect(result).to be_a_failure }
it { expect(result.error).to eq "Invalid format for \"#{file_name}\"" }
end
end
end
In this case I preferred to use result
to avoid checking exceptions.
WDYT?
please check following example.
# i assign result.institutions = ??? in this service.
result = RetrieveInstitutions.result(params)
if result.success?
@institutions = result.institutions1 # But i never defined this method in above service, but when invoke it, not raise any error, just return nil, this is not acceptable?
end
Above result.institutions1 never raise any error, just return nil, which make result.success?
almost no any means for this case.
Create a https://github.com/sunny/actor/wiki to receive pull request for documentation.
For example:
More examples using if:
.
:)
class OutputWithBlacklistedName < Actor
input :value, type: Integer
output :object_id, type: Integer
play -> actor { actor.object_id = actor.value.succ }
end
OutputWithBlacklistedName.call(value: 1).object_id != 2
Before #127 object_id would be correctly set (I doubt that overriding object_id
is a good idea though).
One possible solution is to inherit Result
from BasicObject and explicitly disallow output
names that collide with Result.instance_methods
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.