Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to respond to text in attachments #177 #180

Merged
merged 6 commits into from
Mar 9, 2018

Conversation

mdudzinski
Copy link
Collaborator

@mdudzinski mdudzinski commented Feb 28, 2018

Closes #177

Added a new DSL attachment for scanning text in attachments. This simple approach just iterates over pretext, text and title fields of each attachment and test each field against the match. It returns the first match so if there is more than one attachment in the array then there is no easy way to tell which attachment record has been matched.

I generated a new .rubocop_todo.yml file as the new offenses require a general refactoring of SlackRubyBot::Commands::Base class.

I'll update the documentation and the changelog once this approach is accepted.

@@ -122,6 +131,23 @@ def finalize_routes!
command command_name_from_class
end

def match_attachments(data, route)
fields_to_scan = %i[pretext text title]
data.attachments_.each do |attachment|
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary usage of under-bang from Hashie::Mash as there is a validation against missing data (data_missing?). Will take it out.

@mdudzinski
Copy link
Collaborator Author

I also thought a bit about a possibility to know which attachment record has been actually matched. Adding a new param to the block seems to be overcomplicated to me but perhaps I'm wrong.
Anyway, I think it should be available via the match param. It could be achieved by either:

  • dynamically defining a new method on the match instance that would return the matched attachment or its index in the array
  • the bot_matcher approach - add the attachment record index value to each field value before testing it and extending the route[match] with a matcher for the index value. Then the matched attachment's index in the array could be accessing by match[:__attachment_index or something like that. Double underscore to avoid eventual conflicts.

Copy link
Collaborator

@dblock dblock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

I think we need to make the attachment available inside the block as well as being able to tell what matched in the attachment (title, etc.). Eventually we can have more deliberate matching (match only title).

What do you think?

Some detailed comments below.

README.md Outdated

```ruby
class Attachment < SlackRubyBot::Bot
attachment 'Slack API Documentation' do |client, data, stocks|
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's stocks here? I think what we want is to pass in the matching attachment into this block.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy-paste issue 😃will fix this, thanks


def data_missing?(match_method, expression, data)
return true if match_method != :attachment && expression.nil?
return true if match_method == :attachment && (data.attachments.nil? || data.attachments.empty?)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this muddles two somewhat separate things and isn't reused elsewhere. I would actually roll this method into two next statements above without extracting into a method or write this here as a case statement. Either way return false explicitly (I think currently it's nil).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look on this, thanks.

@mdudzinski
Copy link
Collaborator Author

mdudzinski commented Feb 28, 2018

@dblock

I think we need to make the attachment available inside the block

So how about returning a Hashie::Mash that contains MatchData as well as the matched attachment and the matched field. Or extend the MatchData class itself and include information about matched attachment.

Eventually we can have more deliberate matching (match only title).

The DSL can be extended so it takes the attachment object fields to match against. If no fields are passed, the default one will be used.

)
match.singleton_class.send(
:define_method, :attachment_field, -> { field }
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dblock The above code adds the access to matched attachment information to the match object instance.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels way to complicated. You should just subclass the match object.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see but subclassing a core class without a constructor doesn't seem to be less complicate. Unless there is some easy way I missed.
I think we can use

  • a new class that uses delegation instead of inheritance
  • a module and make the match object extend it on the fly

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am good with a new class that uses delegation.

return false unless expression.nil?
when :attachment
return false unless data.attachments.nil? || data.attachments.empty?
end
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer extracting logic into methods even if they aren't re-used. Just to increase readability and reduce the cyclomatic complexity. So I decided to re-organize the method to use a case statement according to your suggestion.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something doesn't smell right here as we do the case twice, once in invoke and once in data_missing. Haven't tried to rewrite it, but usually this means both can be combined avoiding having to case twice.

Copy link
Collaborator

@dblock dblock Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is simpler :)

case match_method
when ...
  expression.nil?
when :attachment
  data.attachments.nil? || data.attachments.empty
else
  true
end

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just put the checks directly in the case in invoke but this will increase the cyclomatic complexity.
It would look like

when :match
  next unless expression
  # ...
when :scan
  next unless expression
  # ...
when :attachment
  next unless data.attachments && !data.attachments.empty?
  # ...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a code purist re: cyclomatic complexity :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I'll add the checks to the invoke method then :)

@mdudzinski
Copy link
Collaborator Author

mdudzinski commented Feb 28, 2018

@dblock I implemented some of the improvements you had suggested. I pointed them as inline comments. The rest will be added soon. Let me know what you think so far.

@@ -81,6 +85,11 @@ def scan(match, &block)
self.routes[match] = { match_method: :scan, block: block }
end

def attachment(match, &block)
self.routes ||= ActiveSupport::OrderedHash.new
Copy link
Collaborator

@dblock dblock Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like we need to make self.routes a method that does @routes ||= or something like that? We repeat this a few times. NBD

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree.

@@ -122,6 +131,36 @@ def finalize_routes!
command command_name_from_class
end

def match_attachments(data, route)
fields_to_scan = %i[pretext text title]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be the third optional fields parameter here defaulted to these. Just thinking about the future as we pass this in dynamically from the attachments options.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will think about this.

@mdudzinski
Copy link
Collaborator Author

@dblock Everything is fixed. I think. Please take a look.

Copy link
Collaborator

@dblock dblock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. I am merging this.

In next steps - we already have a support folder with things like CommandsHelper, I think Commands::Help::Match should just be SlackRubyBot::Commands::Match or maybe SlackRubyBot::CommandMatch and moved into the support folder? Maybe something else? What do you think? Maybe try a few options, potentially renaming our current CommandsHelper class too?

@dblock dblock merged commit 9e7d0c9 into slack-ruby:master Mar 9, 2018
@dblock
Copy link
Collaborator

dblock commented Mar 9, 2018

@mdudzinski interested in joining the (co)maintainers of this gem? Maybe cutting the next release? If so lmk what your rubygems email is.

@mdudzinski
Copy link
Collaborator Author

@dblock Sure I'm interested -> michal[at]dudzin[dot]ski (https://rubygems.org/profiles/dudek)

Yeah I don't like Commands::Help::Match too and it was meant to be a temp solution.
I'll think about this but here go some thoughts off the top of my head:
SlackRubyBot::CommandMatch could work and would be consistent with CommandHelper. But since CommandHelper is used only inside of the commands directory then perhaps it would make more sense to move CommandHelper to the help directory.
Then the dir could be renamed from help to support and/or an additional namespace (Help:: or Support::) could be omitted. So we end up with SlackRubyBot::Commands::Helper, SlackRubyBot::Commands::Match etc.
But won't it be too confusing? SlackRubyBot::Commands::Match sounds like a command implementation.

@dblock
Copy link
Collaborator

dblock commented Mar 12, 2018

Added you, feel free to cut a release whenever you see fit. Please follow https://github.com/slack-ruby/slack-ruby-bot/blob/master/RELEASING.md.

I think everything you say makes sense - play with it - try to make a PR that you can stand behind!

@mdudzinski
Copy link
Collaborator Author

@dblock Thanks! I'll do.

@mdudzinski
Copy link
Collaborator Author

@dblock Lets move the discussion to #182. Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants