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 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