yard-virtus2 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/.idea/modules.xml +8 -0
  4. data/Gemfile +17 -0
  5. data/Gemfile.lock +75 -0
  6. data/Guardfile +11 -0
  7. data/LICENSE.txt +19 -0
  8. data/README.md +61 -0
  9. data/Rakefile +9 -0
  10. data/example/README.md +4 -0
  11. data/example/address.rb +7 -0
  12. data/example/city.rb +5 -0
  13. data/example/user.rb +9 -0
  14. data/lib/yard-virtus.rb +18 -0
  15. data/lib/yard/virtus/code_objects.rb +2 -0
  16. data/lib/yard/virtus/code_objects/attribute_reader.rb +26 -0
  17. data/lib/yard/virtus/code_objects/attribute_writer.rb +48 -0
  18. data/lib/yard/virtus/declarations.rb +4 -0
  19. data/lib/yard/virtus/declarations/options.rb +53 -0
  20. data/lib/yard/virtus/declarations/type.rb +71 -0
  21. data/lib/yard/virtus/declarations/virtus_attribute.rb +73 -0
  22. data/lib/yard/virtus/declarations/virtus_model.rb +40 -0
  23. data/lib/yard/virtus/handlers.rb +2 -0
  24. data/lib/yard/virtus/handlers/include_virtus_model.rb +32 -0
  25. data/lib/yard/virtus/handlers/virtus_attribute.rb +58 -0
  26. data/lib/yard/virtus/mixin_handler_monkey_patch.rb +25 -0
  27. data/lib/yard/virtus/version.rb +5 -0
  28. data/spec/code_objects/attribute_reader_spec.rb +38 -0
  29. data/spec/code_objects/attribute_writer_spec.rb +75 -0
  30. data/spec/declarations/options_spec.rb +50 -0
  31. data/spec/declarations/type_spec.rb +37 -0
  32. data/spec/declarations/virtus_attribute_spec.rb +73 -0
  33. data/spec/declarations/virtus_model_spec.rb +35 -0
  34. data/spec/examples/include_virtus_model_001.rb.txt +15 -0
  35. data/spec/examples/virtus_attribute_001.rb.txt +65 -0
  36. data/spec/handlers/include_virtus_model_spec.rb +22 -0
  37. data/spec/handlers/virtus_attribute_spec.rb +67 -0
  38. data/spec/spec_helper.rb +65 -0
  39. data/spec/support/helpers/handler_helpers.rb +9 -0
  40. data/spec/support/helpers/parsing_helpers.rb +13 -0
  41. data/spec/support/matchers/attribute_matchers.rb +39 -0
  42. data/spec/support/matchers/method_type_matchers.rb +11 -0
  43. data/yard-virtus2.gemspec +22 -0
  44. metadata +119 -0
@@ -0,0 +1,73 @@
1
+ module YARD
2
+ module Virtus
3
+ module Declarations
4
+ # VirtusModel declaration wraps AST which represents
5
+ # call to `attribute` method.
6
+ # It's job is to provide information which documents attribute.
7
+ class VirtusAttribute
8
+ attr_reader :ast
9
+
10
+ # @param [YARD::Parser::Ruby::MethodCallNode] ast
11
+ def initialize(ast)
12
+ @ast = ast
13
+ @options = Options.new(parameters[2])
14
+ end
15
+
16
+ def readable?
17
+ true
18
+ end
19
+
20
+ def writable?
21
+ true
22
+ end
23
+
24
+ # Predicate to check if attribute has private writer.
25
+ def has_private_writer?
26
+ options[:writer] == :private
27
+ end
28
+
29
+ # Name of the attribute.
30
+ # @return [Symbol]
31
+ def attr_name
32
+ parameters.first.jump(:ident).first.to_sym
33
+ end
34
+
35
+ # Type of the attribute in YARD format.
36
+ # @return [String]
37
+ def type
38
+ Type.new(type_param).yard_type_string
39
+ end
40
+
41
+ def attribute_reader
42
+ CodeObjects::AttributeReader.new(attr_name, type)
43
+ end
44
+
45
+ def attribute_writer
46
+ CodeObjects::AttributeWriter.new(attr_name, type, has_private_writer?)
47
+ end
48
+
49
+ protected
50
+ attr_reader :options
51
+
52
+ def parameters
53
+ ast.parameters(false)
54
+ end
55
+
56
+ def type_param
57
+ parameters[1]
58
+ end
59
+
60
+ def scalar_type_to_string(ref)
61
+ ref.path.join("::")
62
+ end
63
+
64
+ def collection_type_to_string(type)
65
+ collection_type = scalar_type_to_string(type_param[0])
66
+ element_type = scalar_type_to_string(type_param[1].jump(:var_ref))
67
+
68
+ "#{collection_type}<#{element_type}>"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,40 @@
1
+ module YARD
2
+ module Virtus
3
+ module Declarations
4
+ # VirtusModel declaration wraps AST which represents mixin of Virtus.
5
+ # It's job is to provide information about Virtus features mixed-in
6
+ # via Virtus declaration.
7
+ #
8
+ # @example
9
+ # # This is AST for mixin source of `include Virtus.model`.
10
+ # ast = s(:call, s(:var_ref, s(:const, "Virtus")),
11
+ # :".",
12
+ # s(:ident, "model"))
13
+ #
14
+ # model = YARD::Virtus::Declarations::VirtusModel.new(ast)
15
+ # model.module_proxies_in_ns(ns) # => P("Virtus.model")
16
+ class VirtusModel
17
+ attr_reader :ast
18
+
19
+ # @param [YARD::Parser::Ruby::MethodCallNode] ast
20
+ def initialize(ast)
21
+ @ast = ast
22
+ end
23
+
24
+ # Get list of proxies to modules which document fetaures inherited
25
+ # via mixin.
26
+ #
27
+ # @param [YARD::CodeObjects::ClassObject] namespace
28
+ # @return [Array<YARD::CodeObjects::Proxy>]
29
+ def module_proxies_in_ns(namespace)
30
+ [YARD::CodeObjects::Proxy.new(namespace, mixin_name, :module)]
31
+ end
32
+
33
+ protected
34
+ def mixin_name
35
+ "%s.%s" % [ast.namespace, ast.method_name].map(&:source)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,2 @@
1
+ require File.join(File.dirname(__FILE__), "handlers", "include_virtus_model")
2
+ require File.join(File.dirname(__FILE__), "handlers", "virtus_attribute")
@@ -0,0 +1,32 @@
1
+ module YARD
2
+ module Virtus
3
+ module Handlers
4
+ class IncludeVirtusModel < YARD::Handlers::Ruby::Base
5
+ handles method_call(:include)
6
+ namespace_only
7
+
8
+ def process
9
+ raise YARD::Handlers::HandlerAborted unless virtus_module?
10
+
11
+ declaration = Declarations::VirtusModel.new(virtus_call)
12
+ declaration.module_proxies_in_ns(namespace).each do |proxy|
13
+ namespace.mixins(scope).unshift(proxy)
14
+ end
15
+
16
+ namespace[:supports_virtus_attributes] = true
17
+ end
18
+
19
+ protected
20
+
21
+ def virtus_module?
22
+ included_module = statement.parameters.jump(:var_ref)
23
+ included_module and included_module.source == "Virtus"
24
+ end
25
+
26
+ def virtus_call
27
+ statement.parameters(false)[0]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,58 @@
1
+ module YARD
2
+ module Virtus
3
+ module Handlers
4
+ class VirtusAttribute < YARD::Handlers::Ruby::Base
5
+ handles method_call(:attribute)
6
+ namespace_only
7
+
8
+ def process
9
+ raise YARD::Handlers::HandlerAborted unless virtus_model?
10
+
11
+ declaration = Declarations::VirtusAttribute.new(statement)
12
+
13
+
14
+ register_attribute!(declaration.attribute_reader, :read) if declaration.readable?
15
+ register_attribute!(declaration.attribute_writer, :write) if declaration.writable?
16
+ end
17
+
18
+ protected
19
+
20
+ # @param [CodeObjects::AttributeReader, CodeObjects::AttributeWriter] mobject
21
+ # @param [YARD::CodeObjects::MethodObject] mobject
22
+ # @param [:read, :write] type
23
+ def register_attribute!(mobject, type)
24
+ yard_mobject = mobject.yard_method_object(namespace)
25
+
26
+ register_preserving_tags!(yard_mobject)
27
+ attributes_data_for(mobject.attr_name)[type] = yard_mobject
28
+ end
29
+
30
+ def attributes_data_for(name)
31
+ namespace.attributes[scope][name] ||= SymbolHash[:read => nil, :write => nil]
32
+ end
33
+
34
+ def virtus_model?
35
+ namespace.inheritance_tree(true).any? { |n| n[:supports_virtus_attributes] == true }
36
+ end
37
+
38
+ # When you register an object it can get assigned docstring which
39
+ # followed statement which was handled. If such a docstring exists it
40
+ # can result in removal of previously extracted tags like `@return` and
41
+ # `@private` as well as default values. To prevent it we restore all
42
+ # tags which disappeared after registration.
43
+ #
44
+ # Do you love uncontrolled mutations as much as I do?
45
+ def register_preserving_tags!(object)
46
+ tags_before_registration = object.tags
47
+
48
+ register(object)
49
+
50
+ lost_tags = tags_before_registration - object.tags
51
+ if lost_tags.size > 0
52
+ object.add_tag(*lost_tags)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ # Standard YARD mixin handler can not process mixins which include method call.
2
+ # It throws UndocumentableError when it encounters any virtus mixin because they
3
+ # all include method call:
4
+ #
5
+ # [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class City
6
+ # [warn]: in file 'example/city.rb':2:
7
+ #
8
+ # 2: include Virtus.model
9
+ #
10
+ # This monkey patch aborts parsing of statement instead of raising UndocumentableError.
11
+ class YARD::Handlers::Ruby::MixinHandler < YARD::Handlers::Ruby::Base
12
+ protected
13
+ alias_method :original_process_mixin, :process_mixin
14
+
15
+ def process_mixin(mixin)
16
+ raise YARD::Handlers::HandlerAborted if virtus_module?(mixin)
17
+
18
+ original_process_mixin(mixin)
19
+ end
20
+
21
+ def virtus_module?(mixin)
22
+ included_module = mixin.jump(:var_ref)
23
+ included_module and included_module.source == "Virtus"
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ module YARD
2
+ module Virtus
3
+ VERSION = "0.0.5"
4
+ end
5
+ end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ describe YARD::Virtus::CodeObjects::AttributeReader do
4
+ subject { described_class.new(:title, "String") }
5
+
6
+ it "has #attr_name" do
7
+ expect(subject.attr_name).to eq(:title)
8
+ end
9
+
10
+ it "has #type" do
11
+ expect(subject.type).to eq("String")
12
+ end
13
+
14
+ it "has #method_name" do
15
+ expect(subject.method_name).to eq(:title)
16
+ end
17
+
18
+ describe "#yard_method_object" do
19
+ let(:reader) { described_class.new(:title, "String") }
20
+ let(:namespace) { YARD::CodeObjects::ClassObject.new(nil, "TemporarySpecClass") }
21
+
22
+ subject { reader.yard_method_object(namespace) }
23
+
24
+ it { expect(subject).to be_instance_of(YARD::CodeObjects::MethodObject) }
25
+
26
+ it "is not explicit" do
27
+ expect(subject.is_explicit?).to be_false
28
+ end
29
+
30
+ it "has the same name as attribute it reads" do
31
+ expect(subject.name).to eq(:title)
32
+ end
33
+
34
+ it "has @return tag" do
35
+ expect(subject.has_tag?(:return)).to be_true
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,75 @@
1
+ require "spec_helper"
2
+
3
+ describe YARD::Virtus::CodeObjects::AttributeWriter do
4
+ before :each do
5
+ # All YARD::CodeObjects::* objects are added to
6
+ # registry on creation which causes conflicts in
7
+ # this test.
8
+ YARD::Registry.clear
9
+ end
10
+
11
+ subject { described_class.new(:title, "String") }
12
+
13
+ it "has #attr_name" do
14
+ expect(subject.attr_name).to eq(:title)
15
+ end
16
+
17
+ it "has #type" do
18
+ expect(subject.type).to eq("String")
19
+ end
20
+
21
+ it "has #method_name" do
22
+ expect(subject.method_name).to eq(:"title=")
23
+ end
24
+
25
+ describe "#yard_method_object" do
26
+ let(:writer) { described_class.new(:title, "String") }
27
+ let(:namespace) { YARD::CodeObjects::ClassObject.new(nil, "TemporarySpecClass") }
28
+
29
+ subject { writer.yard_method_object(namespace) }
30
+
31
+ it { expect(subject).to be_instance_of(YARD::CodeObjects::MethodObject) }
32
+
33
+ it "is not explicit" do
34
+ expect(subject.is_explicit?).to be_false
35
+ end
36
+
37
+ it "has the writer name for attribute" do
38
+ expect(subject.name).to eq(:"title=")
39
+ end
40
+
41
+ it "has parameter" do
42
+ expect(subject.parameters).not_to be_empty
43
+ end
44
+
45
+ it "has type signature tag for parameter" do
46
+ param_tags = subject.tags(:param).select { |t| t.name == "value" }
47
+
48
+ expect(param_tags).not_to be_empty
49
+ end
50
+
51
+ context "when writer visibility is not specified" do
52
+ let(:writer) { described_class.new(:title, "String") }
53
+
54
+ it "does not have @private tag" do
55
+ expect(subject.tags(:private)).to be_empty
56
+ end
57
+ end
58
+
59
+ context "when writer is public" do
60
+ let(:writer) { described_class.new(:title, "String", false) }
61
+
62
+ it "does not have @private tag" do
63
+ expect(subject.tags(:private)).to be_empty
64
+ end
65
+ end
66
+
67
+ context "when writer is private" do
68
+ let(:writer) { described_class.new(:title, "String", true) }
69
+
70
+ it "has @private tag" do
71
+ expect(subject.tags(:private)).to have(1).item
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,50 @@
1
+ require "spec_helper"
2
+
3
+ describe YARD::Virtus::Declarations::Options do
4
+ def self.example(src, &block)
5
+ c = context "'#{src}'"
6
+ # We can not generate AST for `:a => 1` so we wrap it into
7
+ # method call like in `attribute :a => 1` and then extract
8
+ # part corresponding to options. AST for wrapped code will look
9
+ # like this:
10
+ #
11
+ # s(:command,
12
+ # s(:ident, "attribute"),
13
+ # s(s(s(:assoc,
14
+ # s(:symbol_literal, s(:symbol, s(:ident, "a"))),
15
+ # s(:int, "1"))), false))
16
+ #
17
+ c.let(:ast) { ruby_ast("attribute #{src}")[1] }
18
+ c.class_eval(&block)
19
+ end
20
+
21
+ subject { described_class.new(ast) }
22
+
23
+ example "" do
24
+ it { expect(subject).to be_empty }
25
+ end
26
+
27
+ example ":a => :b" do
28
+ it { expect(subject).not_to be_empty }
29
+
30
+ it "has information about key :a" do
31
+ expect(subject[:a]).to eq(:b)
32
+ end
33
+ end
34
+
35
+ example "a: :b" do
36
+ it { expect(subject).not_to be_empty }
37
+
38
+ it "has information about key :a" do
39
+ expect(subject[:a]).to eq(:b)
40
+ end
41
+ end
42
+
43
+ example ":default => lambda { 123 }" do
44
+ it { expect(subject).not_to be_empty }
45
+
46
+ it "has information about lambda function" do
47
+ expect(subject[:default]).to be_kind_of(Proc)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe YARD::Virtus::Declarations::Type do
4
+ def self.example(src, &block)
5
+ c = context "'#{src}'"
6
+ c.let(:ast) { ruby_ast(src) }
7
+ c.class_eval(&block)
8
+ end
9
+
10
+ describe "#yard_type_string" do
11
+ let(:subject) { described_class.new(ast).yard_type_string }
12
+
13
+ example "String" do
14
+ it { expect(subject).to eq "String" }
15
+ end
16
+
17
+ example "Some::Nested::String" do
18
+ it { expect(subject).to eq "Some::Nested::String" }
19
+ end
20
+
21
+ example "Array[String]" do
22
+ it { expect(subject).to eq "Array<String>" }
23
+ end
24
+
25
+ example "Array[Nested::String]" do
26
+ it { expect(subject).to eq "Array<Nested::String>" }
27
+ end
28
+
29
+ example "Collection[Address]" do
30
+ it { expect(subject).to eq "Collection<Address>" }
31
+ end
32
+
33
+ example "Hash[String => Integer]" do
34
+ it { expect(subject).to eq "Hash{String => Integer}" }
35
+ end
36
+ end
37
+ end