#TODO: Cacheing can work because controllers return the renderd values, just need to wrap/alias the controller actions module PushRoute extend ActiveSupport::Concern #Computes the file path of inherited classes for use when auto-generating routes included do #class_attribute :controller_path class_attribute :push_routes class_attribute :trigger_symbols self.trigger_symbols = [] self.push_routes = {} #Computed this way when mixed in to the controller base class def self.inherited(subclass) subclass.push_routes = {} subclass.trigger_symbols = [] # controller_matches = caller[0].match(/^.*?\/app\/controllers\/(.*?)_controller.rb:/) # if controller_matches # # subclass.controller_path = controller_matches[1] # else # # Happens with rspec test cases # # this might not be the best thing to do with this case # puts "Warning: No standard controller path found for #{subclass.inspect}" # end super end #Do it this way when used as a mixin for controllers #Seems like the controller file is the third method in the call stack. May change in later rails versions #self.controller_path = caller[2].match(/^.*?\/app\/controllers\/(.*?)_controller.rb:/)[1] end module ClassMethods #TODO: if not route already make, make it; set association between route(s) and action # test method aliasing, perhaps can still do caching explicitly, or maybe just # expiring caches properly; ignore caches for now # # maybe just internal whisper pub sub with a routing adapter # also pull out seperate class for each push route to store routes and triggers # Maybe rails magic on for trigger function names def enable_push_route(action, route = nil) matched_url = nil Rails.application.routes.routes.each do |e| if e.defaults[:controller] == self.controller_path && e.defaults[:action] == action.to_s unless matched_url matched_url = PushRoutes::PushRouteUrl.new(e) else raise ArgumentError.new("Duplicate route for push route controller") end end end #If a route exists and one is provided, make sure they are compatable if matched_url and route raise ArgumentError.new("Provided route does not match pre-existing route") unless (matched_url.matches(route)) url = matched_url elsif route and !matched_url #There was no pre-existing route found and a route was specified # Add this route to the list of live routes url = PushRoutes::PushRouteUrl.new(route) elsif !route and matched_url #Matched one route only, we're good url = matched_url else #No route and no match raise ArgumentError.new("No route found and no route provided") end if push_routes[action] raise ArgumentError.new("Push Route enabled twice") else push_routes[action] = url end end # If it's defined as an instance method and not a static method we'll define a static method which wraps the functinality # While this seems crazy, ActionMailer does this so there's precident def method_missing(method_sym, *arguments, &block) if instance_trigger?(method_sym) new.send(method_sym, *arguments, &block) else super end end #Overriding respond_to_missing to allow our method_missing override to behave properly def respond_to_missing?(method_sym, include_private = false) instance_trigger?(method_sym) || super end def instance_trigger?(method_sym) trigger_symbols.include?(method_sym) && instance_methods.include?(method_sym) end # # Sets a trigger for pushing updates # @param [symbol] the action in this controller for the listener to trigger # @param model [ModelType] The type of model to listen to e.g. User, LabOrder # @param type [Symbol] Listener type, e.g. :after_update, see below for list # @param trigger_function [function] Function to determine if an update should be triggered, either proc or reference # function should return true to trigger update of the route; if the route requires params to resolve (i.e /users/:id/files) # the params should be returned as a hash. Return false or nil to not trigger an update # def add_trigger(action, model, type, trigger_function = nil) enable_push_route action unless push_routes[action] # Get get the callback passed in if (!trigger_function and block_given?) # Block only # Note Proc.new gets the passed in block without instantiating an extra proc # See http://mudge.name/2011/01/26/passing-blocks-in-ruby-without-block.html callback = Proc.new elsif trigger_function && trigger_function.is_a?(Proc) # Proc passed callback = trigger_function elsif trigger_function #&& !trigger_function.is_a?(Proc) implied here # Symbol passed trigger_symbols << trigger_function callback = lambda { |e| self.send(trigger_function, e) } else #default value callback = Proc.new { true } end if [:after_commit, :after_save, :after_update, :after_create, :after_destroy].include? type this = self func = lambda do |e| result = callback.call(e) if (result) result = [result] unless result.kind_of?(Array) result.each do |params| PushRoutes.trigger(this.push_routes[action].notification_string(params)) end end end model.send(type, func) else raise ArgumentError.new("Invalid trigger type") end end #TODO: check for full class depth or shallow depth i.e. Api2::VitalsController def model #Note: const_defined is unrelialbe due to rails auto_loading, model might not be loaded begin Object.const_get(controller_path.classify) rescue nil end end def add_id_trigger(action, model, id_name, type = :after_save) add_trigger(action, model, type) { |object| {id: object.send(id_name)} } end #If you have a route like /room/:id/messages where Message belongs_to room #you can use add_belongs_to_trigger :index, Message #It will look at the url and try to add a trigger #on Message save triggering the id as Message.room.id def add_belongs_to_trigger(action, model, type = :after_save) enable_push_route action unless push_routes[action] url = push_routes[action] warn "Push Routes WARNING: Url #{url.inspect} contains too many params" if url.param_associations.count > 1 warn "Push Routes WARNING: Url #{url.inspect} contains not enough notated params" if url.param_associations.count > 1 sym = url.param_associations.first[1].singularize.to_sym if model.table_exists? if model&.send(:new)&.respond_to?(sym) add_trigger(action, model, type) { |object| {id: object.send(sym)&.id} } else warn "Push Routes WARNING: #{model} does not respond to #{sym} in belongs_to trigger" end else warn("Push Routes WARNING: no table exists for #{model}") end end def add_show_trigger(action) if self.model add_id_trigger(action, self.model, :id) else warn "Push Routes WARNING: No model found for #{controller_path}" end end end end