module MasterView # Mixin for directives to provide metadata specifications # # All directives have a standard set of metadata properties: # :attribute_name - the (unqualified) directive attribute name # :namespace - the namespace name for the qualified directive name # :attribute_qname - the fully-qualified directive name (ns:attribute) # :priority - the DirectivePriority for processing # # Additional properties can be set to provide documentation and usage # information (:summary, :description) or to record custom settings. # #-- #TBD: How can we automatically extract directive rdoc from its class?? #++ # module DirectiveMetadata DEBUG_MD_INSTALLATION = false #:nodoc: DEBUG_MD_PROPS = false #:nodoc: # well-known metadata properties MARKUP_PROPERTY_NAMES = [ :namespace, :namespace_prefix, :attribute_name, :attribute_qname ] #:nodoc: PROCESSING_PROPERTY_NAMES = [ :priority ] #:nodoc: DOC_PROPERTY_NAMES = [ :summary, :description ] #:nodoc: STANDARD_PROPERTY_NAMES = MARKUP_PROPERTY_NAMES + PROCESSING_PROPERTY_NAMES + DOC_PROPERTY_NAMES #:nodoc: HARDENED_PROPERTY_NAMES = MARKUP_PROPERTY_NAMES + PROCESSING_PROPERTY_NAMES #:nodoc: # implementation hack for DirectiveMetadata class var # putting class var @@metadata_values got muddled by subclassing, punt to this scheme DirectiveMetadataRegistry = {} #:nodoc: #convenience constants defined to allow priority to directives #higher priority (lower value) will be executed first in chain module DirectivePriorities Highest = 0 UltraHigh = 0x3FFFFFFF/16 VeryHigh = 0x3FFFFFFF/8 High = 0x3FFFFFFF/4 MediumHigh = 0x3FFFFFFF/3 Medium = 0x3FFFFFFF/2 MediumLow = (0x3FFFFFFF/3)*2 Low = (0x3FFFFFFF/4)*3 VeryLow = (0x3FFFFFFF/8)*7 UltraLow = (0x3FFFFFFF/16)*15 Lowest = 0x3FFFFFFF # convenience Default = Medium end PriorityNames = [] #:nodoc: PrioritiesMap = {} #:nodoc: DirectivePriorities.constants.each { | const_name | level_name = const_name.to_s #??? next if level_name == 'Default' # only record real names PriorityNames << level_name PrioritiesMap[DirectivePriorities.const_get(const_name)] = level_name } # Answer the name of a priority level def self.get_priority_name(priority) #:nodoc: PrioritiesMap.fetch( priority, priority.to_s ) #?? "%08x" % priority end def self.get_priority_label(priority) #:nodoc: name = get_priority_name(priority) # 'default (Med)' was nicer, but admin page doesn't format width properly name = 'default' if priority == DirectivePriorities::Default name end # Answer whether a name is a legal XML markup identifier. # Used to validate attribute and namespace names for directives. # def self.validate_xml_identifier(name) #todo: add re defn to verify syntactically legal xml id # XML 1.0 spec: A Name is a token beginning with a letter or one of a few # punctuation characters, and continuing with letters, digits, hyphens, # underscores, colons, or full stops, together known as name characters. #TODO: name =~ /^[a-zA-Z](\w|-|_)*$/ true end # Ensure that a set of metadata values are valid and consistent. # This is a banger because it takes liberties normalizing the properties. def self.validate_metadata_props!(md_props) md_prop_name = :attribute_name if md_props.has_key?( md_prop_name ) attr_name = md_props[md_prop_name] raise ArgumentError, "Invalid #{md_prop_name.inspect} '#{attr_name}'" if ! DirectiveMetadata.validate_xml_identifier(attr_name) end # allow :namespace_name as a synonym for :namespace md_prop_name = :namespace_name if md_props.has_key?( md_prop_name ) ns_name = md_props[md_prop_name] if md_props.has_key?(:namespace) # we actually ought to just unconditionally complain, it's silly and redundant to do both of these other_ns_name = md_props[:namespace] raise ArgumentError, "Inconsistent namespace settigs (:namespace='#{other_ns_name}', :namespace_name='#{ns_name}')" if other_ns_name != ns_name else md_props[:namespace] = ns_name end # always get rid of :namespace_name, we just want to record :namespace md_props.delete(md_prop_name) end md_prop_name = :namespace if md_props.has_key?( md_prop_name ) ns_name = md_props[md_prop_name] raise ArgumentError, "Invalid #{md_prop_name.inspect} '#{ns_name}'" if ns_name[-1..-1] == ':' raise ArgumentError, "Invalid #{md_prop_name.inspect} '#{ns_name}'" if ! DirectiveMetadata.validate_xml_identifier(ns_name) # ensure both namespace props we use are present and consistent ns_prefix = "#{ns_name}:" if md_props.has_key?( :namespace_prefix ) ns_prefix_prop = md_props[:namespace_prefix] raise ArgumentError, "Inconsistent namespace settings (:namespace='#{ns_name}', :namespace_prefix='#{ns_prefix_prop}')" if ns_prefix_prop != ns_prefix else md_props[:namespace_prefix] = ns_prefix end end md_prop_name = :namespace_prefix if md_props.has_key?( md_prop_name ) ns_prefix = md_props[md_prop_name] raise ArgumentError, "Invalid #{md_prop_name.inspect} '#{ns_prefix}'" if ns_prefix[-1..-1] != ':' ns_name = ns_prefix[0...-1] raise ArgumentError, "Invalid #{md_prop_name.inspect} '#{ns_prefix}'" if ! DirectiveMetadata.validate_xml_identifier(ns_name) # ensure namespace settings are both present and consistent if md_props.has_key?( :namespace ) ns_name_prop = md_props[:namespace] raise ArgumentError, "Inconsistent namespace settings (:namespace='#{ns_name_prop}', :namespace_prefix='#{ns_prefix}')" if ns_name_prop != ns_name else md_props[:namespace] = ns_name end end md_prop_name = :priority if md_props.has_key?( md_prop_name ) priority = md_props[md_prop_name] if priority.is_a?( String ) || priority.is_a?( Symbol ) begin priority = 'Default' if [ 'default', :default ].include?(priority) #synonyms priority = DirectiveMetadata::DirectivePriorities.const_get(priority) md_props[md_prop_name] = priority rescue NameError #invalid priority name - fall through to error code below end end priority_range = (DirectiveMetadata::DirectivePriorities::Highest..DirectiveMetadata::DirectivePriorities::Lowest) raise ArgumentError, "Invalid #{md_prop_name.inspect}: #{md_props[md_prop_name].inspect}" if ! (priority_range.include? priority) end if DEBUG_MD_PROPS err_msg = "BAD DIRECTIVE MD PROPS: #{md_props.inspect}" raise RuntimeError, err_msg if md_props.has_key?(:namespace_name) # temp check on internal impl rework cutover raise RuntimeError, err_msg if (md_props.has_key?(:namespace) && ! md_props.has_key?(:namespace_prefix)) raise RuntimeError, err_msg if (md_props.has_key?(:namespace_prefix) && ! md_props.has_key?(:namespace)) #??raise RuntimeError, err_msg if md_props.has_key?(:attribute_qname)??? end end module ClassMethods # the metadata values for the directive class def metadata_values #:nodoc: return DirectiveMetadataRegistry[self.name] || {} end def self.extended(base) #:nodoc: STDOUT.puts "...adding #{self} into #{base} (id=#{base.object_id})" if DEBUG_MD_INSTALLATION #base.class_eval '@@metadata_values = {}' #md_accessor_code = <<-END # def #{base}.metadata_values # @@metadata_values ||= {} # end #END #base.class_eval md_accessor_code DirectiveMetadataRegistry[base.name] = {} STDOUT.puts "...#{base}.@@metadata_values=#{base.metadata_values.object_id}" if DEBUG_MD_INSTALLATION end def self.inherited(directive_class) #:nodoc: STDOUT.puts "\n###inherited #{self} in #{directive_class} (id=#{directive_class.object_id})" if DEBUG_MD_INSTALLATION #directive_class.class_eval '@@metadata_values = {}' #md_accessor_code = <<-END # def #{directive_class}.metadata_values # @@metadata_values ||= {} # end #END #directive_class.class_eval md_accessor_code DirectiveMetadataRegistry[directive_class.name] = {} STDOUT.puts "...#{directive_class}.@@metadata_values=#{directive_class.metadata_values.object_id}" if DEBUG_MD_INSTALLATION end # Answer the (unqualified) attribute name of the directive def attribute_name metadata_values[:attribute_name] || default_directive_name end def namespace metadata_values[:namespace] || default_namespace_prefix[0...-1] end ##ISSUE: deprecate this?? def namespace_name namespace end def namespace_prefix metadata_values[:namespace_prefix] || default_namespace_prefix end # Answer the fully-qualified attribute name of the directive def attribute_qname metadata_values[:attribute_qname] || "#{namespace}:#{attribute_name}" end def priority metadata_values[:priority] || DirectivePriorities::Default end # Answer the default directive attribute name for a directive class. # # If not explicitly specified, lowercased name of the class is # assumed to be the attribute name used in template markup. # def default_directive_name #:nodoc: simple_name = self.name.split(':').last # strip off module qualifiers simple_name = simple_name.downcase_first_letter # convert camel-case class name FooBar to snake-case foo_bar simple_name.gsub( /[A-Z]/ ) { |letter| "_#{letter.downcase}" } end # Answer the default namespace prefix for a directive class. # def default_namespace_prefix #:nodoc: # this is a bit squirrelly to rely on module namespace convention # masterview brings in String#starts_with? from facets, thank you if self.name.starts_with?('MasterView::Directives::') MasterView::ConfigSettings.namespace_prefix else MasterView::ConfigSettings.namespace_prefix_extensions end end # Declare metadata properties of the directive # Specify one more prop_name => value entries # :attibute_name, :namespace, :summary, :description def metadata(md_props) md_props = md_props.clone DirectiveMetadata.validate_metadata_props!(md_props) if ! DirectiveMetadataRegistry.has_key?( self.name ) DirectiveMetadataRegistry[self.name] = {} STDOUT.puts "...#{self}.@@metadata_values=#{self.metadata_values.object_id} " if DEBUG_MD_INSTALLATION else # ISSUE: not clear why we need to do this, but something's getting initialized # by initial class load/include/extend magic that I can't figure out. # This probably is safe, there should be exactly one metadata decl in # a directive implementation and it should probably always have a clean # point of view. # [DJL 06-Oct-2006] DirectiveMetadataRegistry[self.name].clear() end metadata_values.merge! md_props end # Fill in any defaults and ensure that all required metadata properties are defined. # # Ordinarily done exactly once by the directive loading mechanisms. # def harden_metadata(defaults={}) trace_enabled = DEBUG_MD_PROPS || DEBUG_MD_INSTALLATION if ! DirectiveMetadataRegistry.has_key?( self.name ) DirectiveMetadataRegistry[self.name] = {} STDOUT.puts "...#{self}.@@metadata_values=#{self.metadata_values.inspect} (id=#{self.metadata_values.object_id}) " if trace_enabled STDOUT.puts "...DirectiveMetadataRegistry=#{DirectiveMetadataRegistry.inspect}" if trace_enabled end dc_md = metadata_values STDOUT.puts "\n****HARDENING: #{self}.metadata_values=#{dc_md.inspect} (id=#{dc_md.object_id})" if trace_enabled # install any defaults for properties which aren't explicitly set md_to_add = defaults.reject { |key, value| dc_md.has_key?( key ) } #assert ! md_to_add.equal?(defaults), 'safe to mess with our own copy now' DirectiveMetadata.validate_metadata_props!( md_to_add ) # this should check and normalize any namespace-related entries raise RuntimeError, "BAD: attr naming inappropriate in md defaults #{md_to_add.inspect}" if md_to_add.has_key?(:attribute_name) || md_to_add.has_key?(:attribute_qname) # ensure that required properties are set if ! dc_md.has_key?( :attribute_name ) md_to_add[:attribute_name] = default_directive_name end if ! dc_md.has_key?( :priority ) md_to_add[:priority] = DirectivePriorities::Default end if ! dc_md.has_key?( :namespace ) raise RuntimeError, "BUG: inconsistent #{self.name}.metadata_values ns entries: #{dc_md.inspect}" if dc_md.has_key?(:namespace_prefix) || dc_md.has_key?(:attribute_qname) if md_to_add.has_key?( :namespace ) #ok, we'll fill in namespace from defaults # quadruple-check programming errors in this mess until this stuff stabilizes [DJL 06-Oct-2006] raise RuntimeError, 'BUG: validate_metadata_props! did not handle defaults normalization: #{md_to_add.inspect}' if ! md_to_add.has_key?(:namespace_prefix) elsif md_to_add.has_key?( :namespace_prefix ) # quadruple-check programming errors in this mess until this stuff stabilizes [DJL 06-Oct-2006] raise RuntimeError, 'BUG: validate_metadata_props! did not handle defaults normalization: #{md_to_add.inspect}' else default_prefix = default_namespace_prefix md_to_add[:namespace] = default_prefix[0...-1] md_to_add[:namespace_prefix] = default_prefix end end if ! dc_md.has_key?( :namespace_prefix ) # we should have just taken care of this raise RuntimeError, "BUG: inconsistent #{self.name}.metadata_values ns entries: #{dc_md.inspect}" if dc_md.has_key?(:namespace) || dc_md.has_key?(:attribute_qname) raise RuntimeError, "BUG: incorrect construction of #{self.name} md_to_add: #{md_to_add}" if ! (md_to_add.has_key?(:namespace) && md_to_add.has_key?(:namespace_prefix)) end dc_md.merge! md_to_add #assert dc_md.has_key?( :namespace) && dc_md.has_key?( :namespace_prefix) if true # ! dc_md.has_key?( :attribute_qname ) # just always do this, there's something funky about load/include/extend # somehow we're getting backstop defaults in ahead of even the first class load # I do not understand something subtle, so giving up and just hammering here [DJL 06-Oct-2006] dc_md[:attribute_qname] = "#{dc_md[:namespace]}:#{dc_md[:attribute_name]}" end #???dc_md[:hardened] = true??? #??dc_md.freeze if DEBUG_MD_PROPS # quadruple-check programming errors in this mess until this stuff stabilizes [DJL 06-Oct-2006] oops = false HARDENED_PROPERTY_NAMES.each { |md_prop_name| oops = true if ! metadata_values.has_key?(md_prop_name) } if oops raise RuntimeError, "BAD MD HARDENING IN #{self.name}: #{metadata_values.inspect}" end if metadata_values[:namespace_prefix] != "#{metadata_values[:namespace]}:" raise RuntimeError, "BAD MD HARDENING IN #{self.name}: #{metadata_values.inspect}" end if metadata_values[:attribute_qname] != "#{metadata_values[:namespace]}:#{metadata_values[:attribute_name]}" raise RuntimeError, "BAD MD HARDENING IN #{self.name}: #{metadata_values.inspect}" end end STDOUT.puts "...HARDENED MD: #{self.metadata_values.inspect}\n" if DEBUG_MD_PROPS end end #ClassMethods # add class methods for DSL declarations to class which is mixing in DirectiveMetadata def self.included(base) #:nodoc: STDOUT.puts "\n####Mixing #{self} into #{base} (id=#{base.object_id})" if DEBUG_MD_INSTALLATION base.extend(ClassMethods) end # Answer the (unqualified) attribute name used in template document markup # for this directive. def attribute_name self.class.attribute_name end def namespace self.class.namespace end def namespace_name self.class.namespace_name end def namespace_prefix self.class.namespace_prefix end # Answer the fully-qualified attribute name of the directive def attribute_qname self.class.attribute_qname end def priority return self.class.priority end end end