yard-virtus 0.0.4

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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/Gemfile +17 -0
  4. data/Gemfile.lock +75 -0
  5. data/Guardfile +12 -0
  6. data/LICENSE.txt +19 -0
  7. data/README.md +49 -0
  8. data/Rakefile +7 -0
  9. data/example/README.md +4 -0
  10. data/example/address.rb +7 -0
  11. data/example/city.rb +5 -0
  12. data/example/user.rb +9 -0
  13. data/lib/yard-virtus.rb +28 -0
  14. data/lib/yard/virtus/code_objects.rb +2 -0
  15. data/lib/yard/virtus/code_objects/attribute_reader.rb +26 -0
  16. data/lib/yard/virtus/code_objects/attribute_writer.rb +48 -0
  17. data/lib/yard/virtus/declarations.rb +4 -0
  18. data/lib/yard/virtus/declarations/options.rb +43 -0
  19. data/lib/yard/virtus/declarations/type.rb +65 -0
  20. data/lib/yard/virtus/declarations/virtus_attribute.rb +67 -0
  21. data/lib/yard/virtus/declarations/virtus_model.rb +33 -0
  22. data/lib/yard/virtus/handlers.rb +2 -0
  23. data/lib/yard/virtus/handlers/include_virtus_model.rb +32 -0
  24. data/lib/yard/virtus/handlers/virtus_attribute.rb +58 -0
  25. data/lib/yard/virtus/version.rb +5 -0
  26. data/spec/code_objects/attribute_reader_spec.rb +38 -0
  27. data/spec/code_objects/attribute_writer_spec.rb +75 -0
  28. data/spec/declarations/options_spec.rb +50 -0
  29. data/spec/declarations/type_spec.rb +37 -0
  30. data/spec/declarations/virtus_attribute_spec.rb +73 -0
  31. data/spec/declarations/virtus_model_spec.rb +35 -0
  32. data/spec/examples/include_virtus_model_001.rb.txt +15 -0
  33. data/spec/examples/virtus_attribute_001.rb.txt +67 -0
  34. data/spec/handlers/include_virtus_model_spec.rb +22 -0
  35. data/spec/handlers/virtus_attribute_spec.rb +71 -0
  36. data/spec/spec_helper.rb +65 -0
  37. data/spec/support/helpers/handler_helpers.rb +9 -0
  38. data/spec/support/helpers/parsing_helpers.rb +13 -0
  39. data/spec/support/matchers/attribute_matchers.rb +45 -0
  40. data/spec/support/matchers/method_type_matchers.rb +11 -0
  41. data/yard-virtus.gemspec +22 -0
  42. metadata +117 -0
@@ -0,0 +1,67 @@
1
+ module YARD
2
+ module Virtus
3
+ module Declarations
4
+ # VirtusModel declaration wraps AST which represents
5
+ # call to `attribute` method
6
+ class VirtusAttribute
7
+ attr_reader :ast
8
+
9
+ # @params [YARD::Parser::Ruby::MethodCallNode] ast
10
+ def initialize(ast)
11
+ @ast = ast
12
+ @options = Options.new(parameters[2])
13
+ end
14
+
15
+ def readable?
16
+ true
17
+ end
18
+
19
+ def writable?
20
+ true
21
+ end
22
+
23
+ def has_private_writer?
24
+ options[:writer] == :private
25
+ end
26
+
27
+ def attr_name
28
+ parameters.first.jump(:ident).first.to_sym
29
+ end
30
+
31
+ def type
32
+ Type.new(type_param).yard_type_string
33
+ end
34
+
35
+ def attribute_reader
36
+ CodeObjects::AttributeReader.new(attr_name, type)
37
+ end
38
+
39
+ def attribute_writer
40
+ CodeObjects::AttributeWriter.new(attr_name, type, has_private_writer?)
41
+ end
42
+
43
+ protected
44
+ attr_reader :options
45
+
46
+ def parameters
47
+ ast.parameters(false)
48
+ end
49
+
50
+ def type_param
51
+ parameters[1]
52
+ end
53
+
54
+ def scalar_type_to_string(ref)
55
+ ref.path.join("::")
56
+ end
57
+
58
+ def collection_type_to_string(type)
59
+ collection_type = scalar_type_to_string(type_param[0])
60
+ element_type = scalar_type_to_string(type_param[1].jump(:var_ref))
61
+
62
+ "#{collection_type}<#{element_type}>"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,33 @@
1
+ module YARD
2
+ module Virtus
3
+ module Declarations
4
+ # VirtusModel declaration wraps AST which represents
5
+ # inclusion of Virtus and allows to extract useful data
6
+ # like module proxies from it.
7
+ #
8
+ # @example
9
+ # ast = s(:call, s(:var_ref, s(:const, "Virtus")), :".", s(:ident, "model"))
10
+ # d = VirtusModel.new(ast)
11
+ # d.module_proxies_in_ns(ns) # => P("Virtus.model")
12
+ #
13
+ class VirtusModel
14
+ attr_reader :ast
15
+
16
+ # @params [YARD::Parser::Ruby::MethodCallNode] ast
17
+ def initialize(ast)
18
+ @ast = ast
19
+ end
20
+
21
+ # @param [YARD::CodeObjects::ClassObject] namespace
22
+ def module_proxies_in_ns(namespace)
23
+ [YARD::CodeObjects::Proxy.new(namespace, mixin_name, :module)]
24
+ end
25
+
26
+ protected
27
+ def mixin_name
28
+ "%s.%s" % [ast.namespace, ast.method_name].map(&:source)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ 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,5 @@
1
+ module YARD
2
+ module Virtus
3
+ VERSION = "0.0.4"
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
@@ -0,0 +1,73 @@
1
+ require "spec_helper"
2
+
3
+ describe YARD::Virtus::Declarations::VirtusAttribute do
4
+ let(:ast) { ruby_ast(declaration) }
5
+ let(:namespace) { YARD::CodeObjects::ClassObject.new(nil, "TemporarySpecClass") }
6
+ let(:subject) { described_class.new(ast) }
7
+
8
+ def self.example(src, &block)
9
+ c = context "'#{src}'"
10
+ c.let(:declaration) { src }
11
+ c.class_eval(&block)
12
+ end
13
+
14
+ example "attribute :title, String" do
15
+ it { expect(subject).to be_readable }
16
+ it { expect(subject).to be_writable }
17
+
18
+ it { expect(subject.attr_name).to eq(:title) }
19
+ it { expect(subject.type).to eq("String") }
20
+
21
+ it "produces reader method object" do
22
+ expect(subject.attribute_reader).to be_kind_of(YARD::Virtus::CodeObjects::AttributeReader)
23
+ end
24
+
25
+ it "produces writer method object" do
26
+ expect(subject.attribute_writer).to be_kind_of(YARD::Virtus::CodeObjects::AttributeWriter)
27
+ end
28
+ end
29
+
30
+ example "attribute :title, Namespaced::String" do
31
+ it { expect(subject).to be_readable }
32
+ it { expect(subject).to be_writable }
33
+
34
+ it { expect(subject.attr_name).to eq(:title) }
35
+ it { expect(subject.type).to eq("Namespaced::String") }
36
+ end
37
+
38
+ example "attribute :locations, Array[Address]" do
39
+ it { expect(subject).to be_readable }
40
+ it { expect(subject).to be_writable }
41
+
42
+ it { expect(subject.attr_name).to eq(:locations) }
43
+ it { expect(subject.type).to eq("Array<Address>") }
44
+ end
45
+
46
+ example "attribute :locations, Array[Array[Point]]" do
47
+ it { expect(subject).to be_readable }
48
+ it { expect(subject).to be_writable }
49
+
50
+ it { expect(subject.attr_name).to eq(:locations) }
51
+ it { expect(subject.type).to eq("Array<Array<Point>>") }
52
+ end
53
+
54
+ example "attribute :locations, Hash[Symbol => Address]" do
55
+ it { expect(subject).to be_readable }
56
+ it { expect(subject).to be_writable }
57
+
58
+ it { expect(subject.attr_name).to eq(:locations) }
59
+ it { expect(subject.type).to eq("Hash{Symbol => Address}") }
60
+ end
61
+
62
+ example "attribute :unique_id, String, :writer => :private" do
63
+ it { expect(subject).to have_private_writer }
64
+ end
65
+
66
+ example "attribute :unique_id, String, writer: :private" do
67
+ it { expect(subject).to have_private_writer }
68
+ end
69
+
70
+ example "attribute :unique_id, String, :writer => :private, :a => :b" do
71
+ it { expect(subject).to have_private_writer }
72
+ end
73
+ end