root/trunk/lib/fm/mixins.rb

Revision 289, 25.1 kB (checked in by ged, 6 weeks ago)

Checkpoint commit.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Rev Author URL Id
Line 
1#!/usr/bin/ruby
2
3require 'sync'
4
5require 'fm/exceptions'
6
7#
8# This file contains a collection of modules that are mixed into various FaerieMUD
9# classes.
10#
11# == Subversion ID
12#
13# $Id$
14#
15# == Authors
16#
17# * Michael Granger <ged@FaerieMUD.org>
18#
19
20module FaerieMUD
21
22        ### A mixin that adds logging to its including class.
23        module Loggable
24                require 'fm/logger'
25
26                ### Add the interface to both classes and instances
27                def self::included( klass )
28                        super
29                        klass.extend( self )
30                end
31
32                ######
33                public
34                ######
35
36                ### Return the FaerieMUD::Logger object for the receiving class.
37                def log
38                        if self.is_a?( Class )
39                                FaerieMUD::Logger[ self ]
40                        else
41                                FaerieMUD::Logger[ self.class ]
42                        end
43                end
44
45        end # module Loggable
46
47
48        ### Hides your class's ::new method and adds a method generator called 'virtual' for
49        ### defining API methods. If subclasses of your class don't provide implementations of
50        ### "virtual" methods, NotImplementedErrors will be raised if they are called.
51        ###
52        ###   # AbstractClass
53        ###   class MyBaseClass
54        ###       include ThingFish::AbstractClass
55        ###
56        ###       # Define a method that will raise a NotImplementedError if called
57        ###       virtual :api_method
58        ###   end
59        ###
60        module AbstractClass
61
62                ### Methods to be added to including classes
63                module ClassMethods
64
65                        ### Define one or more "virtual" methods which will raise
66                        ### NotImplementedErrors when called via a concrete subclass.
67                        def virtual( *syms )
68                                syms.each do |sym|
69                                        define_method( sym ) {
70                                                raise ::NotImplementedError,
71                                                        "%p does not provide an implementation of #%s" % [ self.class, sym ],
72                                                        caller(1)
73                                        }
74                                end
75                        end
76
77
78                        ### Turn subclasses' new methods back to public.
79                        def inherited( subclass )
80                                subclass.module_eval { public_class_method :new }
81                                super
82                        end
83
84                end # module ClassMethods
85
86
87                extend ClassMethods
88
89                ### Inclusion callback
90                def self::included( mod )
91                        super
92                        if mod.respond_to?( :new )
93                                mod.extend( ClassMethods )
94                                mod.module_eval { private_class_method :new }
95                        end
96                end
97
98
99        end # module AbstractClass
100
101
102        ### Interface for object classes which can have and be affected by
103        ### properties. Applies to both the including class and its instances.
104        module Propertied
105
106
107                ### Make including classes propertied.
108                def self::included( mod )
109                        mod.extend( self )
110                        super
111                end
112               
113               
114                #############################################################
115                ###     I N S T A N C E   M E T H O D S
116                #############################################################
117
118                ### Add data structures to support properties to new instances of
119                ### Propertied objects.
120                def initialize( *args, &block ) #:notnew:
121                        super
122                        @properties = []
123                end
124
125
126                ######
127                public
128                ######
129
130                ### Returns the properties array made up of the union of the receiver's
131                ### properties, its class's properties, and all those of its class's
132                ### Propertied superclasses.
133                def properties
134                        @properties ||= []
135                        union = @properties.dup
136
137                        # For classes, use the superclass's property list; for instances,
138                        # use the class's property list.
139                        super_source =
140                                if self.is_a?( Class )
141                                        self.superclass
142                                else
143                                        self.class
144                                end
145
146                        # Add any properties that aren't occluded to the union if the
147                        # next-higher class is propertied.
148                        if super_source.include?( FaerieMUD::Propertied )
149                                union += super_source.properties.reject do |sp|
150                                        union.find {|prop| prop.occludes?(sp) }
151                                end
152                        end
153
154                        return union
155                end
156
157
158                ### Returns true if the Propertied object has a property of the
159                ### specified +kind+, which is either a string, in which case it is
160                ### compared against the #kind of each property, or a +Class+,
161                ### in which case a #kind_of? test is done.
162                def has?( kind )
163                        if kind.is_a?( Class )
164                                return true if self.properties.find {|prop| prop.kind_of?(kind) }
165                        else
166                                kindString = kind.to_s
167                                return true if self.properties.find do |prop|
168                                        prop.kind == kindString || !(prop.synonyms & [kindString]).empty?
169                                end
170                        end
171                end
172
173
174                ### Returns true if any of the receiving object's properties match the
175                ### given +specifier+ with a degree of truth > 0. The specifier is a
176                ### +String+ which usually contains an adjective.
177                def is?( specifier )
178                        self.properties.detect {|prop| prop.how(specifier.to_s) > 0 }
179                end
180
181
182                ### Returns a degree of truth (in the range 0.0 to 1.0, inclusive) for
183                ### the given specifier.
184                def how( specifier )
185                        prop = self.is?( specifier ) or return 0.0
186                        prop.how( specifier )
187                end
188
189
190                ### Add the specified properties to the receiving object.
191                def add_properties( *prop )
192                        @properties ||= []
193
194                        # Add properties that aren't already in our array
195                        newProperties = (prop - (@properties & prop)).select {|newprop|
196                                newprop.adding_to( self )
197                        }
198
199                        # Delete current properties that will be occluded by a new property
200                        @properties.delete_if {|oldprop|
201                                newProperties.find {|newprop|
202                                        newprop.occludes?( oldprop )
203                                }
204                        }
205
206                        # Add the new properties
207                        @properties |= newProperties
208                        return newProperties
209                end
210
211
212                ### Remove the specified properties from the receiving object. The +prop+
213                ### array can contain Property objects, or Property classes. If there are
214                ### no arguments, all properties are removed. The properties that are
215                ### actually removed are returned.
216                def remove_properties( *prop )
217                        prop.push FaerieMUD::Property if prop.empty?
218                        @properties ||= []
219
220                        # Find properties to be removed
221                        removedProperties = prop.map! {|thisProp|
222                                case thisProp.type
223                                when ::Class
224                                        @properties.find_all {|oldProp|
225                                                oldProp.kind_of?( thisProp )
226                                        }
227
228                                when FaerieMUD::Property
229                                        @properties & [ thisProp ]
230                                end
231                        }.flatten.uniq
232                       
233                        # Call the property callback for each property that will be removed.
234                        removedProperties.select {|oldprop|
235                                oldprop.removing_from( self )
236                        }
237
238                        # Remove properties
239                        @properties -= removedProperties
240                        return removedProperties
241                end
242
243        end # module Propertied
244
245
246        ### Hookable mixin. Including this module allows a class's methods to be
247        ### hooked. Method hooking is similar to Aspect-Oriented Programming; it
248        ### allows an object's methods to be selectively overridden (and
249        ### un-overridden) at runtime.
250        module Hookable
251
252
253                ### Initializer for including classes: adds a hooks table to new
254                ### objects.
255                def initialize( *args )
256                        @hooks = {}
257                        super
258                end
259
260                ######
261                public
262                ######
263
264                ### Get the Array of hooks applied to the current object for the method
265                ### specified by +sym+. If no argument is given, returns a hash of all
266                ### hooks keyed by the symbol of the hooked method. The array of hooks
267                ### includes the Method object for the original method as its first
268                ### member.
269                def hooks( sym=nil )
270                        @hooks ||= {}
271                        if sym.nil?
272                                return @hooks
273                        else
274                                return @hooks[sym]
275                        end
276                end
277
278
279                ### Hook the specified method (+symbol+) with the specified block. Returns the
280                ### specified block as a Proc object, which can be used later to #unhook it.
281                def hook( symbol, &block )
282                        @hooks ||= {}
283
284                        # If there aren't any hooks yet installed for this symbol, bind the
285                        # original method, stick it into the hook hash, and then replace the
286                        # method with our hook-calling surrogate. We also alias away the
287                        # original method so if someone really wants to call the unhooked
288                        # one, they can.
289                        if ! @hooks[ symbol ]
290                                boundMeth = method( symbol )
291                                @hooks[ symbol ] = [ boundMeth ]
292
293                                name = symbol.to_s
294                                instance_eval %Q{
295                                        alias :__unhooked_#{name} :#{name}
296                                        def #{name}( *args )
297                                                @hooks[ :#{name} ].reverse.inject(args) {|args,hook|
298                                                        hook.call(*args)
299                                                }
300                                        end
301                                }
302                        end
303
304                        # Add the specified proc object to the stack, and return it for
305                        # later reference
306                        @hooks[ symbol ].push( block )
307                        return block
308                end
309
310
311                ### Unhook either the last installed hook, or the hook specified by
312                ### +hook+ (the object returned from the call to #hook) from the method
313                ### specified. Returns the Proc or Method object that was unhooked.
314                def unhook( symbol, hook=nil )
315                        @hooks ||= {}
316
317                        # If we've hooks for this method, find the method to retrieve
318                        if @hooks[ symbol ]
319                                name = symbol.id2name
320
321                                # If the caller hasn't specified a method, just pop the last one
322                                # off the stack. If the caller specified a Proc, subtract it
323                                # from the stack after making sure it's there for the return
324                                # value.
325                                if hook.nil?
326                                        hook = @hooks[ symbol ].pop
327                                elsif @hooks[ symbol ].include?( hook )
328                                        @hooks[ symbol ].delete_if {|h| h == hook }
329                                else
330                                        hook = nil
331                                end
332
333                                # If there aren't any more hooks, re-install the original
334                                # method
335                                if @hooks[ symbol ].length <= 1
336                                        self.unhook_all( symbol )
337                                end
338
339                                # Return the removed hook, if any
340                                return hook
341                        else
342                                return nil
343                        end
344                end
345
346
347                ### Remove all hooks from the specified method and return them (minus
348                ### the Method object for the original method, which will be
349                ### reinstalled).
350                def unhook_all( symbol )
351                        hooks = @hooks[ symbol ]
352                        @hooks.delete( symbol )
353
354                        name = symbol.to_s
355                        instance_eval %Q{
356                                alias :#{name} :__unhooked_#{name}
357                        }
358
359                end
360
361        end # module Hookable
362
363
364        ### A mixin that can be used to add debugging facilities to a class and its
365        ### instances.
366        module Debuggable
367
368                ### Add and initialize the @debug_level of the reciever.
369                def initialize( *args ) # :notnew:
370                        @debug_level = 0
371                        super
372                end
373
374
375                ### Returns the current debugging level as a Fixnum. Higher values = more
376                ### debugging output
377                def debug_level
378                        @debug_level ||= 0
379                end
380
381
382                ### Set the debugging level of the reciever. The <tt>value</tt> argument
383                ### can be <tt>true</tt>, <tt>false</tt>, a Numeric, or a String that
384                ### yields something Numeric when <tt>to_i</tt> is called.
385                def debug_level=( value )
386                        case value
387                        when true
388                                @debug_level = 1
389                        when false
390                                @debug_level = 0
391                        when Numeric, String
392                                value = value.to_i
393                                value = 5 if value > 5
394                                value = 0 if value < 0
395                                @debug_level = value
396                        else
397                                raise TypeError, 
398                                        "Cannot set debugging level to %p (%s)" %
399                                        [ value, value.class ]
400                        end
401                end
402
403
404                ### Returns true if the current debugging level of the reciever is
405                ### greater than or equal to the specified <tt>level</tt>.
406                def debugged?( level=1 )
407                        debug_level() >= level
408                end
409
410
411                ###############
412                module_function
413                ###############
414
415                ### Output <tt>messages</tt> to the debugging log if the
416                ### <tt>debug_level</tt> of the calling object is greater than or equal
417                ### to <tt>level</tt>
418                def debug_msg( level, *messages )
419                        return unless debugged?( level )
420
421                        logMessage = messages.collect {|m| m.to_s}.join('')
422                        frame = caller(1)[0]
423                        if Thread.current != Thread.main && Thread.current.respond_to?( :desc )
424                                logMessage = "[Thread: #{Thread.current.desc}] #{frame}: #{logMessage}"
425                        elsif Thread.current != Thread.main
426                                logMessage = "[Thread #{Thread.current.id}] #{frame}: #{logMessage}"
427                        else
428                                logMessage = "#{frame}: #{logMessage}"
429                        end
430
431                        self.log.debug( logMessage )
432                end
433
434               
435        end # module Debuggable
436
437       
438        ### A mixin which causes including classes to become flyweights, ala the
439        ### Flyweight pattern from Design Patterns.
440        ###
441        ### Classes which implement this interface are instantiated with the class
442        ### method called #fetch, and are removed from the flyweight pool with the
443        ### #remove method. The arguments to #fetch are used as the key to the
444        ### flyweight instances -- calling #fetch on the Flyweighted class twice
445        ### with identical arguments will return the same object instance. You can
446        ### remove an object from the pool by passing it as an argument to #remove;
447        ### thereafter any remaining references to it are no longer associated with
448        ### the pool, and a new object will be instantiated and returned for
449        ### identical creation arguments.
450        ###
451        ### If you wish only some of the creation arguments to be used as the key to
452        ### the pool, you can define a class method named #flyweightArgs, which
453        ### should return an Integer specifying how many arguments to use.
454        module Flyweight
455
456                ### The Ouroboros Trick
457                def self::included( klass )
458                        klass.extend( self )
459                end
460
461                ### Add the pool instance variable and accessor, and make the
462                ### constructor private.
463                def self::extend_object( obj )
464                        unless obj.is_a?( Class )
465                                raise TypeError,
466                                        "Can only extend Classes, not a #{obj.class.name}"
467                        end
468
469                        obj.instance_variable_set( :@__pool__, {} )
470                        obj.instance_eval %{
471                                private_class_method :new
472                        }
473                        super
474                end
475
476
477                ### Override the copy initializer to raise an exception
478                def initialize_copy( *args )
479                        raise RuntimeError, "Can't duplicate a #{self.class} (Flyweight)"
480                end
481
482
483                ### Fetch the instance that corresponds to the specified arguments,
484                ### creating it and registering it with the pool of instances if
485                ### necessary. Calling this method twice with identical arguments will
486                ### return the same object instance.
487                def fetch( *args )
488                        if self::respond_to?( :flyweight_args )
489                                key = args[ 0, self.flyweight_args ]
490                                @__pool__[key] ||= new( *args )
491                        else
492                                @__pool__[args] ||= new( *args )
493                        end
494                end
495
496
497                ### Remove the specified +object+ from the pool of instances; any
498                ### remaining references to it are no longer associated with the pool,
499                ### and a new object will be instantiated and returned for identical
500                ### creation arguments.
501                def remove( object )
502                        @__pool__.delete_if {|key,val| val == object}
503                end
504
505
506                ### Disallow duplication and cloning.
507                def dup
508                        raise RuntimeError, "Can't duplicate a #{self.class} (Flyweight)."
509                end
510                alias_method :clone, :dup
511        end
512
513
514
515        ### A module full of 'attr_*' functions to facilitate generation of
516        ### accessors that are more elaborate than +attr+ can generate.
517        module AccessorFunctions
518
519                ### The Ouroboros Trick
520                def self::included( klass )
521                        klass.extend( self )
522                end
523
524                ###############
525                module_function
526                ###############
527
528                ### Defines an attribute named +sym+ for the calling class. If +reader+
529                ### is +true+, a reader which accesses it through a sharable mutex is
530                ### also created. If +writer+ is +true+, also creates a mutator called
531                ### +name=+ that is accessed through an exclusive mutex. Depends on an
532                ### instance variable called '@mutex' being present in the including
533                ### class which contains an instance of the Sync class or a compatible
534                ### replacement; one will be added to the object on the first call to a
535                ### method generated by this function if one is not already present.
536                def attr_locked( sym, reader=true, writer=false )
537                        ivarname = "@#{sym.id2name}"
538
539                        # Define the accessor
540                        if reader
541                                define_method( sym ) {
542                                        if instance_variables.include?( ivarname )
543                                                readlocked do
544                                                        instance_variable_get( ivarname )
545                                                end
546                                        else
547                                                nil
548                                        end
549                                }
550                        end
551
552                        # Define the mutator if the writer flag is set.
553                        if writer
554                                define_method( "#{sym}=".intern ) {|arg|
555                                        writelocked do
556                                                instance_variable_set( ivarname, arg )
557                                        end
558                                }
559                        end
560                end
561
562
563                ### Define a locked reader via #attr_locked for the attribute specified
564                ### by +sym+
565                def attr_locked_reader( sym )
566                        attr_locked( sym, true )
567                end
568
569
570                ### Define a mutator via #attr_locked for the attribute specified by
571                ### +sym+.
572                def attr_locked_writer( sym )
573                        attr_locked( sym, false, true )
574                end
575
576
577                ### Define a locked accessor and mutator via #attr_locked for the
578                ### attribute specified by +sym+.
579                def attr_locked_accessor( sym )
580                        attr_locked( sym, true, true )
581                end
582        end # module AccessorFunctions
583
584
585        ### Adds the #to_html method to including classes, which can dump the description of an
586        ### object as HTML.
587        ###
588        ### == Customization
589        ### You can customize the HTML generated for your class by overriding one of the following
590        ### protected methods:
591        ###
592        ### #html_object_header, #html_object_footer, #html_ivar_section, #html_wrapper
593        module HTML
594
595                require 'fm/linguistics'
596                require 'fm/gameobject'
597
598                ######
599                public
600                ######
601
602                ### Return the object as one or more HTML fragments. If +inline+ is
603                ### +true+, the result will be wrapped in a SPAN element suitable for
604                ### inline display. If it is +false+, it will be wrapped in a DIV
605                ### element for block display.
606                def to_html( inline=false )
607                        rval = []
608
609                        rval << self.html_object_header( inline )
610                        rval << self.html_ivar_section( inline, "@id" )
611                        rval << self.html_object_footer( inline )
612
613                        return self.html_wrapper( rval.join("\n"), inline )
614                end
615
616
617                #########
618                protected
619                #########
620
621                ### Return an HTML fragment for the header part of the given object.
622                def html_object_header( inline )
623                        func = inline ?
624                                FaerieMUD::HTML::method(:span) :
625                                FaerieMUD::HTML::method(:div)
626
627                        # The outer object header
628                        return func.call( :class => "object-header" ) {
629                                [
630                                        FaerieMUD::HTML.span( :class => "object-noun" ) {
631                                                FaerieMUD::Linguistics::make_noun( self )
632                                        },
633                                        FaerieMUD::HTML.span( :class => "object-class" ) {
634                                                self.class.name
635                                        },
636                                        FaerieMUD::HTML.span( :class => "object-id" ) {
637                                                "#%s" % self.object_id
638                                        },
639                                        FaerieMUD::HTML.span( :class => "object-class-version" ) {
640                                                self.class.respond_to?( :rev ) ? "(v%d)" % self.class.rev : '(unknown)'
641                                        },
642                                ].join(" ")
643                        }
644                end
645
646
647                ### Return an HTML fragment for the footer part of the given object.
648                def html_object_footer( inline )
649                        func = inline ?
650                                FaerieMUD::HTML.method(:span) :
651                                FaerieMUD::HTML.method(:div)
652
653                        contents = []
654
655                        if self.class.respond_to?( :authors )
656                                contents << FaerieMUD::HTML.span( :class => "object-contrib" ) do
657                                        "Authors: " +
658                                        self.class.authors.collect do |author,points|
659                                                FaerieMUD::HTML.span(
660                                                        :class => "object-contrib-author" ) {author.to_s} +
661                                                ": " +
662                                                FaerieMUD::HTML.span(
663                                                        :class => "object-contrib-points" ) {points.to_s}
664                                        end.join("; ")
665                                end
666                        end
667                       
668                        if self.class.const_defined?( :SVNId )
669                                contents << FaerieMUD::HTML.span( :class => 'object-svn-id' ) do
670                                        self.class.const_get(:SVNId)
671                                end
672                        end
673
674                        return func.call( :class => "object-footer" ) { contents }
675                end
676
677
678                ### Return the object's instance variables as a series of HTML
679                ### fragments. If +inline+ is +true+, each object will be wrapped in
680                ### SPAN elements for inline display. If it is false, they will be
681                ### wrapped in a DIV element. Any variables listed in the +skiplist+
682                ### will not be shown.
683                def html_ivar_section( inline=false, *skiplist )
684                        rval = []
685
686                        instance_variables.sort.each {|var|
687                                next if skiplist.include?( var ) or
688                                        skiplist.include?( var.sub(/^@/,'') )
689
690                                # Fetch the instance variable and turn it into HTML
691                                value = instance_variable_get( var )
692                                html = FaerieMUD::HTML::convert_object( value, inline )
693                                classes = []
694
695                                # If it's a FaerieMUD object, add the entity-ified version of
696                                # its class
697                                classes << value.class.name.gsub( /::/, '-' ).downcase
698
699                                rval << FaerieMUD::HTML::ivar_element( var, html, inline, *classes )
700                        }
701
702                        return *rval
703                end
704
705
706                ### Return the given content wrapper in a container with the receiver's
707                ### attributes. If +inline+ is +true+, the container will be a SPAN
708                ### element, suitable for inline display. If it is +false+, the
709                ### container will be a DIV element for block display.
710                def html_wrapper( content, inline=false )
711                        func = inline ?
712                                FaerieMUD::HTML::method(:span) :
713                                FaerieMUD::HTML::method(:div)
714
715                        classname = self.class.name == '' ? 
716                                "anonymous-class-object" :
717                                self.class.name.gsub(/::/,'-').downcase
718
719                        return func.call( :id => "oid-%d" % [self.object_id], :class => "object #{classname}" ) do
720                                "\n" + content + "\n"
721                        end
722                end
723
724
725                #############################################################
726                ###     G E N E R I C   F U N C T I O N S
727                #############################################################
728
729                ###############
730                module_function
731                ###############
732
733                ### Escape any HTML in the specified +string+. (Stolen from cgi.rb)
734                def escape_html( string )
735                        string.
736                                gsub(/&/n, '&amp;').
737                                gsub(/\"/n, '&quot;').
738                                gsub(/>/n, '&gt;').
739                                gsub(/</n, '&lt;')
740                end
741
742
743                ### Return an html tag of the specified name and attributes (a
744                ### Hash). The content (if any) is expected to be returned from the
745                ### given block.
746                def element( name, attributes={} ) # :yeilds:
747                        openElem, body, closeElem = nil,nil,nil
748
749                        # Start the opening tag with the downcased name (XHTML-compatible)
750                        # and the given attributes.
751                        openElem = "<%s%s%s" % [
752                                name.to_s.downcase,
753                                attributes.empty? ? "" : " ",
754                                attributes.collect {|k,v| %{#{k}="#{v}"} }.join(" "),
755                        ]
756
757