import github::github_curl
import template
+import opts
redef class Object
# Factorize cast
redef class GithubCurl
# Get a given pull request (PR)
- fun getpr(number: Int): JsonObject
+ fun getpr(repo: String, number: Int): nullable JsonObject
do
- var pr = get_and_check("https://api.github.com/repos/nitlang/nit/pulls/{number}")
+ var ir = get_and_check("https://api.github.com/repos/{repo}/issues/{number}")
+ var irm = ir.json_as_map
+ if not irm.has_key("pull_request") then return null
+ var pr = get_and_check("https://api.github.com/repos/{repo}/pulls/{number}")
var prm = pr.json_as_map
var sha = prm["head"].json_as_map["sha"].to_s
- var statuses = get_and_check("https://api.github.com/repos/nitlang/nit/statuses/{sha}")
+ var statuses = get_and_check("https://api.github.com/repos/{repo}/commits/{sha}/status")
+ statuses = statuses.json_as_map
prm["statuses"] = statuses
print "{prm["title"].to_s}: by {prm["user"].json_as_map["login"].to_s} (# {prm["number"].to_s})"
var mergeable = prm["mergeable"]
else
print "\tmergeable: unknown"
end
- var st = prm["statuses"].json_as_a
- if not st.is_empty then
- print "\tstatus: {st[0].json_as_map["state"].to_s}"
- else
+ var state = statuses["state"]
+ if state == null then
print "\tstatus: not tested"
+ else
+ print "\tstatus: {state}"
+ var sts = statuses["statuses"].json_as_a
+ for st in sts do
+ st = st.json_as_map
+ var ctx = st["context"].to_s
+ state = st["state"].to_s
+ print "\tstatus {ctx}: {state}"
+ prm["status-{ctx}"] = state
+ end
end
return prm
end
# Get reviewers of a PR
- fun getrev(pr: JsonObject): Array[String]
+ fun getrev(repo: String, pr: JsonObject): Array[String]
do
var number = pr["number"].as(Int)
var user = pr["user"].json_as_map["login"].as(String)
var comments = new Array[nullable Object]
- comments.add_all(get_and_check("https://api.github.com/repos/nitlang/nit/issues/{number}/comments").json_as_a)
- comments.add_all(get_and_check("https://api.github.com/repos/nitlang/nit/pulls/{number}/comments").json_as_a)
+ comments.add_all(get_and_check("https://api.github.com/repos/{repo}/issues/{number}/comments").json_as_a)
+ comments.add_all(get_and_check("https://api.github.com/repos/{repo}/pulls/{number}/comments").json_as_a)
var logins = new Array[String]
for c in comments do
var cm = c.json_as_map
if "NIT_TESTING".environ == "true" then exit 0
-var auth = get_github_oauth
+var opt_repo = new OptionString("Repository (e.g. nitlang/nit)", "-r", "--repo")
+var opt_auth = new OptionString("OAuth token", "--auth")
+var opt_query = new OptionString("Query to get issues (e.g. label=ok_will_merge)", "-q", "--query")
+var opt_keepgoing = new OptionBool("Skip merge conflicts", "-k", "--keep-going")
+var opt_all = new OptionBool("Merge all", "-a", "--all")
+var opt_status = new OptionArray("A status context that must be \"success\" (e.g. default)", "--status")
+var opts = new OptionContext
+opts.add_option(opt_repo, opt_auth, opt_query, opt_status, opt_all, opt_keepgoing)
+
+opts.parse(sys.args)
+var args = opts.rest
+var auth = opt_auth.value or else ""
+if auth == "" then auth = get_github_oauth
if auth == "" then
print "Warning: no github oauth token, you can configure one with"
print " git config --add github.oauthtoken MYOAUTHTOKEN"
end
+var repo = opt_repo.value or else "nitlang/nit"
+
+var query = opt_query.value or else "labels=ok_will_merge"
+
var curl = new GithubCurl(auth, "Merge-o-matic (nitlang/nit)")
-if args.length != 1 then
+if args.is_empty then
# Without args, list `ok_will_merge`
- var x = curl.get_and_check("https://api.github.com/repos/nitlang/nit/issues?labels=ok_will_merge")
+ var x = curl.get_and_check("https://api.github.com/repos/{repo}/issues?{query}")
+ var list = new Array[String]
for y in x.json_as_a do
var number = y.json_as_map["number"].as(Int)
- curl.getpr(number)
- end
-else
+ var pr = curl.getpr(repo, number)
+ if pr == null then continue
+ for ctx in opt_status.value do
+ if pr.get_or_null("status-{ctx}") != "success" then
+ print "No \"success\" for {ctx}. Skip."
+ continue label
+ end
+ end
+ list.add number.to_s
+ end label
+
+ if not opt_all.value then return
+ args = list
+end
+
+for arg in args do
# With a arg, merge the PR
- var number = args.first.to_i
- var pr = curl.getpr(number)
- var revs = curl.getrev(pr)
+ var number = arg.to_i
+ var pr = curl.getpr(repo, number)
+ if pr == null then
+ print "Not a PR: {number}"
+ return
+ end
+ var revs = curl.getrev(repo, pr)
var mergemsg = new Template
mergemsg.add "Merge: {pr["title"].to_s}\n\n"
print "Commit {sha} not in local repository; did you fetch github?"
return
end
+ if system("git merge-base --is-ancestor {sha} HEAD") == 0 then
+ print "Is already merged."
+ continue
+ end
if system("git merge --no-ff --no-commit {sha}") != 0 then
+ if opt_keepgoing.value then
+ system("git reset --merge")
+ continue
+ end
system("cp mergemsg `git rev-parse --git-dir`/MERGE_MSG")
print "Problem during merge... Let's do the commit manually."
return
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Server that implements an OAuth2-like authentication bound to a `shibuqam` valid server.
+#
+# The protocol is based on [OAuth2](https://tools.ietf.org/html/rfc6749),
+# especially the [Authorization Code Grant](https://tools.ietf.org/html/rfc6749#section-4.1).
+#
+# # Use as a web service
+#
+# * User: the human user and its user-agent (browser) that use the client website.
+# * Client: the 3rd party web site that need authentication. It owns `https://client.example.com`
+# * Server: the public OAuth server. It owns `https://server.example.com`
+#
+# From the user & client point of view, the process is the following:
+#
+# 1. The client redirects the user to the server.
+# 2. The user authenticates on the server.
+# 3. The server redirects the user to the client with a token.
+# 4. The client sends the token to the server.
+# 5. The server responds with the user information.
+#
+# ## 1. User Request
+#
+# Two GET fields
+#
+# * `redirect_uri`: A full URI that will be used to redirect the user once the authentication is done.
+# * `state`: A temporary string used to check that the callback is legitimate.
+#
+# On request, the service authenticate the user then redirect to the callback with the user informations.
+#
+# Example:
+#
+# ~~~raw
+# GET https://server.example.com/login?redirect_uri=https://client.example.com/callback&state=secret
+# ~~~
+#
+# ## 3. Response
+#
+# After the authentication, the response is a 303 redirection to `redirect_uri`
+# with the following GET arguments:
+#
+# * `code:` a temporary token to send-back to the server
+# * `state`: the same state to check that the response is not forged.
+#
+# Example:
+#
+# ~~~raw
+# HTTP/1.1 303 See Other
+# Location: https://client.example.com/callback?code=something&state=secret
+# ~~~
+#
+# ## 3b. Errors
+#
+# If the request is badly formed or if the `redirect_uri` is not authorized,
+# then there is no redirection and a text error message is send to the user.
+#
+# If there is a problem during the authentication, then there is a redirection
+# but the GET fields are only `state` and `error=access_denied`.
+#
+# ## 4. Client request
+#
+# A single POST field
+#
+# * `code`: The one you get as the user response.
+#
+# Example:
+#
+# ~~~raw
+# POST https://server.example.com/info
+# code=something
+# ~~~
+#
+# ## 5. Response
+#
+# The response is a JSON object that is the plain serialization of a `User` instance.
+#
+# ~~~json
+# HTTP/1.1 200 OK
+# Content-Type: application/json;charset=UTF-8
+#
+# {
+# "id": "jdoe",
+# "given_name": "John",
+# "display_name": "John Doe",
+# "email": "doe.john@example.com",
+# "avatar": "https://www.gravatar.com/avatar/4fe50a575e1c28773800a0aa03c62dbe?d=retro"
+# }
+# ~~~
+#
+# # To configure and execute
+#
+# ## Run the server
+#
+# shibuqamoauth authorized_list [host] [port]
+#
+# `authorized_list` is a textfile that registers the list of accepted `redirect_uri`.
+#
+# Example:
+#
+# ~~~raw
+# https://example.com/foo
+# https://other.example.com/
+# ~~~
+#
+# To be authorized, a `redirect_uri` must have one of the line of `authorized_list` as a prefix.
+# For instance, the previous `authorized_list` accepts `https://example.com/foo` and `https://example.com/foo/bar`
+# but not `https://example.com/bar` nor `https://sub.example.com/foo`.
+#
+#
+# ## Install as a server
+#
+# A correct `shibuqam` reverse proxy must be configured (see `shibuqam` for details).
+# In a full scenario, the server is replaced by:
+#
+# * Proxy: the public reverse proxy that can do the genuine Shibboleth authentication.
+# It owns `https://server.example.com`
+# * Service: the private shibuqamoauth service. It owns a NATed `https://shibuqamoauth.example.com/`
+#
+# On the first access to the server (user request):
+#
+# 1. Proxy gets the request
+# 2. Proxy does the Shibboleth authentication.
+# 3. Proxy enhances the request header.
+# 4. Proxy forwards the request to service.
+# 5. Service checks that `redir` is authorized.
+# 6. Service digs in the enhanced request header to generate a token and associate it to the user.
+# 7. Redirect the user to client with the callback and the token.
+#
+# On the second access to the server (client request):
+#
+# 1. Proxy gets the request
+# 2. Proxy forwards the request to service without authentification (it is not a user).
+# 3. Service checks that the token existe and is not expired.
+# 4. Service serialize and send the user as in the
+#
+# The Proxy should only do the authentication on the user request.
+# The simplest way is to configure two routes that reverse proxy on the same server.
+#
+# # Why not using real OAuth2?
+#
+# OAuth2 is centered about *access_tokens* that allow clients to performs
+# a (possibly scoped) set of actions/queries on the behalf of an authenticated user.
+#
+# In our scenario, there is no action/queries to do once an user is authenticated.
+# So we do not delivers *access_tokens* since there is nothing to access once the user is known.
+#
+# We need only the third-party authentication part of the protocol.
+#
+# Since we do not have the same goals than the RFC, we also have a simplified protocol.
+# Noways, OAuth is mainly a framework and the implementations are very diverse and unfortunately no
+# not interoperable anyway.
+#
+# Here is the specific changes we have:
+#
+# * no `response_type`: there is a single kind of grant and the two steps are identified by the http method (GET or POST) and the configured routes on the server.
+# * no `client_id`: because we do not want to code a db of clients. The `redirect_uri` can be seen as a simple client_id. We have also no way to present the client to the user since we do not control the authorization page of shibboleth since is done by the reverse proxy.
+# * no `scope` and no `authorisation_code` since since there is nothing to access. We just get minimal information after the successful shibboleth login. Thus we have nothing more to authorise once the user is authenticated.
+# * no `client_secret`: the user information are already public. There is no need to make the code more complex to protect public information.
+module shibuqamoauth
+
+import popcorn
+import shibuqam
+import json::serialization
+
+redef class HttpRequest
+ # percent decoded get or post parameter.
+ fun dec_param(name: String): nullable String
+ do
+ var res = get_args.get_or_null(name)
+ if res == null then res = post_args.get_or_null(name)
+ if res == null then return null
+ return res.from_percent_encoding
+ end
+end
+
+class AuthHandler
+ super Handler
+
+ # List of prefixes of authorized redirections.
+ var authorized: Array[String]
+
+ # Check is `redir` is an authorized redirection (client)
+ fun is_authorized(redir: String): Bool
+ do
+ for r in authorized do if redir.has_prefix(r) then return true
+ return false
+ end
+
+
+ # Associate each `code` issued to the user, to the info intended to the client.
+ var infos = new HashMap[String, Info]
+
+ # Remove expired keys
+ fun expiration
+ do
+ var now = get_time
+ for k, i in infos do
+ if i.expiration < now then
+ infos.keys.remove(k)
+ print "{i.user.id} -> expired"
+ end
+ end
+ end
+
+ # Generate a new usable token
+ #
+ # Not thread safe!
+ fun new_token: String
+ do
+ var token
+ loop
+ token = generate_token
+ if not infos.has_key(token) then break
+ end
+ return token
+ end
+
+ redef fun get(http_request, response)
+ do
+ # GET means a Authorization Request from the user-agent
+
+ # Get the `redirect_uri` parameter, we use it to identify the client
+ var redir = http_request.dec_param("redir")
+ if redir == null then redir = http_request.dec_param("redirect_uri")
+ if redir == null then
+ response.send("No redirect_uri.", 400)
+ return
+ end
+
+ # Check if the client is authorized
+ if not is_authorized(redir) then
+ response.send("Site not authorized.", 403)
+ return
+ end
+
+ # Get the state, we use it to avoid CSRF attacks
+ var state = http_request.dec_param("state")
+ var res = redir + "?"
+
+ # If we are here, the reverse proxy did the authentication
+ # Is there an user?
+ var user = http_request.user
+ if user == null then
+ print "no user -> {redir}"
+ res += "error=access_denied"
+ else
+ # The user is authenticated.
+ print "{user.id} -> {redir}"
+
+ # We prepare a token (code) that the client will use to get the information.
+ expiration
+ var token = new_token
+
+ res += "code={token.to_percent_encoding}"
+
+ var ttl = 10*60*60 # 10 minutes
+ var info = new Info(get_time + ttl, user)
+ infos[token] = info
+ end
+ if state != null then res += "&state={state.to_percent_encoding}"
+ response.redirect res
+ end
+
+ redef fun post(http_request, response)
+ do
+ # POST means an Access Token Request from the client.
+ # Unfortunately, we have no access to grant, only informations.
+ print http_request.post_args.join(" ; ", ": ")
+
+ expiration
+
+ var code = http_request.dec_param("code")
+ if code == null then
+ print "POST: no code"
+ return
+ end
+ var info = infos.get_or_null(code)
+ if info == null then
+ print "POST: bad code {code}"
+ return
+ end
+
+ print "{info.user.id} -> retrieved"
+
+ # Drop the code as it is a single use
+ infos.keys.remove(code)
+
+ # Send the requested information.
+ response.json(info.user)
+ end
+end
+
+redef class User
+ super Jsonable
+ redef fun to_json do return serialize_to_json(plain=true)
+end
+
+# Information about an authenticated used stored on the server to be given to the client.
+class Info
+ # Time to live
+ var expiration: Int
+
+ # The identified user
+ var user: User
+end
+
+var host = "localhost"
+var port = 3000
+
+if args.is_empty then
+ print "usage: shibuqamoauth authorized_list [host] [port]"
+ return
+end
+
+var list = args[0]
+if args.length > 1 then host = args[1]
+if args.length > 2 then port = args[2].to_i
+
+var authorized = list.to_path.read_lines
+if authorized.is_empty then
+ print_error "{list}: not found or empty"
+ exit 1
+end
+
+var app = new App
+app.use("/*", new AuthHandler(authorized))
+app.listen(host, port)
import nitcorn
private import md5
+import serialization
# Information on a user from Shibboleth/UQAM
class User
+ serialize
+
# The *code permanent* (or the uid for non student)
var id: String
## Core Basic Types and Operations
-[[doc:kernel]]
+[[doc: kernel]]
### Object
-[[doc:Object]]
+[[doc: Object]]
#### Equality
-[[doc:Object::==]]
-[[doc:Object::!=]]
-[[doc:Object::hash]]
-[[doc:Object::is_same_instance]]
-[[doc:Object::object_id]]
+[[doc: core::Object::==]]
+[[doc: core::Object::!=]]
+[[doc: core::Object::hash]]
+[[doc: core::Object::is_same_instance]]
+[[doc: core::Object::object_id]]
#### Debuging
-[[doc:Object::output]]
-[[doc:Object::output_class_name]]
-[[doc:Object::is_same_type]]
+[[doc: core::Object::output]]
+[[doc: core::Object::output_class_name]]
+[[doc: core::Object::is_same_type]]
### Sys
-[[doc:Sys]]
+[[doc: Sys]]
#### Program Execution
-[[doc:Sys::main]]
-[[doc:Sys::run]]
+[[doc: core::Sys::main]]
+[[doc: core::Sys::run]]
### Other
-[[list:kernel]]
+[[list: kernel]]
## Core Collections
-[[doc:collection]]
+[[doc: collection]]
## String and Text manipulation
-[[doc:text]]
+[[doc: text]]
### Authentification
-[[doc: GithubAPI::auth]]
+[[doc: auth]]
Token can also be recovered from user config with `get_github_oauth`.
### Other data
-[[list: api]]
+[[list: github::api]]
### Advanced uses
#### Custom requests
-[[doc: GithubAPI::get]]
+[[doc: github::GithubAPI::get]]
#### Change the user agent
-[[doc: GithubAPI::user_agent]]
+[[doc: github::GithubAPI::user_agent]]
#### Debugging
var count = issue.comments or else 0
var page = 1
loop
- var array = get("/repos/{repo.full_name}/comments?page={page}")
+ var array = get("/repos/{repo.full_name}/issues/{issue.number}/comments?page={page}")
if not array isa JsonArray then break
- if array.is_empty or res.length < count then break
+ if array.is_empty then break
for obj in array do
if not obj isa JsonObject then continue
var id = obj["id"].as(Int)
if comment == null then continue
res.add(comment)
end
+ if res.length >= count then break
page += 1
end
return res
var closed_by: nullable User is writable
# Is this issue linked to a pull request?
- var is_pull_request: Bool is noserialize, writable
+ var is_pull_request: Bool = false is writable, noserialize
end
# A Github pull request.
# User pointed by `self`.
var user: User is writable
- # Repo pointed by `self`.
- var repo: Repo is writable
+ # Repo pointed by `self` (if any).
+ #
+ # A `null` value means the `repo` was deleted.
+ var repo: nullable Repo is writable
end
# A Github label.
end
return null
end
+
+ redef fun deserialize_class(name) do
+ if name == "Issue" then
+ var issue = super.as(Issue)
+ if path.last.has_key("pull_request") then
+ issue.is_pull_request = true
+ end
+ return issue
+ end
+ return super
+ end
end
redef fun deserialize_attribute(name, static_type)
do
- assert not path.is_empty # This is an internal error, abort
+ if path.is_empty then
+ # The was a parsing error or the root is not an object
+ if not root isa Error then
+ errors.add new Error("Deserialization Error: parsed JSON value is not an object.")
+ end
+ return null
+ end
+
var current = path.last
if not current.keys.has(name) then
class MongoMatch
super JsonObject
- private fun op(name: String, field: String, value: nullable Jsonable): MongoMatch do
- var q = new JsonObject
- q["${name}"] = value
- self[field] = q
+ # Define a custom operaton for `field`
+ #
+ # If no `field` is specified, append the operator the the root object:
+ # ~~~json
+ # {$<name>: <value>}
+ # ~~~
+ #
+ # Else, append the operator to the field:
+ # ~~~json
+ # {field: {$<name>: <value>} }
+ # ~~~
+ fun op(name: String, field: nullable String, value: nullable Jsonable): MongoMatch do
+ if field != null then
+ var q = new JsonObject
+ q["${name}"] = value
+ self[field] = q
+ else
+ self[name] = value
+ end
return self
end
return self
end
+ # Match documents where `field` matches `pattern`
+ #
+ # To read more about the available options, see:
+ # https://docs.mongodb.com/manual/reference/operator/query/regex/#op._S_regex
+ #
+ # ~~~json
+ # {field: {$regex: 'pattern', $options: '<options>'} }
+ # ~~~
+ #
+ # Provides regular expression capabilities for pattern matching strings in queries.
+ # MongoDB uses Perl compatible regular expressions (i.e. "PCRE" ).
+ fun regex(field: String, pattern: String, options: nullable String): MongoMatch do
+ var q = new JsonObject
+ q["$regex"] = pattern
+ if options != null then q["$options"] = options
+ self[field] = q
+ return self
+ end
+
# Match documents where `field` is in `values`
#
# https://docs.mongodb.com/manual/reference/operator/query/in/
op("$nin", field, new JsonArray.from(values))
return self
end
+
+ # Logical `or`
+ #
+ # https://docs.mongodb.com/manual/reference/operator/query/or/#op._S_or
+ #
+ # The `$or` operator performs a logical OR operation on an array of two or
+ # more `expressions` and selects the documents that satisfy at least one of
+ # the `expressions`.
+ #
+ # The `$or` has the following syntax:
+ #
+ # ~~~json
+ # { field: { $or: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } }
+ # ~~~
+ fun lor(field: nullable String, expressions: Array[Jsonable]): MongoMatch do
+ op("$or", field, new JsonArray.from(expressions))
+ return self
+ end
+
+ # Logical `and`
+ #
+ # https://docs.mongodb.com/manual/reference/operator/query/and/#op._S_and
+ #
+ # The `$and` operator performs a logical AND operation on an array of two or
+ # more `expressions` and selects the documents that satisfy all of the `expressions`.
+ #
+ # The `$and` has the following syntax:
+ #
+ # ~~~json
+ # { field: { $and: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } }
+ # ~~~
+ fun land(field: nullable String, expressions: Array[Jsonable]): MongoMatch do
+ op("$and", field, new JsonArray.from(expressions))
+ return self
+ end
+
+ # Logical `not`
+ #
+ # https://docs.mongodb.com/manual/reference/operator/query/not/#op._S_not
+ #
+ # `$not` performs a logical NOT operation on the specified `expression` and
+ # selects the documents that do not match the `expression`.
+ # This includes documents that do not contain the field.
+ #
+ # The $not has the following syntax:
+ #
+ # ~~~json
+ # { field: { $not: { <expression> } } }
+ # ~~~
+ fun lnot(field: nullable String, expression: Jsonable): MongoMatch do
+ op("$not", field, expression)
+ return self
+ end
+
+ # Logical `nor`
+ #
+ # https://docs.mongodb.com/manual/reference/operator/query/nor/#op._S_nor
+ #
+ # `$nor` performs a logical NOR operation on an array of one or more query
+ # expression and selects the documents that fail all the query expressions
+ # in the array.
+ #
+ # The $nor has the following syntax:
+ #
+ # ~~~json
+ # { field: { $nor: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } }
+ # ~~~
+ fun lnor(field: nullable String, expressions: Array[Jsonable]): MongoMatch do
+ op("$nor", field, new JsonArray.from(expressions))
+ return self
+ end
end
# Mongo pipelines are arrays of aggregation stages
[[doc: NLPDocument]]
-[[doc: NLPDocument::from_xml]]
-[[doc: NLPDocument::from_xml_file]]
-[[doc: NLPDocument::sentences]]
+[[doc: nlp::NLPDocument::from_xml]]
+[[doc: nlp::NLPDocument::from_xml_file]]
+[[doc: nlp::NLPDocument::sentences]]
### NLPSentence
[[doc: NLPSentence]]
-[[doc: NLPSentence::tokens]]
+[[doc: nlp::NLPSentence::tokens]]
### NLPToken
[[doc: NLPToken]]
-[[doc: NLPToken::word]]
-[[doc: NLPToken::lemma]]
-[[doc: NLPToken::pos]]
+[[doc: nlp::NLPToken::word]]
+[[doc: nlp::NLPToken::lemma]]
+[[doc: nlp::NLPToken::pos]]
### NLP Processor
[[doc: NLPProcessor]]
-[[doc: NLPProcessor::java_cp]]
+[[doc: nlp::NLPProcessor::java_cp]]
-[[doc: NLPProcessor::process]]
-[[doc: NLPProcessor::process_file]]
-[[doc: NLPProcessor::process_files]]
+[[doc: nlp::NLPProcessor::process]]
+[[doc: nlp::NLPProcessor::process_file]]
+[[doc: nlp::NLPProcessor::process_files]]
## Vector Space Model
[[doc: NLPVector]]
-[[doc: NLPDocument::vector]]
+[[doc: vector]]
-[[doc: NLPVector::cosine_similarity]]
+[[doc: nlp::NLPVector::cosine_similarity]]
## NitNLP binary
# When the first iterator is terminated, the second is started.
#
# assert ([1..20[.iterator + [20..40[.iterator).to_a == ([1..40[).to_a
+ #
+ # SEE: `Iterator2`
fun +(other: Iterator[E]): Iterator[E]
do
return new PipeJoin[E](self, other)
end
end
+# Concatenates a sequence of iterators.
+#
+# Wraps an iterator of sub-iterators and iterates over the elements of the
+# sub-iterators.
+#
+# ~~~nit
+# var i: Iterator[Int]
+# var empty = new Array[Int]
+#
+# i = new Iterator2[Int]([
+# [1, 2, 3].iterator,
+# empty.iterator,
+# [4, 5].iterator
+# ].iterator)
+# assert i.to_a == [1, 2, 3, 4, 5]
+#
+# i = new Iterator2[Int]([
+# empty.iterator,
+# [42].iterator,
+# empty.iterator
+# ].iterator)
+# assert i.to_a == [42]
+# ~~~
+#
+# SEE: `Iterator::+`
+class Iterator2[E]
+ super Iterator[E]
+
+ # The inner iterator over sub-iterators.
+ var inner: Iterator[Iterator[E]]
+
+ redef fun finish
+ do
+ var i = current_iterator
+ if i != null then i.finish
+ end
+
+ redef fun is_ok
+ do
+ var i = current_iterator
+ if i == null then return false
+ return i.is_ok
+ end
+
+ redef fun item
+ do
+ var i = current_iterator
+ assert i != null
+ return i.item
+ end
+
+ redef fun next
+ do
+ var i = current_iterator
+ assert i != null
+ i.next
+ end
+
+ redef fun start
+ do
+ var i = current_iterator
+ if i != null then i.start
+ end
+
+ private var previous_iterator: nullable Iterator[E] = null
+
+ private fun current_iterator: nullable Iterator[E]
+ do
+ if previous_iterator == null then
+ # Get the first sub-iterator.
+ if inner.is_ok then
+ previous_iterator = inner.item
+ previous_iterator.start
+ inner.next
+ else
+ return null
+ end
+ end
+ # Get the first sub-iterator that has a current item.
+ while inner.is_ok and not previous_iterator.is_ok do
+ previous_iterator.finish
+ previous_iterator = inner.item
+ previous_iterator.start
+ inner.next
+ end
+ return previous_iterator
+ end
+end
+
# Wraps an iterator to skip nulls.
#
# ~~~nit
end
# Redirect response to `location`
+ #
+ # Use by default 303 See Other as it is the RFC
+ # way to redirect web applications to a new URI.
fun redirect(location: String, status: nullable Int) do
header["Location"] = location
if status != null then
status_code = status
else
- status_code = 302
+ status_code = 303
end
check_sent
sent = true
redef fun all(req, res) do
var clock = req.clock
if clock != null then
- log "{req.method} {req.uri} {status(res)} ({clock.total}s)"
+ log "{req.method} {req.url} {status(res)} ({clock.total}s)"
else
- log "{req.method} {req.uri} {status(res)}"
+ log "{req.method} {req.url} {status(res)}"
end
end
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Popcorn web tracker
+#
+# Easy and ready to use web tracker you can apply to your popcorn application.
+#
+# The only thing you have to do is to use the tracker in your app routes:
+
+# ~~~nitish
+# var config = new AppConfig
+# var app = new App
+# app.use("/", new HomeHandler)
+# app.use("/products", new ProductsHandler)
+# app.use("customers/", new CustomersHandler)
+#
+# app.use_after("/*", new PopTracker(config)) # tracker listens to /*
+# ~~~
+#
+# You can also use multiple tracker at once on different route.
+# All the data will be aggregated for you.
+
+# ~~~nitish
+# app.use_after("/api/*", new PopTracker(config))
+# app.use_after("/admin/*", new PopTracker(config))
+# ~~~
+#
+# To retrieve your tracker data use the `PopTrackerAPI` which serves the tracker
+# data in JSON format.
+#
+# ~~~nitish
+# app.use("/api/tracker_data", new PopTrackerAPI(config))
+# ~~~
+module pop_tracker
+
+import popcorn
+import popcorn::pop_config
+import popcorn::pop_logging
+import popcorn::pop_repos
+
+redef class AppConfig
+
+ # Logs collection
+ var tracker_logs = new TrackerRepo(db.collection("tracker_logs"))
+
+ # Tracker handler
+ var tracker = new PopTracker(self)
+end
+
+# JSON API of the PopTracker
+#
+# Serves the collected data in JSON format.
+class PopTrackerAPI
+ super Router
+
+ # Config used to access tracker db
+ var config: AppConfig
+
+ init do
+ use("/entries", new PopTrackerEntries(config))
+ use("/queries", new PopTrackerQueries(config))
+ use("/browsers", new PopTrackerBrowsers(config))
+ use("/times", new PopTrackerResponseTime(config))
+ end
+end
+
+# Base tracker handler
+abstract class TrackerHandler
+ super Handler
+
+ # Config used to access tracker db
+ var config: AppConfig
+
+ # Get the `limit` GET argument from `req`
+ #
+ # Return `10` by default.
+ fun limit(req: HttpRequest): Int do return req.int_arg("limit") or else 10
+end
+
+# Saves logs into a MongoDB collection
+class PopTracker
+ super ConsoleLog
+ super TrackerHandler
+
+ redef fun all(req, res) do
+ config.tracker_logs.save new LogEntry(req, res)
+ end
+end
+
+# List all tracker log entries
+class PopTrackerEntries
+ super TrackerHandler
+
+ redef fun get(req, res) do
+ res.json new JsonArray.from(config.tracker_logs.find_all)
+ end
+end
+
+# Group and count entries by query string
+class PopTrackerQueries
+ super TrackerHandler
+
+ redef fun get(req, res) do
+ var pipe = new MongoPipeline
+ pipe.group((new MongoGroup("$request.uri")).
+ sum("visits", 1).
+ avg("response_time", "$response_time").
+ addToSet("uniq", "$session"))
+ pipe.sort((new MongoMatch).eq("visits", -1))
+ pipe.limit(limit(req))
+ res.json new JsonArray.from(config.tracker_logs.collection.aggregate(pipe))
+ end
+end
+
+# Group and count entries by browser
+class PopTrackerBrowsers
+ super TrackerHandler
+
+ # MongoMatch query used for each browser key
+ #
+ # Because parsing user agent string is a pain in the nit, we go lazy on this
+ # one. We associate each broswer key like `Chromium` to the query that allows
+ # us to count the number of visits.
+ var browser_queries: HashMap[String, MongoMatch] do
+ var map = new HashMap[String, MongoMatch]
+ map["Chromium"] = (new MongoMatch).regex("user_agent", "Chromium")
+ map["Edge"] = (new MongoMatch).regex("user_agent", "Edge")
+ map["Firefox"] = (new MongoMatch).regex("user_agent", "Firefox")
+ map["IE"] = (new MongoMatch).regex("user_agent", "(MSIE)|(Trident)")
+ map["Netscape"] = (new MongoMatch).regex("user_agent", "Netscape")
+ map["Opera"] = (new MongoMatch).regex("user_agent", "Opera")
+ map["Safari"] = (new MongoMatch).land(null, [
+ (new MongoMatch).regex("user_agent", "Safari"),
+ (new MongoMatch).regex("user_agent", "^((?!Chrome).)*$")])
+ map["Chrome"] = (new MongoMatch).land(null, [
+ (new MongoMatch).regex("user_agent", "Chrome"),
+ (new MongoMatch).regex("user_agent", "^((?!Edge).)*$")])
+
+ return map
+ end
+
+ # Apply the `query` on `TrackerRepo::count`
+ fun browser_count(query: MongoMatch): Int do return config.tracker_logs.count(query)
+
+ redef fun get(req, res) do
+ var browsers = new Array[BrowserCount]
+ for browser, query in self.browser_queries do
+ var count = new BrowserCount(browser, browser_count(query))
+ if count.count > 0 then browsers.add count
+ end
+ var sum = 0
+ for browser in browsers do sum += browser.count
+ var other = config.tracker_logs.count - sum
+ if other > 0 then browsers.add new BrowserCount("Other", other)
+ default_comparator.sort(browsers)
+ res.json new JsonArray.from(browsers)
+ end
+end
+
+# Associate each browser to its count.
+#
+# Only used to serialize the results.
+private class BrowserCount
+ super Comparable
+ super RepoObject
+ serialize
+
+ redef type OTHER: SELF
+
+ var browser: String
+ var count: Int
+
+ redef fun <=>(o) do return o.count <=> count
+end
+
+# Return last month response time
+class PopTrackerResponseTime
+ super TrackerHandler
+
+ redef fun get(req, res) do
+ var limit = get_time - (3600 * 24 * 30)
+ var pipe = new MongoPipeline
+ pipe.match((new MongoMatch).gte("timestamp", limit))
+ pipe.group((new MongoGroup("$timestamp")).
+ sum("visits", 1).
+ avg("response_time", "$response_time"))
+ pipe.sort((new MongoMatch).eq("_id", -1))
+ res.json new JsonArray.from(config.tracker_logs.collection.aggregate(pipe))
+ end
+end
+
+# A tracker log entry used to store HTTP requests and their given HTTP responses
+class LogEntry
+ super RepoObject
+ serialize
+
+ # HTTP request that triggered that log entry
+ var request: HttpRequest
+
+ # HTTP response returned by the serveur
+ var response: HttpResponse
+
+ # Request user-agent shortcut (easier for db requests
+ var user_agent: nullable String is lazy do return request.header["User-Agent"]
+
+ # Processing time in miliseconds (or null if no clock was found in request)
+ var response_time: nullable Int is lazy do
+ var clock = request.clock
+ if clock == null then return null
+ return (clock.total * 1000.0).to_i
+ end
+
+ # Log entry timestamp
+ var timestamp: Int = get_time
+
+ # Session ID associated to this entry
+ var session: nullable String is lazy do
+ var session = request.session
+ if session == null then return null
+ return session.id_hash
+ end
+end
+
+# Repository to store track logs.
+class TrackerRepo
+ super MongoRepository[LogEntry]
+end
# assert trie["foo"] == 1
#
# # Search by prefix
-# assert trie.find_by_prefix("") == [1, 2, 3]
+# assert trie.find_by_prefix("") == [1, 3, 2]
# assert trie.find_by_prefix("foo") == [1, 2]
# assert trie.find_by_prefix("baz").is_empty
# ~~~
# trie["foooo"] = 3
# trie["bar"] = 4
#
- # assert trie.find_by_prefix("") == [1, 2, 3, 4]
+ # assert trie.find_by_prefix("") == [1, 4, 2, 3]
# assert trie.find_by_prefix("foo") == [1, 2, 3]
# assert trie.find_by_prefix("bar") == [4]
# assert trie.find_by_prefix("baz").is_empty
# ~~~
fun find_by_prefix(prefix: String): Array[E] do
- var res = new Array[E]
var node
if prefix == "" then
node = root
else
node = search_node(prefix)
end
- if node != null then
- node.collect_values(res)
- end
- return res
+ if node == null then return new Array[E]
+ return node.collect_values
end
# Find values stored under `prefix`
var children = new HashMap[Char, TrieNode[E]]
var is_leaf: Bool = false
- fun collect_values(values: Array[E]) do
- var value = self.value
- if value != null then values.add value
- for child in children.values do
- child.collect_values(values)
+ fun collect_values: Array[E] do
+ var values = new Array[E]
+
+ var todo = new List[TrieNode[E]]
+ todo.add self
+ while todo.not_empty do
+ var node = todo.shift
+ var value = node.value
+ if value != null then values.add value
+ for child in node.children.values do
+ todo.push child
+ end
end
+ return values
end
end
controllerAs: 'entityCtrl'
})
.otherwise({
- redirectTo: '/'
+ templateUrl: 'views/error.html'
});
$locationProvider.html5Mode(true);
});
--- /dev/null
+<div class='container-fluid'>
+ <div class='page-header'>
+ <h2>404 Not found</h2>
+ <p>The page you requested does not exist.</p>
+ </div>
+</div>
import doc_commands
import doc_poset
import doc::console_templates
+import model::model_index
# Nitx handles console I/O.
#
return
end
var res = query.perform(self, doc)
- var page = query.make_results(self, res)
+ var suggest = null
+ if res.is_empty then
+ suggest = query.suggest(self, doc)
+ end
+ var page = query.make_results(self, res, suggest)
print page.write_to_string
end
end
# Looks up the `doc` model and returns possible matches.
fun perform(nitx: Nitx, doc: DocModel): Array[NitxMatch] is abstract
+ # Looks up the `doc` model and returns possible suggestions.
+ fun suggest(nitx: Nitx, doc: DocModel): nullable Array[MEntity] do
+ return find_suggestions(doc, args.first)
+ end
+
# Pretty prints the results for the console.
- fun make_results(nitx: Nitx, results: Array[NitxMatch]): DocPage do
+ fun make_results(nitx: Nitx, results: Array[NitxMatch], suggest: nullable Array[MEntity]): DocPage do
var page = new DocPage("results", "Results")
- page.root.add_child(new QueryResultArticle("results", "Results", self, results))
+ page.root.add_child(new QueryResultArticle("results", "Results", self, results, suggest))
return page
end
+
+ # Lookup mentities based on a `query` string.
+ #
+ # 1- lookup by first name (returns always one value)
+ # 2- lookup by name (can return conflicts)
+ fun find_mentities(doc: DocModel, query: String): Array[MEntityMatch] do
+ var res = new Array[MEntityMatch]
+
+ # First lookup by full_name
+ var mentity = doc.mentity_by_full_name(query)
+ if mentity != null then
+ res.add new MEntityMatch(self, mentity)
+ return res
+ end
+
+ # If no results, lookup by name
+ for m in doc.mentities_by_name(query) do
+ res.add new MEntityMatch(self, m)
+ end
+
+ return res
+ end
+
+ # Suggest some mentities based on a `query` string.
+ fun find_suggestions(doc: DocModel, query: String): Array[MEntity] do
+ return doc.find(query, 3)
+ end
end
# Something that matches a `DocCommand`.
end
redef class CommentCommand
- redef fun perform(nitx, doc) do
- var name = args.first
- var res = new Array[NitxMatch]
- for mentity in doc.mentities_by_name(name) do
- res.add new MEntityMatch(self, mentity)
- end
- return res
- end
+ redef fun perform(nitx, doc) do return find_mentities(doc, args.first)
- redef fun make_results(nitx, results) do
+ redef fun make_results(nitx, results, suggest) do
var len = results.length
if len == 1 then
var res = results.first.as(MEntityMatch)
return res
end
- redef fun make_results(nitx, results) do
+ redef fun make_results(nitx, results, suggest) do
var len = results.length
# FIXME how to render the pager for one worded namespaces like "core"?
if len == 1 then
abstract class HierarchiesQuery
super DocCommand
- redef fun make_results(nitx, results) do
+ redef fun make_results(nitx, results, suggest) do
var page = new DocPage("hierarchy", "Hierarchy")
for result in results do
if not result isa PageMatch then continue
return res
end
# else, lookup the model by name
- for mentity in doc.mentities_by_name(name) do
- if mentity isa MClass then continue
- if mentity isa MProperty then continue
- res.add new CodeMatch(self, mentity.cs_location, mentity.cs_source_code)
+ for match in find_mentities(doc, name) do
+ if match.mentity isa MClass then continue
+ if match.mentity isa MProperty then continue
+ res.add new CodeMatch(self, match.mentity.cs_location, match.mentity.cs_source_code)
end
return res
end
- redef fun make_results(nitx, results) do
+ redef fun make_results(nitx, results, suggest) do
var page = new DocPage("results", "Code Results")
for res in results do
page.add new CodeQueryArticle("results", "Results", self, res.as(CodeMatch))
# Results to display.
var results: Array[NitxMatch]
+ # Optional suggestion when no matches where found
+ var suggest: nullable Array[MEntity] = null is optional
+
redef fun render_title do
var len = results.length
if len == 0 then
- add "No result found for '{query.string}'..."
+ addn "No result found for '{query.string}'..."
+ var suggest = self.suggest
+ if suggest != null and suggest.not_empty then
+ add "\nDid you mean "
+ var i = 0
+ for s in suggest do
+ add "`{s.full_name}`"
+ if i == suggest.length - 2 then add ", "
+ if i == suggest.length - 1 then add " or "
+ i += 1
+ end
+ add "?"
+ end
else
add "# {len} result(s) for '{query.string}'".green.bold
end
import doc_commands
import doc_down
import doc_intros_redefs
+import model::model_index
# Generate content of `ReadmePage`.
#
context.pop
pop_buffer
end
+
+ # Find mentities matching `query`.
+ fun find_mentities(query: String): Array[MEntity] do
+ # search MEntities by full_name
+ var mentity = phase.doc.mentity_by_full_name(query)
+ if mentity != null then return [mentity]
+ # search MEntities by name
+ return phase.doc.mentities_by_name(query)
+ end
+
+ # Suggest mentities based on `query`.
+ fun suggest_mentities(query: String): Array[MEntity] do
+ return phase.doc.find(query, 3)
+ end
+
+ # Display a warning message with suggestions.
+ fun warn(token: TokenWikiLink, message: String, suggest: nullable Array[MEntity]) do
+ var msg = new Buffer
+ msg.append message
+ if suggest != null and suggest.not_empty then
+ msg.append " (suggestions: "
+ var i = 0
+ for s in suggest do
+ msg.append "`{s.full_name}`"
+ if i < suggest.length - 1 then msg.append ", "
+ i += 1
+ end
+ msg.append ")"
+ end
+ phase.warning(token.location, page, msg.write_to_string)
+ end
end
# MarkdownDecorator used to decorated the Readme file with links between doc entities.
var cmd = new DocCommand(link)
if cmd isa UnknownCommand then
# search MEntities by name
- var res = v.phase.doc.mentities_by_name(link.to_s)
+ var res = v.find_mentities(link.to_s)
# no match, print warning and display wikilink as is
if res.is_empty then
- v.phase.warning(token.location, v.page, "Link to unknown entity `{link}`")
+ v.warn(token, "Link to unknown entity `{link}`", v.suggest_mentities(link.to_s))
super
else
add_mentity_link(v, res.first, token.name, token.comment)
# Render the content of the doc command.
fun render(v: ReadmeMdEmitter, token: TokenWikiLink) is abstract
-
- # Search `doc` model for mentities match `string`.
- fun search_model(doc: DocModel, string: String): Array[MEntity] do
- var res
- if string.has("::") then
- res = doc.mentities_by_namespace(string).to_a
- else
- res = doc.mentities_by_name(string).to_a
- end
- return res
- end
end
redef class ArticleCommand
redef fun render(v, token) do
var string = args.first
- var res = search_model(v.phase.doc, string)
- res = filter_results(res)
+ var res = v.find_mentities(string)
if res.is_empty then
- v.phase.warning(
- token.location, v.page,
- "Try to include documentation of unknown entity `{args.first}`")
+ v.warn(token,
+ "Try to include documentation of unknown entity `{string}`",
+ v.suggest_mentities(string))
return
end
- if res.length > 1 then
- v.phase.warning(token.location, v.page, "conflicting article for `{args.first}` (choices : {res.join(", ")})")
- end
v.add_article new DocumentationArticle("readme", "Readme", res.first)
end
-
- private fun filter_results(res: Array[MEntity]): Array[MEntity] do
- var out = new Array[MEntity]
- for e in res do
- if e isa MPackage then continue
- if e isa MGroup then continue
- out.add e
- end
- return out
- end
end
redef class ListCommand
redef fun render(v, token) do
var string = args.first
- var res = search_model(v.phase.doc, string)
+ var res = v.find_mentities(string)
if res.is_empty then
- v.phase.warning(token.location, v.page, "include article for unknown entity `{args.first}`")
+ v.warn(token,
+ "Try to include article of unknown entity `{string}`",
+ v.suggest_mentities(string))
return
end
if res.length > 1 then
- v.phase.warning(token.location, v.page, "conflicting article for `{args.first}` (choices : {res.join(", ")})")
+ v.warn(token, "Conflicting article for `{args.first}`", res)
end
var mentity = res.first
if mentity isa MModule then
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Search things from the Model
+#
+# ModelIndex allows you to index mentities then retrieve them by their `name`
+# or `full_name`.
+# It offers a set of `find_*` methods that can be used to match queries
+# against entities name.
+#
+# Because each use is different, ModelIndex only provide base raw search services.
+# All of them return IndexMatches that can be ordered and filtered by the client.
+#
+# ## Indexing mentities
+#
+# Before searching something from the ModelIndex, you have to index it.
+# Use the `ModelIndex::index` method to do that:
+#
+# ~~~nitish
+# var index = new ModelIndex
+#
+# for mentity in model.private_view.mentities do
+# index.index(mentity)
+# end
+# ~~~
+#
+# ## Search mentities
+#
+# You can then run queries on the ModelIndex:
+#
+# ~~~nitish
+# for res in index.find("Array").limit(10) do
+# print res
+# end
+# ~~~
+#
+# ## Examples
+#
+# Here some examples of how you can use the ModelIndex.
+#
+# ### Smart type prediction
+#
+# Use ModelIndex to implement a smart prediction system based on the typed prefix:
+#
+# ~~~nitish
+# var index = new ModelIndex
+#
+# for mentity in model.private_view.mentities do
+# # We don't really care about definitions
+# if mentity isa MClassDef or mentity isa MPropDef then continue
+# index.index(mentity)
+# end
+#
+# var typed_prefix = "Arr"
+# for res in index.find_by_name_prefix(typed_prefix).
+# uniq. # keep only the best ranked mentity
+# limit(5). # limit to ten results
+# sort(new VisibilityComparator, new NameComparator) do # order by visibility then name
+# print res
+# end
+# ~~~
+#
+# Will output something like:
+#
+# ~~~raw
+# Array (1)
+# ArraySet (2)
+# ArrayMap (3)
+# ArrayCmp (4)
+# ArrayMapKeys (5)
+# ~~~
+#
+# ### Method autocompletion
+#
+# Find methods from a class full_name:
+#
+# ~~~nitish
+# var class_full_name = "core::Array"
+# for res in index.find_by_full_name_prefix("{class_full_name}::").
+# uniq. # keep only the best ranked mentity
+# sort(new VisibilityComparator). # put private in the bottom of the list
+# limit(5). # limit to ten results
+# sort(new FullNameComparator) do # order by lexicographic order
+# print res
+# end
+# ~~~
+#
+# Will output something like:
+#
+# ~~~raw
+# * (2)
+# + (1)
+# filled_with (5)
+# from (3)
+# with_items (4)
+# ~~~
+#
+# ### Name typo detection and suggestion
+#
+# Detect unknown names and suggest corrections:
+#
+# ~~~nitish
+# var name = "Zrray"
+#
+# if index.find_by_name_prefix(name).is_empty then
+# printn "`{name}` not found, did you mean: "
+# printn index.find_by_name_similarity(name).sort(new ScoreComparator).limit(2).join(" or ")
+# print "?"
+# end
+# ~~~
+#
+# Will output something like:
+#
+# ~~~raw
+# `Zrray` not found, did you mean: Array (1) or array (1)?
+# ~~~
+module model_index
+
+import model::model_views
+import trees::trie
+
+redef class ModelView
+
+ # Keep a direct link to mentities by full name to speed up `mentity_from_uri`
+ var mentities_by_full_name: HashMap[String, MEntity] is lazy do
+ var mentities_by_full_name = new HashMap[String, MEntity]
+ for mentity in model.private_view.mentities do
+ mentities_by_full_name[mentity.full_name] = mentity
+ end
+ return mentities_by_full_name
+ end
+
+ # ModelIndex used to perform searches
+ var index: ModelIndex is lazy do
+ var index = new ModelIndex
+ for mentity in model.private_view.mentities do
+ if mentity isa MClassDef or mentity isa MPropDef then continue
+ index.index mentity
+ end
+ return index
+ end
+
+ # Find mentities by their `name`
+ fun mentities_by_name(name: String): Array[MEntity] do
+ if index.name_prefixes.has_key(name) then
+ return index.name_prefixes[name]
+ end
+ return new Array[MEntity]
+ end
+
+ redef fun mentity_by_full_name(full_name) do
+ if mentities_by_full_name.has_key(full_name) then
+ return mentities_by_full_name[full_name]
+ end
+ return null
+ end
+
+ private var score_sorter = new ScoreComparator
+ private var vis_sorter = new VisibilityComparator
+ private var kind_sorter = new MEntityComparator
+ private var name_sorter = new NameComparator
+ private var lname_sorter = new NameLengthComparator
+ private var fname_sorter = new FullNameComparator
+ private var lfname_sorter = new FullNameLengthComparator
+
+ # Search mentities based on a `query` string
+ #
+ # Lookup the view index for anything matching `query` and return `limit` results.
+ #
+ # The algorithm used is the following:
+ # 1- lookup by name prefix
+ # 2- lookup by full_name prefix
+ # 3- loopup by levenshtein distance
+ #
+ # At each step if the `limit` is reached, the algorithm stops and returns the results.
+ fun find(query: String, limit: nullable Int): Array[MEntity] do
+ # Find, lookup by name prefix
+ var matches = index.find_by_name_prefix(query).uniq.
+ sort(lname_sorter, name_sorter, kind_sorter)
+ if limit != null and matches.length >= limit then
+ return matches.limit(limit).rerank.sort(vis_sorter, score_sorter).mentities
+ end
+ matches = matches.rerank.sort(vis_sorter, score_sorter)
+
+ # If limit not reached, lookup by full_name prefix
+ var malus = matches.length
+ var full_matches = new IndexMatches
+ for match in index.find_by_full_name_prefix(query).
+ sort(kind_sorter, lfname_sorter, fname_sorter) do
+ match.score += malus
+ full_matches.add match
+ end
+ matches = matches.uniq
+ if limit != null and matches.length + full_matches.length >= limit then
+ matches.add_all full_matches
+ matches = matches.uniq.limit(limit).rerank.sort(vis_sorter, score_sorter)
+ return matches.mentities
+ end
+
+ # If limit not reached, lookup by similarity
+ malus = matches.length
+ var sim_matches = new IndexMatches
+ for match in index.find_by_similarity(query).sort(score_sorter, kind_sorter, lname_sorter, name_sorter) do
+ match.score += malus
+ sim_matches.add match
+ end
+ matches.add_all sim_matches
+ matches = matches.uniq
+ if limit != null then matches = matches.limit(limit)
+ return matches.rerank.sort(vis_sorter, score_sorter).mentities
+ end
+end
+
+# ModelIndex indexes mentities by their name and full name
+#
+# It provides methods to find mentities based on a prefix or string similarity.
+#
+# ~~~nitish
+# # Build index
+# var index = new ModelIndex
+# for mentity in model.private_view.mentities do
+# if mentity isa MClassDef or mentity isa MPropDef then continue
+# index.index(mentity)
+# end
+#
+# for e in index.find("Foo").uniq.sort(new ScoreComparator).limit(10) do
+# print " * {e.score}: {e.mentity.name} ({e.mentity.full_name})"
+# end
+# ~~~
+class ModelIndex
+
+ # List of all indexed mentities.
+ #
+ # Faster than traversing the tries.
+ var mentities = new Array[MEntity]
+
+ # Prefix tree for mentities `name`
+ #
+ # Because multiple mentities can share the same `name`, we use a Trie of
+ # arrays of mentities.
+ #
+ # As for now, we do not index class and property definitions.
+ # TODO add an option.
+ var name_prefixes = new Trie[Array[MEntity]]
+
+ # Prefix tree for mentities `full_name`
+ #
+ # Even if two mentities cannot share the same `full_name`, we use a Trie of
+ # arrays of mentities to be consistent with `name_prefixes`.
+ var full_name_prefixes = new Trie[Array[MEntity]]
+
+ # Index `mentity` by it's `MEntity::name`
+ #
+ # See `name_prefixes`.
+ private fun index_by_name(mentity: MEntity) do
+ var name = mentity.name
+ if not name_prefixes.has_key(name) then
+ name_prefixes[name] = new Array[MEntity]
+ end
+ name_prefixes[name].add mentity
+ end
+
+ # Index `mentity` by its `MEntity::full_name`
+ private fun index_by_full_name(mentity: MEntity) do
+ var name = mentity.full_name
+ if not full_name_prefixes.has_key(name) then
+ full_name_prefixes[name] = new Array[MEntity]
+ end
+ full_name_prefixes[name].add mentity
+ end
+
+ # Index `mentity` so it can be retrieved by a find query
+ #
+ # MEntities are indexed by both name and full_name.
+ fun index(mentity: MEntity) do
+ mentities.add mentity
+ index_by_name mentity
+ index_by_full_name mentity
+ end
+
+ # Translate Trie results to `SearchResult`
+ #
+ # This method is used internally to translate each mentity returned by a prefix
+ # match in a Trie into a SearchResult that can be ranked by score.
+ #
+ # Results from the Trie are returned in a breadth first manner so we get the
+ # matches ordered by prefix.
+ # We preserve that order by giving an incremental score to the `array` items.
+ private fun score_results_incremental(array: Array[Array[MEntity]]): IndexMatches do
+ var results = new IndexMatches
+ var score = 1
+ for mentities in array do
+ for mentity in mentities do
+ results.add new IndexMatch(mentity, score)
+ end
+ score += 1
+ end
+ return results
+ end
+
+ # Find all mentities where `MEntity::name` matches the `prefix`
+ fun find_by_name_prefix(prefix: String): IndexMatches do
+ return score_results_incremental(name_prefixes.find_by_prefix(prefix))
+ end
+
+ # Find all mentities where `MEntity::full_name` matches the `prefix`
+ fun find_by_full_name_prefix(prefix: String): IndexMatches do
+ return score_results_incremental(full_name_prefixes.find_by_prefix(prefix))
+ end
+
+ # Rank all mentities by the distance between `MEntity::name` and `name`
+ #
+ # Use the Levenshtein algorithm on all the indexed mentities `name`.
+ # Warning: may not scale to large indexes.
+ fun find_by_name_similarity(name: String): IndexMatches do
+ var results = new IndexMatches
+ for mentity in mentities do
+ if mentity isa MClassDef or mentity isa MPropDef then continue
+ results.add new IndexMatch(mentity, name.levenshtein_distance(mentity.name))
+ end
+ return results
+ end
+
+ # Rank all mentities by the distance between `MEntity::full_name` and `full_name`
+ #
+ # Use the Levenshtein algorithm on all the indexed mentities `full_name`.
+ # Warning: may not scale to large indexes.
+ fun find_by_full_name_similarity(name: String): IndexMatches do
+ var results = new IndexMatches
+ for mentity in mentities do
+ if mentity isa MClassDef or mentity isa MPropDef then continue
+ results.add new IndexMatch(mentity, name.levenshtein_distance(mentity.full_name))
+ end
+ return results
+ end
+
+ # Rank all mentities by the distance between `name` and both the mentity name and full name
+ fun find_by_similarity(name: String): IndexMatches do
+ var results = new IndexMatches
+ for mentity in mentities do
+ if mentity isa MClassDef or mentity isa MPropDef then continue
+ results.add new IndexMatch(mentity, name.levenshtein_distance(mentity.name))
+ results.add new IndexMatch(mentity, name.levenshtein_distance(mentity.full_name))
+ end
+ return results
+ end
+
+ # Find mentities by name trying first by prefix then by similarity
+ fun find_by_name(name: String): IndexMatches do
+ var results = find_by_name_prefix(name)
+ for mentity in mentities do
+ if mentity isa MClassDef or mentity isa MPropDef then continue
+ results.add new IndexMatch(mentity, name.levenshtein_distance(mentity.name))
+ end
+ return results
+ end
+
+ # Find mentities by full name trying firt by prefix then by similarity
+ fun find_by_full_name(name: String): IndexMatches do
+ var results = find_by_full_name_prefix(name)
+ for mentity in mentities do
+ if mentity isa MClassDef or mentity isa MPropDef then continue
+ results.add new IndexMatch(mentity, name.levenshtein_distance(mentity.full_name))
+ end
+ return results
+ end
+
+ # Find all mentities that matches `name`
+ #
+ # 1. try by name prefix
+ # 2. add full name prefix matches
+ # 3. try similarity by name
+ # 4. try similarity by full_name
+ fun find(name: String): IndexMatches do
+ var results = find_by_name_prefix(name)
+
+ for result in find_by_full_name_prefix(name) do
+ results.add result
+ end
+
+ for mentity in mentities do
+ if mentity isa MClassDef or mentity isa MPropDef then continue
+ results.add new IndexMatch(mentity, name.levenshtein_distance(mentity.name))
+ results.add new IndexMatch(mentity, name.levenshtein_distance(mentity.full_name))
+ end
+ return results
+ end
+end
+
+# An array of IndexMatch instances returned by the ModelIndex
+#
+# Index matches can be sorted, filtered and truncated.
+#
+# Thanks to the fluent interface, the index matches can be manipulated in chain
+# from a model index query:
+#
+# ~~~nitish
+# var res = index.find("Foo").
+# uniq.
+# sort(new ScoreComparator, new MEntityComparator).
+# limit(10).
+# sort(new VisibilityComparator)
+# ~~~
+class IndexMatches
+ super Array[IndexMatch]
+
+ # Create a new ModelMatches from an array of matches
+ #
+ # Elements are copied.
+ init from_matches(matches: Array[IndexMatch]) do self.add_all matches
+
+ # Sort the matches with `comparator` (or a list of comparators)
+ #
+ # Return a new IndexMatches instance with the sorted results.
+ #
+ # When more than one comparator is given, the comparators are applied in a
+ # pipeline where the `n`th comparator is applied only if the `n-1`th comparator
+ # returned 0.
+ fun sort(comparator: ScoreComparator...): IndexMatches do
+ var res = to_a
+ if comparator.length == 1 then
+ comparator.first.sort res
+ else
+ var comparators = new MatchComparators(comparator)
+ comparators.sort res
+ end
+ return new IndexMatches.from_matches(res)
+ end
+
+ # Limit the matches with `limit`
+ #
+ # Return a new IndexMatches instance with only the `limit` first matches.
+ fun limit(limit: Int): IndexMatches do
+ var res = new Array[IndexMatch]
+ for match in self do
+ if res.length >= limit then break
+ res.add match
+ end
+ return new IndexMatches.from_matches(res)
+ end
+
+ # Remove doublons from the matches
+ #
+ # Preverse the lowest score of all the matches for a MEntity.
+ fun uniq: IndexMatches do
+ var scores = new HashMap[MEntity, IndexMatch]
+ var res = new Array[IndexMatch]
+ for match in self do
+ var mentity = match.mentity
+ if scores.has_key(mentity) then
+ var older = scores[mentity]
+ if match.score < older.score then older.score = match.score
+ else
+ scores[mentity] = match
+ res.add match
+ end
+ end
+ return new IndexMatches.from_matches(res)
+ end
+
+ # Reset score of each matches to follow `self` order
+ #
+ # Usefull when you need to apply one sorter over another.
+ fun rerank: IndexMatches do
+ var res = new IndexMatches
+ for match in self do
+ res.add match
+ match.score = res.length
+ end
+ return res
+ end
+
+ # Aggregate the mentities for all the matches
+ #
+ # Preserve the match order.
+ fun mentities: Array[MEntity] do
+ var res = new Array[MEntity]
+ for match in self do res.add match.mentity
+ return res
+ end
+end
+
+# An MEntity matched from a ModelIndex
+#
+# Each match has a `score`. The score should be seen as the distance of
+# the match from the query. In other words, the lowest is the score, the more
+# relevant is the match.
+class IndexMatch
+ super Comparable
+
+ redef type OTHER: IndexMatch
+
+ # MEntity matches
+ var mentity: MEntity
+
+ # Score allocated by the search method
+ #
+ # A lowest score means a more relevant match.
+ #
+ # Scores values are arbitrary, the meaning of `10` vs `2000` really depends
+ # on the search method producing the match and the comparators used to sort
+ # the matches.
+ # The only universal rule is: low score = relevance.
+ var score: Int is writable
+
+ # By default matches are compared only on their score
+ redef fun <=>(o) do return score <=> o.score
+
+ redef fun to_s do return "{mentity} ({score})"
+end
+
+# Compare two matches by their score
+#
+# Since the score can be seen as a distance, we want the lowest score first.
+class ScoreComparator
+ super Comparator
+
+ redef type COMPARED: IndexMatch
+
+ redef fun compare(o1, o2) do return o1.score <=> o2.score
+end
+
+# A pipeline of comparators executed in inclusion order
+#
+# This class is used internally to agregate the behaviors of multiple comparators.
+# Use `IndexMatches::sort(comparator...)` instead.
+private class MatchComparators
+ super ScoreComparator
+
+ # Comparator to use in the array order
+ var comparators: Array[ScoreComparator]
+
+ # Compare with each comparator
+ #
+ # Return the compare value of the first one to return anything else than 0.
+ redef fun compare(o1, o2) do
+ for comparator in comparators do
+ var c = comparator.compare(o1, o2)
+ if c != 0 then return c
+ end
+ return 0
+ end
+end
+
+# Compare two matches by their MEntity kind
+#
+# Usefull to order the mentities by kind in this order:
+# packages, groups, modules and classes, properties.
+class MEntityComparator
+ super ScoreComparator
+
+ # See `MEntity::compare_mentity`
+ redef fun compare(o1, o2) do
+ return o1.mentity.mentity_kind_rank <=> o2.mentity.mentity_kind_rank
+ end
+end
+
+# Compare two matches by their MEntity visibility
+#
+# We reverse the original order so private is at the end of the list.
+class VisibilityComparator
+ super ScoreComparator
+
+ redef fun compare(o1, o2) do return o2.mentity.visibility <=> o1.mentity.visibility
+end
+
+# Compare two matches by their name in lexicographic order
+#
+# Generally, for a same score, we want to put A before Z.
+class NameComparator
+ super ScoreComparator
+
+ redef fun compare(o1, o2) do return o1.mentity.name <=> o2.mentity.name
+end
+
+# Compare two matches by their name length
+class NameLengthComparator
+ super ScoreComparator
+
+ redef fun compare(o1, o2) do return o1.mentity.name.length <=> o2.mentity.name.length
+end
+
+# Compare two matches by their full_name in lexicographic order
+#
+# Generally, for a same score, we want to put A before Z.
+class FullNameComparator
+ super ScoreComparator
+
+ redef fun compare(o1, o2) do return o1.mentity.full_name <=> o2.mentity.full_name
+end
+
+# Compare two matches by their full name length
+class FullNameLengthComparator
+ super ScoreComparator
+
+ redef fun compare(o1, o2) do return o1.mentity.full_name.length <=> o2.mentity.full_name.length
+end
+
+redef class MEntity
+
+ # Compare MEntity class kind
+ #
+ # Unknown kind have a virtually high score.
+ private fun mentity_kind_rank: Int do return 10
+end
+
+redef class MPackage
+ redef fun mentity_kind_rank do return 1
+end
+
+redef class MGroup
+ redef fun mentity_kind_rank do return 2
+end
+
+redef class MModule
+ redef fun mentity_kind_rank do return 3
+end
+
+redef class MClass
+ redef fun mentity_kind_rank do return 4
+end
+
+redef class MClassDef
+ redef fun mentity_kind_rank do return 5
+end
+
+redef class MProperty
+ redef fun mentity_kind_rank do return 6
+end
+
+redef class MPropDef
+ redef fun mentity_kind_rank do return 7
+end
v.include_test_suite = self.include_test_suite
end
- # Searches MEntities that match `name`.
- fun mentities_by_name(name: String): Array[MEntity] do
- var res = new Array[MEntity]
- for mentity in mentities do if mentity.name == name then res.add mentity
- return res
- end
-
# Searches the MEntity that matches `full_name`.
fun mentity_by_full_name(full_name: String): nullable MEntity do
for mentity in mentities do
return null
end
- # Looks up a MEntity by its full `namespace`.
- #
- # Usefull when `mentities_by_name` returns conflicts.
- #
- # Namespaces must be of the form `package::core::module::Class::prop`.
- fun mentities_by_namespace(namespace: String): Array[MEntity] do
- var v = new LookupNamespaceVisitor(namespace)
- init_visitor(v)
- for mpackage in mpackages do
- v.enter_visit(mpackage)
- end
- return v.results
- end
-
# Build an concerns tree with from `self`
fun to_tree: MEntityTree do
var v = new ModelTreeVisitor
end
# Build the properties of `nclassdef`.
- # REQUIRE: all superclasses are built.
private fun build_properties(nclassdef: AClassdef)
do
# Force building recursively
# see `testing/README`
module nitunit
+import frontend
import testing
var toolcontext = new ToolContext
+toolcontext.keep_going = true
toolcontext.option_context.add_option(toolcontext.opt_full, toolcontext.opt_output, toolcontext.opt_dir, toolcontext.opt_noact, toolcontext.opt_pattern, toolcontext.opt_autosav, toolcontext.opt_gen_unit, toolcontext.opt_gen_force, toolcontext.opt_gen_private, toolcontext.opt_gen_show, toolcontext.opt_nitc)
toolcontext.tooldescription = "Usage: nitunit [OPTION]... <file.nit>...\nExecutes the unit tests from Nit source files."
return config
end
- # Build the nit catalog used in homepage.
- fun build_catalog(model: Model, modelbuilder: ModelBuilder): Catalog do
- var catalog = new Catalog(modelbuilder)
- for mpackage in model.mpackages do
- catalog.deps.add_node(mpackage)
- for mgroup in mpackage.mgroups do
- for mmodule in mgroup.mmodules do
- for imported in mmodule.in_importation.direct_greaters do
- var ip = imported.mpackage
- if ip == null or ip == mpackage then continue
- catalog.deps.add_edge(mpackage, ip)
- end
- end
- end
- catalog.git_info(mpackage)
- catalog.package_page(mpackage)
- end
- return catalog
- end
-
redef fun process_mainmodule(mainmodule, mmodules)
do
- var model = mainmodule.model
- var modelbuilder = toolcontext.modelbuilder
var config = build_config(toolcontext, mainmodule)
- var catalog = build_catalog(model, modelbuilder)
var app = new App
app.use_before("/*", new SessionInit)
app.use_before("/*", new RequestClock)
- app.use("/api", new NitwebAPIRouter(config, catalog))
+ app.use("/api", new APIRouter(config))
app.use("/login", new GithubLogin(config.github_client_id))
app.use("/oauth", new GithubOAuthCallBack(config.github_client_id, config.github_client_secret))
app.use("/logout", new GithubLogout)
end
end
-# Group all api handlers in one router.
-class NitwebAPIRouter
- super APIRouter
-
- # Catalog to pass to handlers.
- var catalog: Catalog
-
- init do
- use("/catalog", new APICatalogRouter(config, catalog))
- use("/list", new APIList(config))
- use("/search", new APISearch(config))
- use("/random", new APIRandom(config))
- use("/entity/:id", new APIEntity(config))
- use("/code/:id", new APIEntityCode(config))
- use("/uml/:id", new APIEntityUML(config))
- use("/linearization/:id", new APIEntityLinearization(config))
- use("/defs/:id", new APIEntityDefs(config))
- use("/feedback/", new APIFeedbackRouter(config))
- use("/inheritance/:id", new APIEntityInheritance(config))
- use("/graph/", new APIGraphRouter(config))
- use("/docdown/", new APIDocdown(config))
- use("/metrics/", new APIMetricsRouter(config))
- use("/user", new GithubUser)
- end
-end
-
# build toolcontext
var toolcontext = new ToolContext
var tpl = new Template
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import frontend
+import model_index
+import console
+
+redef class ToolContext
+ var opt_name_prefix = new OptionBool("", "--name-prefix")
+ var opt_full_name_prefix = new OptionBool("", "--full-name-prefix")
+ var opt_name_similarity = new OptionBool("", "--name-similarity")
+ var opt_full_name_similarity = new OptionBool("", "--full-name-similarity")
+ var opt_name = new OptionBool("", "--name")
+ var opt_full_name = new OptionBool("", "--full-name")
+
+ redef init do
+ option_context.add_option(opt_name_prefix, opt_full_name_prefix)
+ option_context.add_option(opt_name_similarity, opt_full_name_similarity)
+ option_context.add_option(opt_name, opt_full_name)
+ end
+end
+
+redef class MEntity
+ fun color: String do
+ if visibility == public_visibility then
+ return name.green
+ else if visibility == protected_visibility then
+ return name.yellow
+ else
+ return name.red
+ end
+ end
+end
+
+# build toolcontext
+var toolcontext = new ToolContext
+toolcontext.process_options(args)
+var args = toolcontext.option_context.rest
+
+if not args.length == 2 then
+ print "usage: test_model_index <nitfile> <search_query>"
+ exit 1
+end
+
+# build model
+var model = new Model
+var mbuilder = new ModelBuilder(model, toolcontext)
+var mmodules = mbuilder.parse_full([args.first])
+
+# process
+if mmodules.is_empty then return
+mbuilder.run_phases
+toolcontext.run_global_phases(mmodules)
+
+# Build index
+var index = new ModelIndex
+for mentity in model.private_view.mentities do
+ if mentity isa MClassDef or mentity isa MPropDef then continue
+ index.index(mentity)
+end
+
+var q = args[1]
+
+print "# {q}\n"
+
+var res
+if toolcontext.opt_name_prefix.value then
+ res = index.find_by_name_prefix(q)
+else if toolcontext.opt_full_name_prefix.value then
+ res = index.find_by_full_name_prefix(q)
+else if toolcontext.opt_name_similarity.value then
+ res = index.find_by_name_similarity(q)
+else if toolcontext.opt_full_name_similarity.value then
+ res = index.find_by_full_name_similarity(q)
+else if toolcontext.opt_name.value then
+ res = index.find_by_name(q)
+else if toolcontext.opt_full_name.value then
+ res = index.find_by_full_name(q)
+else
+ res = index.find(q)
+end
+
+res = res.sort(new ScoreComparator, new MEntityComparator).
+ uniq.
+ limit(10).
+ sort(new VisibilityComparator, new NameComparator)
+
+for e in res do
+ if toolcontext.opt_no_color.value then
+ print " * {e.score}: {e.mentity.name} ({e.mentity.full_name})"
+ else
+ print " * {e.score}: {e.mentity.color} ({e.mentity.full_name})"
+ end
+end
import web_base
import catalog
-# Group all api handlers in one router.
-class APICatalogRouter
- super APIRouter
+redef class NitwebConfig
# Catalog to pass to handlers.
- var catalog: Catalog
-
- init do
- use("/highlighted", new APICatalogHighLighted(config, catalog))
- use("/required", new APICatalogMostRequired(config, catalog))
- use("/bytags", new APICatalogByTags(config, catalog))
- use("/contributors", new APICatalogContributors(config, catalog))
- use("/stats", new APICatalogStats(config, catalog))
+ var catalog: Catalog is lazy do
+ var catalog = new Catalog(modelbuilder)
+ for mpackage in model.mpackages do
+ catalog.deps.add_node(mpackage)
+ for mgroup in mpackage.mgroups do
+ for mmodule in mgroup.mmodules do
+ for imported in mmodule.in_importation.direct_greaters do
+ var ip = imported.mpackage
+ if ip == null or ip == mpackage then continue
+ catalog.deps.add_edge(mpackage, ip)
+ end
+ end
+ end
+ catalog.git_info(mpackage)
+ catalog.package_page(mpackage)
+ end
+ return catalog
+ end
+end
+
+redef class APIRouter
+ redef init do
+ super
+ use("/catalog/highlighted", new APICatalogHighLighted(config))
+ use("/catalog/required", new APICatalogMostRequired(config))
+ use("/catalog/bytags", new APICatalogByTags(config))
+ use("/catalog/contributors", new APICatalogContributors(config))
+ use("/catalog/stats", new APICatalogStats(config))
end
end
abstract class APICatalogHandler
super APIHandler
- var catalog: Catalog
-
# List the 10 best packages from `cpt`
fun list_best(cpt: Counter[MPackage]): JsonArray do
var res = new JsonArray
redef fun get(req, res) do
var obj = new JsonObject
obj["packages"] = config.model.mpackages.length
- obj["maintainers"] = catalog.maint2proj.length
- obj["contributors"] = catalog.contrib2proj.length
- obj["modules"] = catalog.mmodules.sum
- obj["classes"] = catalog.mclasses.sum
- obj["methods"] = catalog.mmethods.sum
- obj["loc"] = catalog.loc.sum
+ obj["maintainers"] = config.catalog.maint2proj.length
+ obj["contributors"] = config.catalog.contrib2proj.length
+ obj["modules"] = config.catalog.mmodules.sum
+ obj["classes"] = config.catalog.mclasses.sum
+ obj["methods"] = config.catalog.mmethods.sum
+ obj["loc"] = config.catalog.loc.sum
res.json obj
end
end
class APICatalogHighLighted
super APICatalogHandler
- redef fun get(req, res) do res.json list_best(catalog.score)
+ redef fun get(req, res) do res.json list_best(config.catalog.score)
end
class APICatalogMostRequired
super APICatalogHandler
redef fun get(req, res) do
- if catalog.deps.not_empty then
+ if config.catalog.deps.not_empty then
var reqs = new Counter[MPackage]
for p in config.model.mpackages do
- reqs[p] = catalog.deps[p].smallers.length - 1
+ reqs[p] = config.catalog.deps[p].smallers.length - 1
end
res.json list_best(reqs)
return
class APICatalogByTags
super APICatalogHandler
- redef fun get(req, res) do res.json list_by(catalog.tag2proj)
+ redef fun get(req, res) do res.json list_by(config.catalog.tag2proj)
end
class APICatalogContributors
redef fun get(req, res) do
var obj = new JsonObject
- obj["maintainers"] = new JsonArray.from(catalog.maint2proj.keys)
- obj["contributors"] = new JsonArray.from(catalog.contrib2proj.keys)
+ obj["maintainers"] = new JsonArray.from(config.catalog.maint2proj.keys)
+ obj["contributors"] = new JsonArray.from(config.catalog.contrib2proj.keys)
res.json obj
end
end
intrude import doc_down
intrude import markdown::wikilinks
import doc_commands
+import model::model_index
+
+redef class APIRouter
+ redef init do
+ super
+ use("/docdown/", new APIDocdown(config))
+ end
+end
# Docdown handler accept docdown as POST data and render it as HTML
class APIDocdown
write_error(v, "Not yet implemented command `{token.link or else "null"}`")
end
- # Find the MEntity ` with `full_name`.
- fun find_mentity(model: ModelView, full_name: nullable String): nullable MEntity do
- if full_name == null then return null
- return model.mentity_by_full_name(full_name.from_percent_encoding)
+ # Find the MEntity that matches `name`.
+ #
+ # Write an error if the entity is not found
+ fun find_mentity(v: MarkdownEmitter, model: ModelView, name: nullable String): nullable MEntity do
+ if name == null then
+ write_error(v, "No MEntity found")
+ return null
+ end
+ # Lookup by full name
+ var mentity = model.mentity_by_full_name(name)
+ if mentity != null then return mentity
+
+ var mentities = model.mentities_by_name(name)
+ if mentities.is_empty then
+ var suggest = model.find(name, 3)
+ var msg = new Buffer
+ msg.append "No MEntity found for name `{name}`"
+ if suggest.not_empty then
+ msg.append " (suggestions: "
+ var i = 0
+ for s in suggest do
+ msg.append "`{s.full_name}`"
+ if i < suggest.length - 1 then msg.append ", "
+ i += 1
+ end
+ msg.append ")"
+ end
+ write_error(v, msg.write_to_string)
+ return null
+ else if mentities.length > 1 then
+ var msg = new Buffer
+ msg.append "Conflicts for name `{name}`"
+ msg.append " (conflicts: "
+ var i = 0
+ for s in mentities do
+ msg.append "`{s.full_name}`"
+ if i < mentities.length - 1 then msg.append ", "
+ i += 1
+ end
+ msg.append ")"
+ write_warning(v, msg.write_to_string)
+ end
+ return mentities.first
end
# Write a warning in the output
return
end
var full_name = link.write_to_string
- var mentity = find_mentity(model, full_name)
- if mentity == null then
- write_error(v, "Unknown command `{link}`")
- return
- end
+ var mentity = find_mentity(v, model, full_name)
+ if mentity == null then return
write_mentity_link(v, mentity)
end
end
return
end
var name = args.first
- var mentity = find_mentity(model, name)
- if mentity == null then
- write_error(v, "No MEntity found for name `{name}`")
- return
- end
+ var mentity = find_mentity(v, model, name)
+ if mentity == null then return
var mdoc = mentity.mdoc_or_fallback
if mdoc == null then
write_warning(v, "No MDoc for mentity `{name}`")
return
end
var name = args.first
- var mentity = find_mentity(model, name)
- if mentity == null then
- write_error(v, "No MEntity found for name `{name}`")
- return
- end
+ var mentity = find_mentity(v, model, name)
+ if mentity == null then return
var mdoc = mentity.mdoc_or_fallback
if mdoc == null then
write_warning(v, "No MDoc for mentity `{name}`")
return
end
var name = args.first
- var mentity = find_mentity(model, name)
+ var mentity = find_mentity(v, model, name)
+ if mentity == null then return
if mentity isa MPackage then
write_list(v, mentity.mgroups)
else if mentity isa MGroup then
return
end
var name = args.first
- var mentity = find_mentity(model, name)
- if mentity == null then
- write_error(v, "No MEntity found for name `{name}`")
- return
- end
+ var mentity = find_mentity(v, model, name)
+ if mentity == null then return
if mentity isa MClass then mentity = mentity.intro
if mentity isa MProperty then mentity = mentity.intro
var source = render_source(mentity, v.decorator.as(NitwebDecorator).modelbuilder)
return
end
var name = args.first
- var mentity = find_mentity(model, name)
- if mentity == null then
- write_error(v, "No MEntity found for name `{name}`")
- return
- end
+ var mentity = find_mentity(v, model, name)
+ if mentity == null then return
var g = new InheritanceGraph(mentity, model)
v.add g.draw(3, 3).to_svg
end
var stars: MongoCollection is lazy do return db.collection("stars")
end
-# Group all api handlers in one router
-class APIFeedbackRouter
- super APIRouter
-
- init do
- use("/stars/:id", new APIStars(config))
+redef class APIRouter
+ redef init do
+ super
+ use("/feedback/stars/:id", new APIStars(config))
end
end
redef fun get(req, res) do
var mentity = mentity_from_uri(req, res)
- if mentity == null then
- res.error 404
- return
- end
-
+ if mentity == null then return
res.json mentity_ratings(mentity)
end
redef fun post(req, res) do
var mentity = mentity_from_uri(req, res)
- if mentity == null then
- res.error 404
- return
- end
+ if mentity == null then return
var obj = req.body.parse_json
if not obj isa JsonObject then
- res.error 400
+ res.api_error(400, "Expected a JSON object")
return
end
var rating = obj["rating"]
if not rating isa Int then
- res.error 400
+ res.api_error(400, "Expected a key `rating`")
return
end
import dot
import uml
-# Group all api handlers in one router.
-class APIGraphRouter
- super APIRouter
-
+redef class APIRouter
init do
- use("/inheritance/:id", new APIInheritanceGraph(config))
+ super
+ use("/graph/inheritance/:id", new APIInheritanceGraph(config))
end
end
super APIHandler
redef fun get(req, res) do
+ var mentity = mentity_from_uri(req, res)
+ if mentity == null then return
var pdepth = req.int_arg("pdepth")
var cdepth = req.int_arg("cdepth")
- var mentity = mentity_from_uri(req, res)
- if mentity == null then
- res.error 404
- return
- end
var g = new InheritanceGraph(mentity, view)
res.send g.draw(pdepth, cdepth).to_svg
end
import web_base
import metrics
-# Group all api handlers in one router.
-class APIMetricsRouter
- super APIRouter
-
- init do
- use("/structural/:id", new APIStructuralMetrics(config))
+redef class APIRouter
+ redef init do
+ super
+ use("/metrics/structural/:id", new APIStructuralMetrics(config))
end
end
redef fun get(req, res) do
var mentity = mentity_from_uri(req, res)
- if mentity == null then
- res.error 404
- return
- end
+ if mentity == null then return
var metrics = mentity.collect_metrics(self)
if metrics == null then
- res.error 404
+ res.api_error(404, "No metric for mentity `{mentity.full_name}`")
return
end
res.json metrics
import highlight
import uml
+redef class APIRouter
+ redef init do
+ super
+ use("/list", new APIList(config))
+ use("/search", new APISearch(config))
+ use("/random", new APIRandom(config))
+ use("/entity/:id", new APIEntity(config))
+ use("/code/:id", new APIEntityCode(config))
+ use("/uml/:id", new APIEntityUML(config))
+ use("/linearization/:id", new APIEntityLinearization(config))
+ use("/defs/:id", new APIEntityDefs(config))
+ use("/inheritance/:id", new APIEntityInheritance(config))
+ end
+end
+
# List all mentities.
#
# MEntities can be filtered on their kind using the `k` parameter.
redef fun get(req, res) do
var mentity = mentity_from_uri(req, res)
- if mentity == null then
- res.error 404
- return
- end
+ if mentity == null then return
res.json mentity.hierarchy_poset(view)[mentity]
end
end
redef fun get(req, res) do
var mentity = mentity_from_uri(req, res)
- if mentity == null then
- res.error 404
- return
- end
+ if mentity == null then return
var lin = mentity.collect_linearization(config.mainmodule)
if lin == null then
- res.error 404
+ res.api_error(404, "No linearization for mentity `{mentity.full_name}`")
return
end
res.json new JsonArray.from(lin)
redef fun get(req, res) do
var mentity = mentity_from_uri(req, res)
+ if mentity == null then return
var arr = new JsonArray
if mentity isa MModule then
for mclassdef in mentity.mclassdefs do arr.add mclassdef
else if mentity isa MProperty then
for mpropdef in mentity.mpropdefs do arr.add mpropdef
else
- res.error 404
+ res.api_error(404, "No definition list for mentity `{mentity.full_name}`")
return
end
res.json arr
redef fun get(req, res) do
var mentity = mentity_from_uri(req, res)
+ if mentity == null then return
var dot
if mentity isa MClassDef then mentity = mentity.mclass
if mentity isa MClass then
var uml = new UMLModel(view, mentity)
dot = uml.generate_package_uml.write_to_string
else
- res.error 404
+ res.api_error(404, "No UML for mentity `{mentity.full_name}`")
return
end
res.send render_dot(dot)
if mentity == null then return
var source = render_source(mentity)
if source == null then
- res.error 404
+ res.api_error(404, "No code for mentity `{mentity.full_name}`")
return
end
res.send source
import api_docdown
import api_metrics
import api_feedback
+
+redef class APIRouter
+ redef init do
+ super
+ use("/*", new APIErrorHandler(config)) # catch 404 errors
+ end
+end
+
+# Error handler user to catch non resolved request by the API
+#
+# Displays a JSON formatted 404 error.
+class APIErrorHandler
+ super APIHandler
+
+ redef fun all(req, res) do
+ res.api_error(404, "Not found")
+ end
+end
var modelbuilder: ModelBuilder
end
-# Specific nitcorn Action that uses a Model
-class ModelHandler
+# Specific handler for the nitweb API.
+abstract class APIHandler
super Handler
# App config.
return model.mentity_by_full_name(full_name.from_percent_encoding)
end
- # Init the model view from the `req` uri parameters.
- fun init_model_view(req: HttpRequest): ModelView do
- var view = new ModelView(config.model)
- var show_private = req.bool_arg("private") or else false
- if not show_private then view.min_visibility = protected_visibility
-
- view.include_fictive = req.bool_arg("fictive") or else false
- view.include_empty_doc = req.bool_arg("empty-doc") or else true
- view.include_test_suite = req.bool_arg("test-suite") or else false
- view.include_attribute = req.bool_arg("attributes") or else true
-
- return view
- end
-end
-
-# Specific handler for nitweb API.
-abstract class APIHandler
- super ModelHandler
-
# The JSON API does not filter anything by default.
#
# So we can cache the model view.
fun mentity_from_uri(req: HttpRequest, res: HttpResponse): nullable MEntity do
var id = req.param("id")
if id == null then
- res.error 400
+ res.api_error(400, "Expected mentity full name")
return null
end
var mentity = find_mentity(view, id)
if mentity == null then
- res.error 404
+ res.api_error(404, "MEntity `{id}` not found")
end
return mentity
end
class APIRouter
super Router
- # App config.
+ # App config
var config: NitwebConfig
end
+redef class HttpResponse
+
+ # Return an HTTP error response with `status`
+ #
+ # Like the rest of the API, errors are formated as JSON:
+ # ~~~json
+ # { "status": 404, "message": "Not found" }
+ # ~~~
+ fun api_error(status: Int, message: String) do
+ json(new APIError(status, message), status)
+ end
+end
+
+# An error returned by the API.
+#
+# Can be serialized to json.
+class APIError
+ super Jsonable
+
+ # Reponse status
+ var status: Int
+
+ # Response error message
+ var message: String
+
+ # Json Object for this error
+ var json: JsonObject is lazy do
+ var obj = new JsonObject
+ obj["status"] = status
+ obj["message"] = message
+ return obj
+ end
+
+ redef fun to_json do return json.to_json
+end
+
redef class MEntity
# URL to `self` within the web interface.
end
# Get the full json repesentation of `self` with MEntityRefs resolved.
- fun api_json(handler: ModelHandler): JsonObject do return json
+ fun api_json(handler: APIHandler): JsonObject do return json
end
redef class MEntityRef
test_parser
test_highlight
test_model_visitor
+test_model_index
^nit
+test_nitunit4/test_bad_comp.nit:27,10--19: Error: method or variable `bad_method` unknown in `TestSuiteBadComp`.
test_nitunit4/test_bad_comp2.nit:19,7--22: Error: a class named `test_nitunit4::TestSuiteBadComp` is already defined in module `test_bad_comp` at test_nitunit4/test_bad_comp.nit:19,1--29,3.
==== Test-suite of module test_nitunit4::test_bad_comp | tests: 2
[KO] test_nitunit4$TestSuiteBadComp$test_good
-\e[1m\e[32m# 2 result(s) for 'comment: A'\e[m\e[m
+\e[1m\e[34m# \e[m\e[m\e[1m\e[34mA\e[m\e[m
+\e[1m\e[30mclass A\e[m\e[m
+
+ \e[1m\e[32mclass A\e[m\e[m
+ \e[1m\e[30mbase_simple3.nit:29,1--32,3\e[m\e[m
- \e[1m\e[32mC\e[m\e[m \e[1m\e[34mA\e[m\e[m
- \e[1m\e[30mbase_simple3::A\e[m\e[m
- class A
- \e[30mbase_simple3.nit:29,1--32,3\e[m
- \e[1m\e[32mC\e[m\e[m \e[1m\e[34mA\e[m\e[m
- \e[1m\e[30mbase_simple3::base_simple3::A\e[m\e[m
- class A
- \e[30mbase_simple3.nit:29,1--32,3\e[m
-\e[1m\e[32m# 2 result(s) for 'comment: foo'\e[m\e[m
+\e[1m\e[34m# \e[m\e[m\e[1m\e[34mfoo\e[m\e[m
+\e[1m\e[30mfun foo\e[m\e[m
+
+ \e[1m\e[32mfun foo\e[m\e[m
+ \e[1m\e[30mbase_simple3.nit:49,1--19\e[m\e[m
- \e[1m\e[32mF\e[m\e[m \e[1m\e[34mfoo\e[m\e[m
- \e[1m\e[30mbase_simple3::Sys::foo\e[m\e[m
- fun foo
- \e[30mbase_simple3.nit:49,1--19\e[m
- \e[1m\e[32mF\e[m\e[m \e[1m\e[34mfoo\e[m\e[m
- \e[1m\e[30mbase_simple3::base_simple3::Sys::foo\e[m\e[m
- fun foo
- \e[30mbase_simple3.nit:49,1--19\e[m
-\e[1m\e[32m# 3 result(s) for 'comment: base_simple3'\e[m\e[m
+\e[1m\e[34m# \e[m\e[m\e[1m\e[34mbase_simple3\e[m\e[m
+\e[1m\e[30mpackage base_simple3\e[m\e[m
- \e[1m\e[32mP\e[m\e[m \e[1m\e[34mbase_simple3\e[m\e[m
- \e[1m\e[30mbase_simple3\e[m\e[m
- package base_simple3
- \e[30mbase_simple3.nit:17,1--66,13\e[m
+ \e[1m\e[32mpackage base_simple3\e[m\e[m
+ \e[1m\e[30mbase_simple3.nit:17,1--66,13\e[m\e[m
- \e[1m\e[32mG\e[m\e[m \e[1m\e[34mbase_simple3\e[m\e[m
- \e[1m\e[30mbase_simple3\e[m\e[m
- group base_simple3
- \e[30mbase_simple3.nit:17,1--66,13\e[m
- \e[1m\e[32mM\e[m\e[m \e[1m\e[34mbase_simple3\e[m\e[m
- \e[1m\e[30mbase_simple3::base_simple3\e[m\e[m
- module base_simple3
- \e[30mbase_simple3.nit:17,1--66,13\e[m
--- /dev/null
+Usage: [OPTION]... [ARG]...
+Use --help for help
--- /dev/null
+# Obj
+
+ * 1: Object (test_prog::Object)
--- /dev/null
+# elfves
+
+ * 4: Elf (test_prog::Elf)
+ * 5: Float (test_prog::Float)
+ * 5: Race (test_prog::Race)
+ * 5: careers (test_prog::careers)
+ * 5: excluded (excluded>)
+ * 5: excluded (excluded::excluded)
+ * 5: excluded (excluded)
+ * 5: game (test_prog>game>)
+ * 5: game (test_prog::game)
+ * 4: races (test_prog::races)
--- /dev/null
+# Dwarves
+
+ * 13: List (test_prog::List)
+ * 12: Sys (test_prog::Sys)
+ * 8: excluded (excluded>)
+ * 7: excluded (excluded)
+ * 13: game (test_prog>game>)
+ * 13: races (test_prog::races)
+ * 13: rpg (test_prog>rpg>)
+ * 13: rpg (test_prog::rpg)
+ * 9: test_prog (test_prog>)
+ * 9: test_prog (test_prog)
--- /dev/null
+# test_prof::Dwarves
+
+ * 5: Career (test_prog::Career)
+ * 4: Dwarf (test_prog::Dwarf)
+ * 6: Game (test_prog::Game)
+ * 6: Race (test_prog::Race)
+ * 5: Starter (test_prog::Starter)
+ * 5: careers (test_prog::careers)
+ * 7: character (test_prog::character)
+ * 6: game (test_prog::game)
+ * 5: races (test_prog::races)
+ * 7: rpg (test_prog::rpg)
--- /dev/null
+# Obj
+
+ * 3: * (test_prog::Float::*)
+ * 3: / (test_prog::Int::/)
+ * 3: == (test_prog::Object::==)
+ * 3: Elf (test_prog::Elf)
+ * 3: Int (test_prog::Int)
+ * 1: Object (test_prog::Object)
+ * 3: Sys (test_prog::Sys)
+ * 3: age (test_prog::Character::age)
+ * 3: rpg (test_prog>rpg>)
+ * 3: rpg (test_prog::rpg)
--- /dev/null
+# C
+
+ * 1: * (test_prog::Float::*)
+ * 1: * (test_prog::Int::*)
+ * 1: + (test_prog::Float::+)
+ * 1: - (test_prog::Int::-)
+ * 1: - (test_prog::Float::-)
+ * 1: / (test_prog::Float::/)
+ * 1: / (test_prog::Int::/)
+ * 1: > (test_prog::Int::>)
+ * 1: > (test_prog::Float::>)
+ * 1: Career (test_prog::Career)
--- /dev/null
+# to
+
+ * 2: * (test_prog::Float::*)
+ * 2: + (test_prog::Float::+)
+ * 2: - (test_prog::Float::-)
+ * 2: / (test_prog::Float::/)
+ * 2: / (test_prog::Int::/)
+ * 2: == (test_prog::Object::==)
+ * 2: > (test_prog::Int::>)
+ * 2: > (test_prog::Float::>)
+ * 1: to_f (test_prog::Int::to_f)
+ * 2: total_strengh (test_prog::Character::total_strengh)
--- /dev/null
+# Dwa
+
+ * 13: Dwarf (test_prog::Dwarf)
+ * 14: Game (test_prog::Game)
+ * 9: excluded (excluded>)
+ * 8: excluded (excluded)
+ * 14: game (test_prog>game>)
+ * 14: game (test_prog::game)
+ * 14: rpg (test_prog::rpg)
+ * 14: rpg (test_prog>rpg>)
+ * 10: test_prog (test_prog>)
+ * 9: test_prog (test_prog)
--- /dev/null
+# test_prog::C
+
+ * 1: Career (test_prog::Career)
+ * 2: Character (test_prog::Character)
+ * 3: Combatable (test_prog::Combatable)
+ * 3: Elf (test_prog::Elf)
+ * 3: Int (test_prog::Int)
+ * 3: Sys (test_prog::Sys)
+ * 4: game (test_prog::game)
+ * 3: rpg (test_prog::rpg)
+ * 3: test_prog (test_prog)
+ * 3: test_prog (test_prog>)
--- /dev/null
+# C
+
+ * 1: * (test_prog::Float::*)
+ * 1: * (test_prog::Int::*)
+ * 1: + (test_prog::Float::+)
+ * 1: - (test_prog::Int::-)
+ * 1: - (test_prog::Float::-)
+ * 1: / (test_prog::Float::/)
+ * 1: / (test_prog::Int::/)
+ * 1: > (test_prog::Int::>)
+ * 1: > (test_prog::Float::>)
+ * 1: Career (test_prog::Career)
--- /dev/null
+# to
+
+ * 2: * (test_prog::Int::*)
+ * 2: * (test_prog::Float::*)
+ * 2: + (test_prog::Float::+)
+ * 2: - (test_prog::Float::-)
+ * 2: / (test_prog::Float::/)
+ * 2: / (test_prog::Int::/)
+ * 2: > (test_prog::Float::>)
+ * 2: > (test_prog::Int::>)
+ * 1: to_f (test_prog::Int::to_f)
+ * 2: total_strengh (test_prog::Character::total_strengh)
--- /dev/null
+# C
+
+ * 1: Career (test_prog::Career)
+ * 2: Character (test_prog::Character)
+ * 3: Combatable (test_prog::Combatable)
--- /dev/null
+# Foo
+
+ * 3: != (test_prog::Object::!=)
+ * 3: + (test_prog::Int::+)
+ * 2: Bool (test_prog::Bool)
+ * 3: Elf (test_prog::Elf)
+ * 3: Float (test_prog::Float)
+ * 3: Int (test_prog::Int)
+ * 3: Sys (test_prog::Sys)
+ * 3: dps (test_prog::Weapon::dps)
+ * 3: rpg (test_prog::rpg)
+ * 3: rpg (test_prog>rpg>)
--- /dev/null
+# test_prog::C
+
+ * 1: Career (test_prog::Career)
+ * 2: Character (test_prog::Character)
+ * 3: Combatable (test_prog::Combatable)
+ * 3: Elf (test_prog::Elf)
+ * 3: Int (test_prog::Int)
+ * 3: Sys (test_prog::Sys)
+ * 3: rpg (test_prog::rpg)
+ * 3: test_prog (test_prog::test_prog)
+ * 3: test_prog (test_prog)
+ * 3: test_prog (test_prog>)
--- /dev/null
+# e
+
+ * 2: endurance_bonus (test_prog::Career::endurance_bonus)
+ * 1: excluded (excluded>)
+ * 1: excluded (excluded::excluded)
+ * 1: excluded (excluded)
+ * 3: endurance_bonus= (test_prog::Career::endurance_bonus=)
--- /dev/null
+# to
+
+ * 1: to_f (test_prog::Int::to_f)
+ * 3: total_endurance (test_prog::Character::total_endurance)
+ * 4: total_intelligence (test_prog::Character::total_intelligence)
+ * 2: total_strengh (test_prog::Character::total_strengh)
--- /dev/null
+# test_prog::
+
+ * 6: Bool (test_prog::Bool)
+ * 4: Elf (test_prog::Elf)
+ * 9: Game (test_prog::Game)
+ * 2: Int (test_prog::Int)
+ * 7: List (test_prog::List)
+ * 8: Race (test_prog::Race)
+ * 3: Sys (test_prog::Sys)
+ * 5: game (test_prog::game)
+ * 10: races (test_prog::races)
+ * 1: rpg (test_prog::rpg)
--- /dev/null
+# test_prog::C
+
+ * 1: Career (test_prog::Career)
+ * 2: Character (test_prog::Character)
+ * 3: Combatable (test_prog::Combatable)
+ * 4: age (test_prog::Character::age)
+ * 8: name (test_prog::Character::name)
+ * 7: quit (test_prog::Character::quit)
+ * 6: race (test_prog::Character::race)
+ * 5: sex (test_prog::Character::sex)
+ * 9: age= (test_prog::Character::age=)
+ * 10: sex= (test_prog::Character::sex=)
--- /dev/null
+# A
+
+ * 1: * (test_prog::Int::*)
+ * 1: * (test_prog::Float::*)
+ * 1: + (test_prog::Int::+)
+ * 1: + (test_prog::Float::+)
+ * 1: - (test_prog::Int::-)
+ * 1: - (test_prog::Float::-)
+ * 1: / (test_prog::Float::/)
+ * 1: / (test_prog::Int::/)
+ * 1: > (test_prog::Float::>)
+ * 1: > (test_prog::Int::>)
--- /dev/null
+# Foo
+
+ * 3: * (test_prog::Int::*)
+ * 3: / (test_prog::Int::/)
+ * 2: Bool (test_prog::Bool)
+ * 3: Elf (test_prog::Elf)
+ * 3: Float (test_prog::Float)
+ * 3: Int (test_prog::Int)
+ * 3: Sys (test_prog::Sys)
+ * 3: rpg (test_prog::rpg)
+ * 3: rpg (test_prog>rpg>)
+ * 3: sex (test_prog::Character::sex)
--- /dev/null
+test_prog Obj --name-prefix --no-color
+test_prog C --name-prefix --no-color
+test_prog e --name-prefix --no-color
+test_prog to --name-prefix --no-color
+test_prog C --full-name-prefix --no-color
+test_prog test_prog:: --full-name-prefix --no-color
+test_prog test_prog::C --full-name-prefix --no-color
+test_prog A --name-similarity --no-color
+test_prog Foo --name-similarity --no-color
+test_prog elfves --name-similarity --no-color
+test_prog Dwarves --full-name-similarity --no-color
+test_prog test_prof::Dwarves --full-name-similarity --no-color
+test_prog Obj --name --no-color
+test_prog C --name --no-color
+test_prog to --name --no-color
+test_prog Dwa --full-name --no-color
+test_prog test_prog::C --full-name --no-color
+test_prog C --no-color
+test_prog to --no-color
+test_prog Foo --no-color
+test_prog test_prog::C --no-color