qdocs 0.1.0 → 0.2.0
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.
- 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
|