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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc6b3ccc77bc958c324f4cdc463febbcd6337e7c6b39a51e55f215c187167d80
4
- data.tar.gz: 503a6012c888d1626a834f1ab95d2fbc194ca9193943ea1595d4e9c732bfd038
3
+ metadata.gz: 3505075b7d10b99e85d446e53362b068a6e3f3d124ce13ab2ff3868be4ee6b2a
4
+ data.tar.gz: 3744fc4e1076d4c38a850dd98eb1c6a3cdf81cef6a0c9d2f9ff7c8c55790d6f7
5
5
  SHA512:
6
- metadata.gz: 3066dea2b451986c417f12ed8e437c0d6131068a546d30969a305be55cc63ec0044f3ec09add1d650356e321810546a49a84c23ecc9a0219fb408d27deddf9b2
7
- data.tar.gz: 679cd7f630fd15b4026646cfb60bbb1692fc8ddcbceb8febecf3585a75e86c705fd08c4399fa3556bcdb379829240dc0254f949e242501988c4760530c8d52e6
6
+ metadata.gz: 80c407034f987fd1db6ef01b159fe14bb21b8a91cf48c7a1f77e7c97e1b3d0d9370a0ba4273a530989b4c450af304ab8f9bed3af93ae5dfa9e9623c0fabde8cb
7
+ data.tar.gz: 199ef4aa13ea9665d8643cd58c1cdd4cd69fb79e58cfb30b54157633f291e9784dcc263003f20ca0432277331f111485fc2a34a18a14f5ee9a77be1f20042a26
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- qdocs (0.1.0)
4
+ qdocs (0.2.0)
5
5
  method_source (~> 1)
6
6
  rack (> 1)
7
7
 
data/README.md CHANGED
@@ -1,15 +1,20 @@
1
1
  # Qdocs
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/qdocs`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Write usage instructions here
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 'method_source'
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
- source_location: source_location_to_str(const_sl),
57
- instance_methods: own_methods(constant.instance_methods).sort,
58
- singleton_methods: own_methods(constant.methods).sort,
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
- class Method
64
- include Helpers
59
+ module Base
60
+ class Const
61
+ include Helpers
65
62
 
66
- def index(const, pattern)
67
- constant = find_constant const
68
- {
69
- constant: constant,
70
- singleton_methods: own_methods(constant.methods.grep(pattern)).sort,
71
- instance_methods: own_methods(constant.instance_methods.grep(pattern)).sort,
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
- def show(const, meth, type)
76
- constant = begin
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
- defined_at: source_location_to_str(method.source_location),
114
- source: source,
115
- arity: method.arity,
116
- parameters: parameters,
117
- comment: (method.comment.strip rescue nil),
118
- name: method.name,
119
- belongs_to: method.owner,
120
- super_method: method.super_method,
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
- Qdocs::Method.new.show(Object, $1, :instance)
182
+ Handler::Method.new(input).show(Object, $1, :instance)
132
183
  when /\A(#{CONST_REGEXP})\.(#{METHOD_REGEXP})\z/
133
- Qdocs::Method.new.show($1, $2, :singleton)
184
+ Handler::Method.new(input).show($1, $2, :singleton)
134
185
  when /\A(#{CONST_REGEXP})#(#{METHOD_REGEXP})\z/
135
- Qdocs::Method.new.show($1, $2, :instance)
186
+ Handler::Method.new(input).show($1, $2, :instance)
136
187
  when /\A(#{CONST_REGEXP})\z/
137
- Qdocs::Const.new.show($1)
188
+ Handler::Const.new(input).show($1)
138
189
  when %r{\A(#{CONST_REGEXP})/([^/]+)/\z}
139
- Qdocs::Method.new.index($1, Regexp.new($2))
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Qdocs
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.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-20 00:00:00.000000000 Z
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