Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Zejnilovic committed Jan 14, 2021
1 parent ca71e1b commit c339688
Show file tree
Hide file tree
Showing 13 changed files with 320 additions and 0 deletions.
30 changes: 30 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
root = true

[*]
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
tab_width = 4
trim_trailing_whitespace = true

[*.bat]
end_of_line = crlf

[*.gemspec]
indent_size = 2

[*.rb]
indent_size = 2

[*.yml]
indent_size = 2

[{*[Mm]akefile*,*.mak,*.mk,depend}]
indent_style = tab

[enc/*]
indent_size = 2

[reg*.[ch]]
indent_size = 2
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*/properties.conf
output/*
10 changes: 10 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
source 'https://rubygems.org'

gem 'uri'
gem 'rake'
gem 'net-http'
gem 'rubocop'
gem 'gnuplot'
gem 'ruby-graphviz', '~> 1.2.2'
gem 'awesome_print'
gem 'neatjson'
50 changes: 50 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
GEM
remote: https://rubygems.org/
specs:
ast (2.4.1)
awesome_print (1.8.0)
gnuplot (2.6.2)
neatjson (0.9)
net-http (0.1.1)
net-protocol
uri
net-protocol (0.1.0)
parallel (1.20.1)
parser (3.0.0.0)
ast (~> 2.4.1)
rainbow (3.0.0)
rake (13.0.3)
regexp_parser (2.0.3)
rexml (3.2.4)
rubocop (1.8.1)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.2.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.4.0)
parser (>= 2.7.1.5)
ruby-graphviz (1.2.5)
rexml
ruby-progressbar (1.11.0)
unicode-display_width (2.0.0)
uri (0.10.1)

PLATFORMS
ruby

DEPENDENCIES
awesome_print
gnuplot
neatjson
net-http
rake
rubocop
ruby-graphviz (~> 1.2.2)
uri

BUNDLED WITH
2.1.4
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Spring API Endpoints visualization

A simple collection of ruby scripts to collect data from `/actuator/mappings` and visualize all endpoint connections and the API "tree".

## Settings

In the folder resources you need create a file called `properties.conf`.
This is expected to be a JSON with keys

- `"URL_BASE"` - Base url to your Spring app API
- `"MAPPING_ENDPOINT"` - actuator endpoint for mapping. If you did not change anything and just enabled it, then it is `"/actuator/mappings"`,
- `"LOGIN_ENDPOINT"` - endpoint for the login to your app. If you leave it empty then the script will skip logging in.
- `"CLASS_PREFIX"` - prefix of your controller classes. Your organization should suffice
- `"OUTPUT_FILE_NAME"` the name of the output file png

## Running

Spring app needs to be running

```shell
$> bundle install
$> rake run
```

## Testing

Not implemented yet. As this is more excercise project then an enterprise ready solution. but you can run `rake lint`

## ToDo

- how to add metadata like method, params, etc.?
- clean up the code
- add tests
16 changes: 16 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require 'rubocop/rake_task'

task default: %w[lint]

RuboCop::RakeTask.new(:lint) do |task|
task.patterns = ['lib/**/*.rb', 'test/**/*.rb']
task.fail_on_error = false
end

task :run do
ruby 'lib/main.rb'
end

# task :test do
# ruby 'test/main.rb'
# end
8 changes: 8 additions & 0 deletions lib/details.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Details
attr_reader :handler_method, :request_mapping_conditions

def initialize(details)
@handler_method = details['handler_method']
@request_mapping_conditions = RequestMappingConditions.new(details['requestMappingConditions'])
end
end
9 changes: 9 additions & 0 deletions lib/dispatcher_servlet.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class DispatcherServlet
attr_reader :handler, :predicate, :details

def initialize(displatcher_servlet)
@handler = displatcher_servlet['handler']
@predicate = displatcher_servlet['predicate']
@details = Details.new(displatcher_servlet['details'])
end
end
50 changes: 50 additions & 0 deletions lib/endpoint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
class Endpoint
attr_reader :name, :path, :request_mapping_conditions, :weight, :parent
attr_accessor :children

def initialize(name, path, weight)
@name = name
@path = path.join
@weight = weight
@children = {}
end

def set_conditions(request_mapping_conditions)
@request_mapping_conditions = request_mapping_conditions
end

def set_parent(parent)
@parent = parent
end

def dig(path)
return self if path.empty?
return nil if children[path.first].nil?
children[path.first].dig(path[1..])
end

def place(path, endpoint)
if children[path.first].nil?
@children[endpoint.name] = endpoint
endpoint.set_parent(self)
else
children[path.first].place(path[1..], endpoint)
end
end

def generate_graph(graph, parent)
children.each do |_, value|
value.generate_graph(graph, @path)
end
attributes = {}
# TODO Currently, if I allow xlabels then there is too
# much around and an overlap
#
# unless request_mapping_conditions.nil?
# attributes[:xlabel] = request_mapping_conditions.get_details
# end
self_node = graph.add_nodes(@path, attributes).label = @name
graph.add_edges(parent, @path) unless parent.nil?
graph
end
end
77 changes: 77 additions & 0 deletions lib/main.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
require 'uri'
require 'net/http'
require 'json'
require 'ruby-graphviz'
require 'awesome_print'
require 'neatjson'

require_relative 'details'
require_relative 'dispatcher_servlet'
require_relative 'endpoint'
require_relative 'request_mapping_conditions'
require_relative 'some_auth'

ROOT_DIR = File.expand_path('..', __dir__)
prop_file = "#{ROOT_DIR}/resources/properties.conf"
throw Exception.new('No properties file') unless File.exist?(prop_file)
file = File.read(prop_file)
APP_CONF = JSON.parse(file)

def get_token
url = URI(APP_CONF['URL_BASE'] + APP_CONF['LOGIN_ENDPOINT'])
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
response = http.request(request)
token = response['X-CSRF-TOKEN']
cookie = response['Set-Cookie']

SomeAuth.new(token, cookie)
end

def get_mappings(menas_auth)
url = URI(APP_CONF['URL_BASE'] + APP_CONF['MAPPING_ENDPOINT'])
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Get.new(url)
unless menas_auth.nil?
request['X-CSRF-TOKEN'] = menas_auth.token
request['Cookie'] = menas_auth.cookie
end
response = http.request(request)
JSON.parse(response.read_body)
end

puts 'Logging in'
menas_auth = APP_CONF['LOGIN_ENDPOINT'] ? get_token : nil
puts 'Logged in'

puts 'Getting mappings'
raw_data = get_mappings(menas_auth)

puts 'Extracting mapping into objects'
selected_endpoints = raw_data
.dig('contexts','application','mappings','dispatcherServlets','dispatcherServlet')
.find_all { |h| (h.dig('details', 'handlerMethod', 'className') || '').start_with?(APP_CONF['CLASS_PREFIX']) }
.map { |ds| DispatcherServlet.new(ds) }

root_endpoint = Endpoint.new('BASE_URL', [''], 0)

selected_endpoints.map do |ds|
ds.details.request_mapping_conditions.patterns.each do |pattern|
path = []
pattern.split(/(?=\/)/).each do |value|
path << value
found = root_endpoint.dig(path)
if found.nil?
found = Endpoint.new(value, path, path.count('/'))
root_endpoint.place(path, found)
end
if found.request_mapping_conditions.nil? && path.join == pattern
found.set_conditions(ds.details.request_mapping_conditions)
end
end
end
end

graph = GraphViz.new(:G, type: 'strict digraph', overlap: :scale)
full_graph = root_endpoint.generate_graph(graph, nil)
full_graph.output(png: "#{ROOT_DIR}/output/#{APP_CONF['OUTPUT_FILE_NAME']}")
20 changes: 20 additions & 0 deletions lib/request_mapping_conditions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'json'
require 'neatjson'

class RequestMappingConditions
attr_reader :consumes, :headers, :methods, :params, :patterns, :produces

def initialize(request_mapping_conditions)
@consumes = request_mapping_conditions['consumes']
@headers = request_mapping_conditions['headers']
@methods = request_mapping_conditions['methods']
@params = request_mapping_conditions['params']
@patterns = request_mapping_conditions['patterns']
@produces = request_mapping_conditions['produces']
@origin = request_mapping_conditions
end

def get_details
JSON.neat_generate(@origin)
end
end
8 changes: 8 additions & 0 deletions lib/some_auth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class SomeAuth
attr_reader :token, :cookie

def initialize(token, cookie)
@token = token
@cookie = cookie
end
end
7 changes: 7 additions & 0 deletions resources/properties.conf.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"URL_BASE": "http://localhost:8080",
"MAPPING_ENDPOINT": "/actuator/mappings",
"LOGIN_ENDPOINT": "/api/login?username=user&password=changeme",
"CLASS_PREFIX": "com.example.mySpringApp",
"OUTPUT_FILE_NAME": "diagram.png"
}

0 comments on commit c339688

Please sign in to comment.