qdocs 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +69 -4
- data/lib/qdocs.rb +125 -74
- data/lib/qdocs/active_record.rb +98 -0
- data/lib/qdocs/server.rb +6 -1
- data/lib/qdocs/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3505075b7d10b99e85d446e53362b068a6e3f3d124ce13ab2ff3868be4ee6b2a
|
4
|
+
data.tar.gz: 3744fc4e1076d4c38a850dd98eb1c6a3cdf81cef6a0c9d2f9ff7c8c55790d6f7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80c407034f987fd1db6ef01b159fe14bb21b8a91cf48c7a1f77e7c97e1b3d0d9370a0ba4273a530989b4c450af304ab8f9bed3af93ae5dfa9e9623c0fabde8cb
|
7
|
+
data.tar.gz: 199ef4aa13ea9665d8643cd58c1cdd4cd69fb79e58cfb30b54157633f291e9784dcc263003f20ca0432277331f111485fc2a34a18a14f5ee9a77be1f20042a26
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,15 +1,20 @@
|
|
1
1
|
# Qdocs
|
2
2
|
|
3
|
-
|
3
|
+
Qdocs is a very lightweight language intelligence server, which provides runtime information about constants and methods. It currently supports:
|
4
|
+
|
5
|
+
- Providing detailed information about instance and singleton methods for Ruby constants (eg. Classes and Modules)
|
6
|
+
- Querying a constant's instance and singleton methods by regular expression, returning the methods whose names match the given pattern
|
7
|
+
- Providing detailed information about active record attributes, if the constant being queried is an ActiveRecord model
|
8
|
+
|
9
|
+
It has minimal dependencies (probably nothing extra, if your application uses rails or another common web framework)
|
4
10
|
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
6
11
|
|
7
12
|
## Installation
|
8
13
|
|
9
14
|
Add this line to your application's Gemfile:
|
10
15
|
|
11
16
|
```ruby
|
12
|
-
gem 'qdocs'
|
17
|
+
gem 'qdocs', require: false
|
13
18
|
```
|
14
19
|
|
15
20
|
And then execute:
|
@@ -22,7 +27,67 @@ Or install it yourself as:
|
|
22
27
|
|
23
28
|
## Usage
|
24
29
|
|
25
|
-
|
30
|
+
This gem offers CLI usage, or server usage:
|
31
|
+
|
32
|
+
#### Server usage:
|
33
|
+
`$ qdocs --server`
|
34
|
+
|
35
|
+
|
36
|
+
`$ curl 'http://localhost:8080/?input=User%2Efind'`
|
37
|
+
|
38
|
+
#### CLI usage
|
39
|
+
|
40
|
+
```
|
41
|
+
$ qdocs 'Set#length'
|
42
|
+
{
|
43
|
+
"original_input": "Set#length",
|
44
|
+
"constant": {
|
45
|
+
"name": "Set",
|
46
|
+
"type": "Class"
|
47
|
+
},
|
48
|
+
"query_type": "instance_method",
|
49
|
+
"attributes": {
|
50
|
+
"defined_at": "/Users/josephjohansen/.rvm/rubies/ruby-2.7.1/lib/ruby/2.7.0/set.rb:151",
|
51
|
+
"source": "def size\n @hash.size\nend\n",
|
52
|
+
"arity": 0,
|
53
|
+
"parameters": {
|
54
|
+
},
|
55
|
+
"comment": "# Returns the number of elements.",
|
56
|
+
"name": "length",
|
57
|
+
"belongs_to": "Set",
|
58
|
+
"super_method": null
|
59
|
+
}
|
60
|
+
}
|
61
|
+
```
|
62
|
+
**also provides support for constants which are recognised to be ActiveRecord models:**
|
63
|
+
|
64
|
+
```
|
65
|
+
$ curl 'http://localhost:7593/?input=User%2Femail%2F'
|
66
|
+
{
|
67
|
+
"constant": {
|
68
|
+
"name": "User",
|
69
|
+
"type": "Class"
|
70
|
+
},
|
71
|
+
"query_type": "methods",
|
72
|
+
"attributes": {
|
73
|
+
"constant": "User",
|
74
|
+
"singleton_methods": [
|
75
|
+
"find_by_unconfirmed_email_with_errors"
|
76
|
+
],
|
77
|
+
"instance_methods": [
|
78
|
+
"postpone_email_change?",
|
79
|
+
"postpone_email_change_until_confirmation_and_regenerate_confirmation_token",
|
80
|
+
"send_email_changed_notification?",
|
81
|
+
"send_verification_email"
|
82
|
+
]
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
```
|
87
|
+
|
88
|
+
#### Further usage examples:
|
89
|
+
|
90
|
+
`$ qdocs --help`
|
26
91
|
|
27
92
|
## Development
|
28
93
|
|
data/lib/qdocs.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "method_source"
|
4
4
|
require_relative "qdocs/version"
|
5
|
+
require "pathname"
|
5
6
|
|
6
7
|
module Qdocs
|
7
8
|
class UnknownClassError < StandardError; end
|
@@ -41,102 +42,152 @@ module Qdocs
|
|
41
42
|
rescue NameError
|
42
43
|
raise UnknownClassError, "Unknown constant #{const}"
|
43
44
|
end
|
44
|
-
end
|
45
|
-
|
46
|
-
class Const
|
47
|
-
include Helpers
|
48
|
-
|
49
|
-
def show(const)
|
50
|
-
const = const.to_s
|
51
|
-
constant = find_constant const
|
52
|
-
|
53
|
-
const_sl = Object.const_source_location const
|
54
45
|
|
46
|
+
def render_response(const, type, attrs)
|
55
47
|
{
|
56
|
-
|
57
|
-
|
58
|
-
|
48
|
+
original_input: @original_input,
|
49
|
+
constant: {
|
50
|
+
name: const.name,
|
51
|
+
type: const.class.name,
|
52
|
+
},
|
53
|
+
query_type: type,
|
54
|
+
attributes: attrs,
|
59
55
|
}
|
60
56
|
end
|
61
57
|
end
|
62
58
|
|
63
|
-
|
64
|
-
|
59
|
+
module Base
|
60
|
+
class Const
|
61
|
+
include Helpers
|
65
62
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
63
|
+
def initialize(original_input)
|
64
|
+
@original_input = original_input
|
65
|
+
end
|
66
|
+
|
67
|
+
def show(const)
|
68
|
+
const = const.to_s
|
69
|
+
constant = find_constant const
|
70
|
+
yield constant if block_given?
|
71
|
+
|
72
|
+
const_sl = Object.const_source_location const
|
73
|
+
|
74
|
+
render_response(constant, :constant, {
|
75
|
+
source_location: source_location_to_str(const_sl),
|
76
|
+
instance_methods: own_methods(constant.instance_methods).sort,
|
77
|
+
singleton_methods: own_methods(constant.methods).sort,
|
78
|
+
})
|
79
|
+
end
|
73
80
|
end
|
74
81
|
|
75
|
-
|
76
|
-
|
77
|
-
find_constant(const)
|
78
|
-
rescue UnknownClassError
|
79
|
-
abort "Unknown class #{const.inspect}"
|
80
|
-
end
|
81
|
-
method = case meth
|
82
|
-
when Symbol, String
|
83
|
-
method_method = case type
|
84
|
-
when :instance
|
85
|
-
:instance_method
|
86
|
-
when :singleton, :class
|
87
|
-
:method
|
88
|
-
else
|
89
|
-
raise UnknownMethodTypeError, "Unknown method type #{type}"
|
90
|
-
end
|
91
|
-
|
92
|
-
begin
|
93
|
-
constant.send method_method, meth
|
94
|
-
rescue NameError
|
95
|
-
raise UnknownMethodError, "No method #{meth.inspect} for #{constant.inspect}. Did you mean #{constant.inspect}/#{meth}/ ?"
|
96
|
-
end
|
97
|
-
when Method
|
98
|
-
meth
|
99
|
-
else
|
100
|
-
raise InvalidArgumentError, "#{meth.inspect} must be of type Symbol, String, or Method"
|
101
|
-
end
|
102
|
-
|
103
|
-
parameters = params_to_hash(method.parameters)
|
104
|
-
src = method.source rescue nil
|
105
|
-
source = if src
|
106
|
-
lines = src.lines
|
107
|
-
first_line = lines.first
|
108
|
-
indent_amount = first_line.length - first_line.sub(/^\s*/, '').length
|
109
|
-
lines.map { |l| l[indent_amount..-1] }.join
|
110
|
-
end
|
82
|
+
class Method
|
83
|
+
include Helpers
|
111
84
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
85
|
+
def initialize(original_input)
|
86
|
+
@original_input = original_input
|
87
|
+
end
|
88
|
+
|
89
|
+
def index(const, pattern)
|
90
|
+
constant = find_constant const
|
91
|
+
|
92
|
+
yield constant if block_given?
|
93
|
+
|
94
|
+
render_response(constant, :methods, {
|
95
|
+
constant: constant,
|
96
|
+
singleton_methods: own_methods(constant.methods.grep(pattern)).sort,
|
97
|
+
instance_methods: own_methods(constant.instance_methods.grep(pattern)).sort,
|
98
|
+
})
|
99
|
+
end
|
100
|
+
|
101
|
+
def show(const, meth, type)
|
102
|
+
constant = find_constant(const)
|
103
|
+
|
104
|
+
yield constant if block_given?
|
105
|
+
|
106
|
+
method = case meth
|
107
|
+
when Symbol, String
|
108
|
+
method_method = case type
|
109
|
+
when :instance
|
110
|
+
:instance_method
|
111
|
+
when :singleton, :class
|
112
|
+
:method
|
113
|
+
else
|
114
|
+
raise UnknownMethodTypeError, "Unknown method type #{type}"
|
115
|
+
end
|
116
|
+
|
117
|
+
begin
|
118
|
+
constant.send method_method, meth
|
119
|
+
rescue NameError
|
120
|
+
raise UnknownMethodError, "No method #{meth.inspect} for #{constant}. Did you mean #{constant}/#{meth}/ ?"
|
121
|
+
end
|
122
|
+
when ::Method
|
123
|
+
meth
|
124
|
+
else
|
125
|
+
raise InvalidArgumentError, "#{meth.inspect} must be of type Symbol, String, or Method"
|
126
|
+
end
|
127
|
+
|
128
|
+
parameters = params_to_hash(method.parameters)
|
129
|
+
src = method.source rescue nil
|
130
|
+
source = if src
|
131
|
+
lines = src.lines
|
132
|
+
first_line = lines.first
|
133
|
+
indent_amount = first_line.length - first_line.sub(/^\s*/, "").length
|
134
|
+
lines.map { |l| l[indent_amount..-1] }.join
|
135
|
+
end
|
136
|
+
sup = method.super_method
|
137
|
+
|
138
|
+
render_response(constant, method_method, {
|
139
|
+
defined_at: source_location_to_str(method.source_location),
|
140
|
+
source: source,
|
141
|
+
arity: method.arity,
|
142
|
+
parameters: parameters,
|
143
|
+
comment: (method.comment.strip rescue nil),
|
144
|
+
name: method.name,
|
145
|
+
belongs_to: method.owner,
|
146
|
+
super_method: sup ? Handler::Method.new.show(sup.owner, sup, type) : nil,
|
147
|
+
})
|
148
|
+
end
|
122
149
|
end
|
123
150
|
end
|
124
151
|
|
125
152
|
METHOD_REGEXP = /(?:[a-zA-Z_]+|\[\])[?!=]?/.freeze
|
126
153
|
CONST_REGEXP = /[[:upper:]]\w*(?:::[[:upper:]]\w*)*/.freeze
|
127
154
|
|
155
|
+
def self.load_env(dir_level = nil)
|
156
|
+
check_dir = dir_level || ["."]
|
157
|
+
project_top_level = Pathname(File.join(*check_dir, "Gemfile")).exist? ||
|
158
|
+
Pathname(File.join(*check_dir, ".git")).exist?
|
159
|
+
if project_top_level && Pathname(File.join(*check_dir, "config", "environment.rb")).exist?
|
160
|
+
require File.join(*check_dir, "config", "environment.rb")
|
161
|
+
elsif project_top_level
|
162
|
+
# no op - no env to load
|
163
|
+
else
|
164
|
+
dir_level ||= []
|
165
|
+
dir_level << ".."
|
166
|
+
Qdocs.load_env(dir_level)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
load_env
|
171
|
+
|
172
|
+
Handler = if Object.const_defined? :ActiveRecord
|
173
|
+
require "qdocs/active_record"
|
174
|
+
Qdocs::ActiveRecord
|
175
|
+
else
|
176
|
+
Qdocs::Base
|
177
|
+
end
|
178
|
+
|
128
179
|
def self.lookup(input)
|
129
180
|
case input
|
130
181
|
when /\A([[:lower:]](?:#{METHOD_REGEXP})?)\z/
|
131
|
-
|
182
|
+
Handler::Method.new(input).show(Object, $1, :instance)
|
132
183
|
when /\A(#{CONST_REGEXP})\.(#{METHOD_REGEXP})\z/
|
133
|
-
|
184
|
+
Handler::Method.new(input).show($1, $2, :singleton)
|
134
185
|
when /\A(#{CONST_REGEXP})#(#{METHOD_REGEXP})\z/
|
135
|
-
|
186
|
+
Handler::Method.new(input).show($1, $2, :instance)
|
136
187
|
when /\A(#{CONST_REGEXP})\z/
|
137
|
-
|
188
|
+
Handler::Const.new(input).show($1)
|
138
189
|
when %r{\A(#{CONST_REGEXP})/([^/]+)/\z}
|
139
|
-
|
190
|
+
Handler::Method.new(input).index($1, Regexp.new($2))
|
140
191
|
else
|
141
192
|
raise UnknownPatternError, "Unrecognised pattern #{input}"
|
142
193
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Qdocs
|
2
|
+
module ActiveRecord
|
3
|
+
module Helpers
|
4
|
+
def active_record_attributes_for(col)
|
5
|
+
if col.is_a? ::ActiveRecord::ConnectionAdapters::NullColumn
|
6
|
+
raise UnknownMethodError, "Unknown attribute #{col.name}"
|
7
|
+
end
|
8
|
+
|
9
|
+
{
|
10
|
+
type: col.sql_type_metadata&.type,
|
11
|
+
comment: col.comment,
|
12
|
+
default: col.default,
|
13
|
+
null: col.null,
|
14
|
+
default_function: col.default_function,
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def if_active_record(constant)
|
19
|
+
if Object.const_defined?("::ActiveRecord::Base") && constant < ::ActiveRecord::Base
|
20
|
+
yield constant
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Const < Qdocs::Base::Const
|
26
|
+
include ActiveRecord::Helpers
|
27
|
+
|
28
|
+
def show(const)
|
29
|
+
database_attributes = {}
|
30
|
+
constant = nil
|
31
|
+
resp = super do |con|
|
32
|
+
if_active_record(con) do |klass|
|
33
|
+
constant = klass
|
34
|
+
klass.columns.each do |col|
|
35
|
+
active_record_attributes_for col
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
if constant
|
41
|
+
{
|
42
|
+
**resp,
|
43
|
+
type: :active_record_class,
|
44
|
+
attributes: {
|
45
|
+
**resp[:attributes],
|
46
|
+
database_attributes: database_attributes,
|
47
|
+
},
|
48
|
+
}
|
49
|
+
else
|
50
|
+
resp
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Method < Qdocs::Base::Method
|
56
|
+
include ActiveRecord::Helpers
|
57
|
+
|
58
|
+
def index(const, pattern)
|
59
|
+
database_attributes = {}
|
60
|
+
attrs = super do |constant|
|
61
|
+
if_active_record(constant) do |klass|
|
62
|
+
klass.columns.each do |col|
|
63
|
+
next unless col.name.to_s.match? pattern
|
64
|
+
|
65
|
+
database_attributes[col.name.to_sym] = active_record_attributes_for col
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if database_attributes.empty?
|
71
|
+
attrs
|
72
|
+
else
|
73
|
+
{ **attrs, database_attributes: database_attributes }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def show(const, meth, type)
|
78
|
+
constant = []
|
79
|
+
super do |klass|
|
80
|
+
constant << klass
|
81
|
+
end
|
82
|
+
rescue UnknownMethodError => e
|
83
|
+
if constant[0] && meth && type == :instance
|
84
|
+
if_active_record(constant[0]) do |klass|
|
85
|
+
m = meth.is_a?(::Method) ? (meth.name rescue nil) : meth
|
86
|
+
return render_response(
|
87
|
+
klass,
|
88
|
+
:active_record_attribute,
|
89
|
+
active_record_attributes_for(klass.column_for_attribute(m))
|
90
|
+
)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
raise e
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/qdocs/server.rb
CHANGED
@@ -12,6 +12,11 @@ module Qdocs
|
|
12
12
|
else
|
13
13
|
[404, { "Content-Type" => "text/html; charset=utf-8" }, ["Not Found"]]
|
14
14
|
end
|
15
|
+
rescue Qdocs::UnknownClassError,
|
16
|
+
Qdocs::UnknownMethodTypeError,
|
17
|
+
Qdocs::UnknownMethodError,
|
18
|
+
Qdocs::UnknownPatternError => e
|
19
|
+
[404, { "Content-Type" => "text/html; charset=utf-8" }, ["Not found: #{e.message}"]]
|
15
20
|
rescue => e
|
16
21
|
[500, { "Content-Type" => "text/html; charset=utf-8" }, ["Error: #{e.message}"]]
|
17
22
|
end
|
@@ -20,4 +25,4 @@ end
|
|
20
25
|
|
21
26
|
handler = Rack::Handler::WEBrick
|
22
27
|
|
23
|
-
handler.run Qdocs::Server.new
|
28
|
+
handler.run Qdocs::Server.new, Port: 7593 # random port, not common 8080
|
data/lib/qdocs/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: qdocs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joseph Johansen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-05-
|
11
|
+
date: 2021-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: method_source
|
@@ -57,6 +57,7 @@ files:
|
|
57
57
|
- bin/setup
|
58
58
|
- exe/qdocs
|
59
59
|
- lib/qdocs.rb
|
60
|
+
- lib/qdocs/active_record.rb
|
60
61
|
- lib/qdocs/server.rb
|
61
62
|
- lib/qdocs/version.rb
|
62
63
|
- qdocs.gemspec
|