Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Custom reference links support #647

Open
wants to merge 1 commit into from

2 participants

@amireh

The feature allows the registration of arbitrary "link keywords" that
can be used to link against certain object types. This is handy when
defining custom CodeObject implementations where referencing them by
path is unnatural and not human readable.

Modifications:

  • RegistryStore now tracks object_links which is a keyword -> object_type map
  • Proxy objects can now act as "links" and resolve themselves
  • HTMLHelper#link_object handles linkable Proxy objects

There are specs that cover the added methods, and all
the modifications are documented with proper YARD syntax.

For lack of better place to write an example, here's what the feature would allow a user to do:

# @see Spec: name of spec
# @see Appendix: YARD Pipeline

As opposed to:

# @see .spec.path_to_spec Spec: name of spec
# @see .appendix.namespace.appendix_name Appendix: YARD Pipeline

Or whatever the object paths might be.


I tried to find the proper place to write some example of how to use the feature outside the actual API documentation (there's a proper example in the Registry.register_link method) but I wasn't sure. So if you're good with the changes and would like to merge them, I'll be happy to write the end-user doc for it.

PS: we talked about this on IRC last night, my alias is kandie.

@amireh amireh Support for custom reference links for CodeObjects
The feature allows the registration of arbitrary "link keywords" that
can be used to link against certain object types. This is handy when
defining custom CodeObject implementations where referencing them by
path is unnatural and not human readable.

Modifications:
  * RegistryStore now tracks object_links which are a keyword ->
    object_type map
  * Proxy objects can now act as "links" and resolve themselves
  * HTMLHelper#link_object handles linkable Proxy objects

There are specs that cover the added methods, and all
the modifications are documented with proper YARD syntax.
b3d0e3e
@lsegal
Owner

I like the work that went into this, the tests and implementation are solid, but I'm concerned about the design for a few reasons:

  1. I don't see why it's necessary to modify the yard database to store links. Since custom link registration is only going to be added in via plugins, storing this in the database is unnecessary and bloats the RegistryStore. Instead, plugins should simply need to modify runtime methods to provide the necessary lookup logic.

  2. Unresolved proxies aren't real objects, so they should never link anywhere. If a proxy hasn't been resolved, that's enough to say that the object has no link anywhere. Using proxies to shoehorn linking behaviour (as mentioned in point 2 of the commit message) is dangerous, since the link resolution might change if the proxy ever resolves in the future.

  3. Feeding on point 2, it's wrong to re-use global registry lookup for links. P('ObjectName') explicitly asks for a lookup of code object, which means if you register a link 'Class:' and a plugin defines an object by the key of 'Class:', P('Class:') is now returning a real object which breaks the plugin's ability to link. Furthermore, the API allows you to register links on any string, which would break if someone defined the string as 'SomeExistingObject'. The Proxy class is really meant to be a subset of the Base object as a delegate class, even though it does not represent this in the hierarchy (which was actually a design mistake). Defining extra methods on Proxy is not correct, and treating any unresolved objects as resolvable links isn't always a correct assumption.

Basically these 3 points go to the fact that the link support added here is defined too low into the object guts of YARD. How links are resolved is really a responsibility of the presentation layer, not the structure of the YARD object graph itself. This keeps with the separation of concerns in YARD's layered architecture-- the low level object database has no need for the concept of "links", as that is really only a concern of HTML, and possibly other output formats like PDF. A feature like this should therefore be placed appropriately in the layer that generates HTML/PDF, which is the Templates architecture.

Let's step back a bit and ask the question: what are we trying to solve? I think the answer to that is: "we want to generate proper links from @see tags that were written in a more readable manner". If that's the answer, then it's clear we really are just talking about the presentation layer. Since we are only talking about how things show up when we render them as HTML, we don't need to make any changes to the object graph.

There actually already is an extension point for plugins to make links prettier, and that is by customizing the Templates::Helpers::BaseHelper#linkify method to add support for new link prefixes and perform custom linking or generation syntaxes. In other words, this functionality is already somewhat supported. I would advise reading through that method to see how we do this in an extensible way. Note that this makes linking supported both with the {...} link syntax as well as any @see tag already, since all links go through the linkify method. You could override linkify to add the custom prefixes that your use case requires, such as "Appendix: Foo". For example:

module MyHelper
  def linkify(link, title, *args)
    if link == 'Appendix:'
      link_appendix(title)
    end
    super(link, *args)
  end

  def link_appendix(title)
    # replace with any custom lookup if you truly want to search for "titles"
    # though optimizing your plugin to do an O(1) lookup on the object path
    # would be much more efficient.
    link_object Registry.at(".appendix.#{object.path}.#{title}")
  end
end

module YARD::Templates::Helpers::BaseHelper
  include MyHelper
end

I think that's a much less obtrusive approach and does not affect the lower layers of YARD to build extra presentation functionality. And since this kind of usage is only introduced by plugins, forcing extra code to be written is reasonable (it's required even with this patch as well).

Arguably there might be easier ways to expose the extensibility of #linkify. I would argue that the reason you did not find this sooner was most likely a documentation problem. We can do more to have guides to overriding the link syntax in YARD and making it more obvious how to customize titles.

We can also look at making the code for this kind of use case easier to write. The above mixin code is probably a little confusing and is not multi-project server-safe. If we had an API to register prefixes or link parsers in the templating layer, it would be more obvious as to how one would create a custom syntax for the @see tag or {...} blocks. If you would be interested in repurposing your code to provide a better layer to defining these prefixes in the templating layer by refactoring the linkify method, that would be a better starting point to getting this kind of a feature accepted.

Maybe something like:

link_parser = LinkSyntaxParser.new
link_parser.register_link_prefix('file:', :link_file) # file:Foo calls link_file(...)
link_parser.register_link_prefix {|link, title| link_appendix(title) if link == 'Appendix:' }
...

BaseHelper.default_link_syntax_parser = link_parser
@amireh

Amazingly detailed response, thank you so much for pointing out the design flaw. I was actually rather concerned about this the moment I stepped into the Serializers, and indeed, faking the Proxy into resolving some non-object "links".

In truth, my first implementation was a really ugly patching of HtmlHelper. I used the alias method chain approach to overriding link_object and I didn't know I should be working on linkify instead. I also didn't know that mixin methods could call super, effectively building an inheritance chain - I'm really not a Ruby guy.

The LinkSyntaxParser approach looks clean, but I'm wondering about the last line where you register the default parser. Would plug-in writers have to do that? Why not have a single LinkSyntaxParser that tracks state of link prefixes -> callbacks? Something like this:

link_parser = BaseHelper.default_link_syntax_parser
link_parser.register_prefix('file:') { |title| lookup_and_link_file(title) }

Also, there should be no reason to restrict assigning a prefix to the callback like I did above. Your example with a callback that receives both link and title should be allowed too.

I'd be happy to implement this if we're good on the design. What do you think?

@lsegal
Owner

but I'm wondering about the last line where you register the default parser. Would plug-in writers have to do that? Why not have a single LinkSyntaxParser that tracks state of link prefixes -> callbacks?

This could be done. Having a default_link_syntax_parser properly allows you to easily reset state or inject a new parser without mutating existing ones. There are a number of ways we do this in YARD, all of which are workable.

The real issue here is that there has to be a way to reset state back to zero when a new object is being formatted. This is not possible with mixins, since you can't "unmixin" a module. I actually don't know the best way to do this.

You can look at the Tags::Library class for details on how we use "defaults", but typically what is usually done in these situations is at the start of parsing (or generating HTML in this case), the link parser attribute would be set to a clone or new instance of whatever the initial "default_link_syntax_parser" object was. That object can be mutated throughout generation and is then thrown away.

In the context of templating, that would mean the LinkSyntaxParser would be passed along with the TemplateOptions, i.e., options.link_syntax_parser, whereby its initial value would be options.link_syntax_parser = BaseHelper.default_link_syntax_parser.

The only problem here is that there is currently no way for a plugin to easily get access to an instance of the syntax parser, since there are no callbacks for when template generation has begun. This means a plugin has no hook to know when to start mutating the link_syntax_parser, and it doesn't even have an instance to mutate. You can hack this by hooking into a template, but this is not reliable. I think we'd need to add those hooks before this would be any more usable than just mixing a module into BaseHelper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 2, 2013
  1. @amireh

    Support for custom reference links for CodeObjects

    amireh authored
    The feature allows the registration of arbitrary "link keywords" that
    can be used to link against certain object types. This is handy when
    defining custom CodeObject implementations where referencing them by
    path is unnatural and not human readable.
    
    Modifications:
      * RegistryStore now tracks object_links which are a keyword ->
        object_type map
      * Proxy objects can now act as "links" and resolve themselves
      * HTMLHelper#link_object handles linkable Proxy objects
    
    There are specs that cover the added methods, and all
    the modifications are documented with proper YARD syntax.
This page is out of date. Refresh to see the latest.
View
17 lib/yard/code_objects/base.rb
@@ -540,6 +540,23 @@ def root?; false end
# and the name (default is {NSEP})
def sep; NSEP end
+ # Override this method if you've registered a link keyword to refer to this
+ # object. The method should return whether the passed title identifies this
+ # instance.
+ #
+ # @param [String] title the component after the registered keyword
+ # @return [Boolean] whether this object is indeed identified by the passed title
+ #
+ # @abstract Should be defined in the subclass that wants to be linked by keywords.
+ #
+ # @note This method is invoked internally by a Proxy object when resolving a link.
+ #
+ # @see Registry#register_link
+ # @see Proxy#resolve_link
+ def linked_by?(title)
+ false
+ end
+
protected
# Override this method if your code object subclass does not allow
View
30 lib/yard/code_objects/proxy.rb
@@ -213,6 +213,36 @@ def method_missing(meth, *args, &block)
# This class is never a root object
def root?; false end
+ # Does this Proxy represent a link to another object?
+ #
+ # @return [TrueClass] when #to_s returns a keyword registered as a link identifier
+ #
+ # @see Registry.type_from_link
+ def object_link?
+ Registry.type_from_link(to_s)
+ end
+
+ # Attempts to locate a CodeObject instance for which a link has been
+ # registered using the given title.
+ #
+ # @param [String] title the title that will be passed to CodeObjects::Base#linked_by?
+ # to identify the object
+ #
+ # @return [CodeObjects::Base] if an object was located
+ # @return nil otherwise
+ #
+ # @see Registry#register_link
+ # @see CodeObjects::Base#linked_by?
+ def resolve_link(title)
+ return if !title || title.empty?
+
+ if klass = object_link?
+ return Registry.all(klass.to_sym).select { |o| o.linked_by?(title) }.first
+ end
+
+ nil
+ end
+
private
# @note this method fixes a bug in 1.9.2: http://gist.github.com/437136
View
91 lib/yard/registry.rb
@@ -176,6 +176,62 @@ def register(object)
return if object.is_a?(CodeObjects::Proxy)
thread_local_store[object.path] = object
end
+
+ # Registers a link between a keyword and an object type found in the Registry.
+ #
+ # Links provide the means to link to objects in a docstring using arbitrary strings
+ # instead of paths. This is mostly useful for custom CodeObject implementations where
+ # the path is usually not human-readable.
+ #
+ # For example, to link to a custom SpecObject whose path might be `.specs.suite.should_do_something`,
+ # you can register a link that accepts a 'Spec: ' keyword, followed by the spec's description.
+ #
+ # The resolving of links is done in CodeObjects::Proxy#resolve_link which uses
+ # CodeObjects::Base#linked_by? to locate the link target.
+ #
+ # @example Linking to custom SpecObject instances using `Spec: spec title` syntax
+ # class SpecObject < YARD::CodeObjects::Base
+ # def type
+ # :spec
+ # end
+ #
+ # def linked_by?(title)
+ # @my_spec_name.to_s == title # you can use any attribute here to identify the object
+ # end
+ # end
+ #
+ # Registry.register_link('Spec:', :spec)
+ #
+ # # A method that's supposed to do something.
+ # # @see Spec: should do something
+ # def method_that_does_something() end
+ #
+ # @example Registering multiple keywords to the same type
+ # Registry.register_link('Spec:', :spec)
+ # Registry.register_link('Test:', :spec)
+ #
+ # @param [String] keyword the first token in the docstring that identifies the link,
+ # must be unique and contain no spaces (ie: 'Spec:', 'Appendix', 'Class:')
+ # @param [Symbol, CodeObjects::Base, CodeObjects::Proxy] type the type of objects
+ # to be linked to (ie: :spec, :class, :module, :method)
+ #
+ # @return [void]
+ #
+ # @see CodeObjects::Base#linked_by?
+ # @see CodeObjects::Proxy#resolve_link
+ def register_link(keyword, type)
+ unless keyword.is_a?(String) && !keyword.empty?
+ raise ArgumentError.new "keyword must be a valid string!"
+ end
+
+ unless type.is_a?(Symbol) || type.is_a?(CodeObjects::Base) || type.is_a?(CodeObjects::Proxy)
+ raise ArgumentError.new "type must be a Symbol or a CodeObject"
+ end
+
+ type = type.type unless Symbol === type
+
+ thread_local_store.put_link(keyword, type)
+ end
# Deletes an object from the registry
# @param [CodeObjects::Base] object the object to remove
@@ -183,6 +239,26 @@ def register(object)
def delete(object)
thread_local_store.delete(object.path)
end
+
+ # Deletes the registered links for the given type.
+ #
+ # If keyword is specified, only that keyword entry will be deleted. Otherwise,
+ # all the type link entries will be deleted.
+ #
+ # @param [Symbol, CodeObjects::Base, CodeObjects::Proxy] type the type of
+ # objects in the Registry that will be identified via this link
+ # @param [String] keyword the keyword that identifies the link
+ #
+ # @return [void]
+ def delete_link(type, keyword = nil)
+ unless type.is_a?(Symbol) || type.is_a?(CodeObjects::Base) || type.is_a?(CodeObjects::Proxy)
+ raise ArgumentError.new "type must be a Symbol or a CodeObject"
+ end
+
+ type = type.type unless Symbol === type
+
+ thread_local_store.delete_link(type, keyword)
+ end
# Clears the registry
# @return [void]
@@ -227,6 +303,21 @@ def all(*types)
def paths(reload = false)
thread_local_store.keys(reload).map {|k| k.to_s }
end
+
+ def links(reload = false)
+ thread_local_store.links(reload)
+ end
+
+ # @return [Symbol] the code object type that's linked to the given keyword
+ # @return nil if the keyword was not registered as a link to any type
+ #
+ # @note Used internally by CodeObjects::Proxy when resolving a link.
+ #
+ # @see Registry.register_link
+ # @see CodeObjects::Proxy#resolve_link
+ def type_from_link(keyword)
+ thread_local_store.type_from_link(keyword)
+ end
# Returns the object at a specific path.
# @param [String, :root] path the pathname to look for. If +path+ is +root+,
View
72 lib/yard/registry_store.rb
@@ -16,6 +16,7 @@ def initialize
@store = {}
@proxy_types = {}
@object_types = {:root => [:root]}
+ @object_links = {}
@notfound = {}
@loaded_objects = 0
@available_objects = 0
@@ -63,6 +64,34 @@ def put(key, value)
@store[key.to_sym] = value
end
end
+
+ # Associates a keyword with an object type to form a "link" that can
+ # be used in docstrings to reference objects using attributes other than paths.
+ #
+ # Keywords are unique; a keyword can be used to identify a single object
+ # type. However, multiple keywords can point to the same object type.
+ #
+ # @param [Symbol] type the type to link against (should be usable in Registry.all(:type))
+ # @param [String] keyword a single word to be used as a keyword for the link
+ #
+ # @see Registry.register_link
+ def put_link(keyword, type)
+ keyword = keyword.to_s.strip
+
+ # not registered yet?
+ unless @object_links[keyword]
+ @object_links[keyword] = type.to_sym
+ log.debug "CodeObject #{type.to_sym} can now be linked by '#{keyword.to_s.strip}'"
+ else
+ # overriding entries is not allowed
+ if type != @object_links[keyword]
+ raise ArgumentError.new %Q[
+ Link keyword '#{keyword}' is already mapped to
+ '#{@object_links[keyword]}', can not map to '#{type}' too!
+ ].gsub(/ +|\n/, ' ').strip
+ end
+ end
+ end
alias [] get
alias []= put
@@ -71,6 +100,17 @@ def put(key, value)
# @param [#to_sym] key the key to delete
# @return [void]
def delete(key) @store.delete(key.to_sym) end
+
+ # Removes a link entry(ies) for the given type.
+ #
+ # @param [Symbol] type the object type for which the links should be removed
+ # @param [String] keyword when passed, only the link with the matching keyword
+ # will be removed, otherwise all the object links will be
+ #
+ # @see Registry.delete_link
+ def delete_link(type, keyword = nil)
+ @object_links.delete_if { |k, t| t == type && (keyword ? k == keyword : true) }
+ end
# Gets all path names from the store. Loads the entire database
# if +reload+ is +true+
@@ -105,6 +145,21 @@ def values_for_type(type, reload = false)
load_all if reload
paths_for_type(type).map {|t| @store[t.to_sym] }
end
+
+ def links(reload = false) load_all if reload; @object_links end
+
+ # @return [Symbol] the code object type that's linked to the given keyword
+ # @return nil if the keyword was not registered as a link to any type
+ #
+ # @note While you shouldn't need to use this as it is internally used by CodeObjects::Proxy,
+ # the accessor lies in Registry.type_from_link instead.
+ #
+ # @see Registry.register_link
+ # @see Registry.type_from_link
+ # @see CodeObjects::Proxy#resolve_link
+ def type_from_link(keyword)
+ @object_links[keyword.to_s.strip]
+ end
# @return [CodeObjects::RootObject] the root object
def root; @store[:root] end
@@ -123,6 +178,7 @@ def load(file = nil)
@store = {}
@proxy_types = {}
@object_types = {}
+ @object_links = {}
@notfound = {}
@serializer = Serializers::YardocSerializer.new(@file)
load_yardoc
@@ -186,6 +242,7 @@ def save(merge = true, file = nil)
end
write_proxy_types
write_object_types
+ write_object_links
write_checksums
true
end
@@ -229,6 +286,10 @@ def checksums_path
def object_types_path
@serializer.object_types_path
end
+
+ def object_links_path
+ @serializer.object_links_path
+ end
def load_yardoc
return false unless @file
@@ -239,6 +300,7 @@ def load_yardoc
load_checksums
load_root
load_object_types
+ load_object_links
true
elsif File.file?(@file) # old format
load_yardoc_old
@@ -270,6 +332,12 @@ def load_object_types
end
end
+ def load_object_links
+ if File.file?(object_links_path)
+ @object_links = Marshal.load(File.read_binary(object_links_path))
+ end
+ end
+
def load_checksums
return unless File.file?(checksums_path)
lines = File.readlines(checksums_path).map do |line|
@@ -315,5 +383,9 @@ def write_checksums
@checksums.each {|k, v| f.puts("#{k} #{v}") }
end
end
+
+ def write_object_links
+ File.open!(object_links_path, 'wb') { |f| f.write(Marshal.dump(@object_links)) }
+ end
end
end
View
1  lib/yard/serializers/yardoc_serializer.rb
@@ -36,6 +36,7 @@ def objects_path; File.join(basepath, 'objects') end
def proxy_types_path; File.join(basepath, 'proxy_types') end
def checksums_path; File.join(basepath, 'checksums') end
def object_types_path; File.join(basepath, 'object_types') end
+ def object_links_path; File.join(basepath, 'object_links') end
def serialized_path(object)
path = case object
View
12 lib/yard/templates/helpers/html_helper.rb
@@ -246,8 +246,19 @@ def link_include_object(obj)
def link_object(obj, otitle = nil, anchor = nil, relative = true)
return otitle if obj.nil?
obj = Registry.resolve(object, obj, true, true) if obj.is_a?(String)
+
if !otitle && obj.root?
title = "Top Level Namespace"
+ elsif otitle && obj.is_a?(CodeObjects::Proxy)
+ # Check for any CodeObjects linked by custom keywords (@obj)
+ proxy = obj
+ if proxy.object_link?
+ if o = proxy.resolve_link(otitle)
+ title, obj = h(o.title), o
+ end
+ end
+
+ return h(obj.to_s) if !title || title.empty?
elsif otitle
title = otitle.to_s
elsif object.is_a?(CodeObjects::Base)
@@ -265,7 +276,6 @@ def link_object(obj, otitle = nil, anchor = nil, relative = true)
title = h(obj.to_s)
end
return title unless serializer
- return title if obj.is_a?(CodeObjects::Proxy)
link = url_for(obj, anchor, relative)
link = link ? link_url(link, title, :title => h("#{obj.title} (#{obj.type})")) : title
View
64 spec/code_objects/proxy_spec.rb
@@ -137,4 +137,68 @@ module B::C; def foo; end end
eof
Proxy.new(:root, 'B::C').should == Registry.at('A::C')
end
+
+ describe "link resolution" do
+ before do
+ Registry.clear
+ Registry.links.empty?.should be_true
+
+ eval <<-'eof'
+ module CodeObjects
+ class ClassObject < NamespaceObject
+ def linked_by?(title)
+ title.to_s == @name.to_s
+ end
+ end
+ end
+ eof
+ end
+
+ after do
+ eval <<-'eof'
+ module CodeObjects
+ class ClassObject < NamespaceObject
+ def linked_by?(title)
+ super
+ end
+ end
+ end
+ eof
+ end
+
+ it "should resolve a link to a linkable object using a keyword" do
+ Registry.register_link('Class:', :class)
+
+ YARD.parse_string 'class Foo; end'
+
+ P('Class:').resolve_link('Foo').should == Registry.at('Foo')
+ P('Class:').resolve_link('Foo').should be_true
+ end
+
+ it "should resolve a link to a linkable object using multiple keywords" do
+ Registry.register_link('Class:', :class)
+ Registry.register_link('Module:', :class)
+
+ YARD.parse_string 'class Foo; end'
+
+ P('Class:').resolve_link('Foo').should == Registry.at('Foo')
+ P('Module:').resolve_link('Foo').should == Registry.at('Foo')
+ end
+
+ it "should gracefully attempt to resolve a non-linkable object" do
+ YARD.parse_string 'class Foo; end'
+
+ P('Class:').resolve_link('Foo').should == nil
+ P('Module:').resolve_link('Foo').should == nil
+ end
+
+ it "should gracefully reject an empty object link" do
+ Registry.register_link('Class:', :class)
+
+ YARD.parse_string 'class Foo; end'
+
+ P('Class:').resolve_link(nil).should == nil
+ end
+
+ end
end
View
63 spec/registry_spec.rb
@@ -309,6 +309,69 @@ def self.Bar; end
Thread.new { Registry.single_object_db.should == nil }.join
end
end
+
+ describe '.register_link' do
+ it "should reject an invalid keyword" do
+ lambda { Registry.register_link(nil, :class) }.should raise_error(ArgumentError)
+ lambda { Registry.register_link('', :class) }.should raise_error(ArgumentError)
+ end
+
+ it "should reject an invalid type" do
+ lambda { Registry.register_link('Class: ', 'Class') }.should raise_error(ArgumentError)
+ end
+
+ it "should register a link between a type and a keyword" do
+ Registry.links.count.should == 0
+ lambda { Registry.register_link('Class: ', :class) }.should_not raise_error
+ Registry.links.count.should == 1
+ end
+
+ it "should map multiple keywords to the same type" do
+ Registry.links.count.should == 0
+ lambda { Registry.register_link('Class: ', :class) }.should_not raise_error
+ lambda { Registry.register_link('Module: ', :class) }.should_not raise_error
+ Registry.links.count.should == 2
+ end
+
+ it "should not register a link between a type and a keyword more than once" do
+ Registry.links.count.should == 0
+ Registry.register_link('Class: ', :class)
+ Registry.register_link('Class: ', :class)
+ Registry.links.count.should == 1
+ end
+
+ it "should not override an entry" do
+ Registry.register_link('ABC', :class)
+ Registry.links['ABC'].should == :class
+ lambda { Registry.register_link('ABC', :method) }.should raise_error(ArgumentError)
+ Registry.links['ABC'].should == :class
+ end
+
+ end
+
+ describe '.delete_link' do
+ it "should remove a link" do
+ Registry.register_link('Class: ', :class)
+ Registry.links.count.should == 1
+ Registry.delete_link(:class)
+ Registry.links.count.should == 0
+ end
+
+ it "should gracefully handle removing of a non-registered link" do
+ Registry.links.count.should == 0
+ lambda { Registry.delete_link(:class) }.should_not raise_error
+ Registry.links.count.should == 0
+ end
+ end
+
+ describe '.clear' do
+ it "should delete all registered links" do
+ Registry.register_link('Class:', :class)
+ Registry.links.count.should == 1
+ Registry.clear
+ Registry.links.count.should == 0
+ end
+ end
describe 'Thread local' do
it "should maintain two Registries in separate threads" do
View
8 spec/registry_store_spec.rb
@@ -16,6 +16,7 @@
File.should_receive(:file?).with('foo/checksums').and_return(false)
File.should_receive(:file?).with('foo/proxy_types').and_return(false)
File.should_receive(:file?).with('foo/object_types').and_return(false)
+ File.should_receive(:file?).with('foo/object_links').and_return(false)
@serializer.should_receive(:deserialize).with('root').and_return({:root => @foo, :A => @bar})
@store.load('foo').should == true
@store.root.should == @foo
@@ -36,6 +37,7 @@
File.should_receive(:file?).with('foo/checksums').and_return(false)
File.should_receive(:file?).with('foo/proxy_types').and_return(false)
File.should_receive(:file?).with('foo/object_types').and_return(false)
+ File.should_receive(:file?).with('foo/object_links').and_return(false)
File.should_receive(:file?).with('foo/objects/root.dat').and_return(false)
@store.load('foo').should == true
@@ -53,6 +55,7 @@
File.should_receive(:file?).with('foo/checksums').and_return(false)
File.should_receive(:file?).with('foo/proxy_types').and_return(false)
File.should_receive(:file?).with('foo/object_types').and_return(false)
+ File.should_receive(:file?).with('foo/object_links').and_return(false)
File.should_receive(:file?).with('foo/objects/root.dat').and_return(false)
@store.load('foo').should == true
end
@@ -71,6 +74,7 @@
File.should_receive(:file?).with('foo/proxy_types').and_return(false)
File.should_receive(:file?).with('foo/objects/root.dat').and_return(false)
File.should_receive(:file?).with('foo/object_types').and_return(false)
+ File.should_receive(:file?).with('foo/object_links').and_return(false)
File.should_receive(:readlines).with('foo/checksums').and_return([
'file1 CHECKSUM1', ' file2 CHECKSUM2 '
])
@@ -83,6 +87,7 @@
File.should_receive(:file?).with('foo/checksums').and_return(false)
File.should_receive(:file?).with('foo/proxy_types').and_return(true)
File.should_receive(:file?).with('foo/object_types').and_return(false)
+ File.should_receive(:file?).with('foo/object_links').and_return(false)
File.should_receive(:file?).with('foo/objects/root.dat').and_return(false)
File.should_receive(:read_binary).with('foo/proxy_types').and_return(Marshal.dump({'a' => 'b'}))
@store.load('foo').should == true
@@ -94,6 +99,7 @@
File.should_receive(:file?).with('foo/checksums').and_return(false)
File.should_receive(:file?).with('foo/proxy_types').and_return(false)
File.should_receive(:file?).with('foo/object_types').and_return(false)
+ File.should_receive(:file?).with('foo/object_links').and_return(false)
File.should_receive(:file?).with('foo/objects/root.dat').and_return(true)
File.should_receive(:read_binary).with('foo/objects/root.dat').and_return(Marshal.dump(@foo))
@store.load('foo').should == true
@@ -214,6 +220,7 @@ def saves_to_multidb
File.should_receive(:file?).with('foo/checksums').and_return(false)
File.should_receive(:file?).with('foo/proxy_types').and_return(false)
File.should_receive(:file?).with('foo/object_types').and_return(false)
+ File.should_receive(:file?).with('foo/object_links').and_return(false)
@serializer.should_receive(:deserialize).with('root').and_return({:'A#foo' => @foo, :A => @bar})
@store.load('foo')
@store.paths_for_type(:method).should == ['#foo']
@@ -256,6 +263,7 @@ def saves_to_multidb
File.should_receive(:directory?).with('foo').and_return(true)
File.should_receive(:file?).with('foo/proxy_types').and_return(false)
File.should_receive(:file?).with('foo/object_types').and_return(false)
+ File.should_receive(:file?).with('foo/object_links').and_return(false)
File.should_receive(:file?).with('foo/checksums').and_return(false)
File.should_receive(:file?).with('foo/objects/root.dat').and_return(false)
@store.should_receive(:all_disk_objects).at_least(1).times.and_return(['foo/objects/foo', 'foo/objects/bar'])
View
49 spec/templates/helpers/html_helper_spec.rb
@@ -217,6 +217,55 @@ def a; end
link_object("Bar").should =~ %r{>TITLE!</a>}
end
+ it "should use custom links if defined" do
+ eval <<-'eof'
+ class LinkableSpec < YARD::CodeObjects::Base
+ def type; :linkable_spec end
+ def title; "Spec: #{name}" end
+ def path; ".#{type}.#{namespace.path}.#{@name.to_s.gsub(/\s/, '_')}" end
+ def linked_by?(text)
+ @name.to_s == text.to_s
+ end
+ end
+ eof
+
+ docstr = <<-'eof'
+ # Some class.
+ #
+ # @see Spec: it should succeed
+ # @see .linkable_spec.Foo.it_should_succeed
+ class Foo
+ end
+ eof
+
+ YARD.parse_string docstr
+ Registry.register_link('Spec:', :linkable_spec)
+
+ stub!(:object).and_return(Registry.at('Foo'))
+ LinkableSpec.new(P('Foo'), "it should succeed")
+ serializer = Serializers::FileSystemSerializer.new
+ stub!(:serializer).and_return(serializer)
+ implicit = link_object("Spec: ", "it should succeed")
+ explicit = link_object(P(".linkable_spec.Foo.it_should_succeed"))
+
+ implicit.should == explicit
+
+ Registry.clear
+
+ # now we try the original behaviour by un-registering the linkage keyword
+ Registry.delete_link(:linkable_spec, 'Spec:')
+
+ YARD.parse_string docstr
+
+ stub!(:object).and_return(Registry.at('Foo'))
+ LinkableSpec.new(P('Foo'), "it should succeed")
+ serializer = Serializers::FileSystemSerializer.new
+ stub!(:serializer).and_return(serializer)
+ implicit = link_object("Spec: ", "it should succeed")
+ explicit = link_object(P(".linkable_spec.Foo.it_should_succeed"))
+ implicit.should_not == explicit
+ end
+
it "should use relative path to parent class in title" do
root = CodeObjects::ModuleObject.new(:root, :YARD)
obj = CodeObjects::ModuleObject.new(root, :SubModule)
Something went wrong with that request. Please try again.