yard-virtus2 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/.idea/modules.xml +8 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +75 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +19 -0
- data/README.md +61 -0
- data/Rakefile +9 -0
- data/example/README.md +4 -0
- data/example/address.rb +7 -0
- data/example/city.rb +5 -0
- data/example/user.rb +9 -0
- data/lib/yard-virtus.rb +18 -0
- data/lib/yard/virtus/code_objects.rb +2 -0
- data/lib/yard/virtus/code_objects/attribute_reader.rb +26 -0
- data/lib/yard/virtus/code_objects/attribute_writer.rb +48 -0
- data/lib/yard/virtus/declarations.rb +4 -0
- data/lib/yard/virtus/declarations/options.rb +53 -0
- data/lib/yard/virtus/declarations/type.rb +71 -0
- data/lib/yard/virtus/declarations/virtus_attribute.rb +73 -0
- data/lib/yard/virtus/declarations/virtus_model.rb +40 -0
- data/lib/yard/virtus/handlers.rb +2 -0
- data/lib/yard/virtus/handlers/include_virtus_model.rb +32 -0
- data/lib/yard/virtus/handlers/virtus_attribute.rb +58 -0
- data/lib/yard/virtus/mixin_handler_monkey_patch.rb +25 -0
- data/lib/yard/virtus/version.rb +5 -0
- data/spec/code_objects/attribute_reader_spec.rb +38 -0
- data/spec/code_objects/attribute_writer_spec.rb +75 -0
- data/spec/declarations/options_spec.rb +50 -0
- data/spec/declarations/type_spec.rb +37 -0
- data/spec/declarations/virtus_attribute_spec.rb +73 -0
- data/spec/declarations/virtus_model_spec.rb +35 -0
- data/spec/examples/include_virtus_model_001.rb.txt +15 -0
- data/spec/examples/virtus_attribute_001.rb.txt +65 -0
- data/spec/handlers/include_virtus_model_spec.rb +22 -0
- data/spec/handlers/virtus_attribute_spec.rb +67 -0
- data/spec/spec_helper.rb +65 -0
- data/spec/support/helpers/handler_helpers.rb +9 -0
- data/spec/support/helpers/parsing_helpers.rb +13 -0
- data/spec/support/matchers/attribute_matchers.rb +39 -0
- data/spec/support/matchers/method_type_matchers.rb +11 -0
- data/yard-virtus2.gemspec +22 -0
- 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,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,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
|