yard-virtus 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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