- Refactor of the app to be a bit more grown up
 - Use the Vue Router to provide a browser history

API Server:
 - Begin support for attribute searching
This commit is contained in:
Simon Marsh 2019-02-12 11:15:01 +00:00
parent c027cd5410
commit 1e467d5d3b
Signed by: burble
GPG Key ID: 7B9FE8780CFB6593
4 changed files with 453 additions and 357 deletions

View File

@ -1,16 +1,21 @@
# dn42regsrv # dn42regsrv
A REST API for the DN42 registry, written in Go, to provide a bridge between A REST API for the DN42 registry, written in Go, to provide a bridge between
interactive applications and the DN42 registry. interactive applications and registry data.
A public instance of the API and explorer web app can be accessed via:
* [https://explorer.burble.com/](https://explorer.burble.com/) (Internet link)
* [http://collector.dn42:8042/](http://collector.dn42:8042/) (DN42 Link)
## Features ## Features
- REST API for querying DN42 registry objects * REST API for querying DN42 registry objects
- Able to decorate objects with relationship information based on SCHEMA type definitions * Able to decorate objects with relationship information based on SCHEMA type definitions
- Includes a simple webserver for delivering static files which can be used to deliver * Includes a simple webserver for delivering static files which can be used to deliver
basic web applications utilising the API (such as the included DN42 Registry Explorer) basic web applications utilising the API (such as the included DN42 Registry Explorer)
- Automatic pull from the DN42 git repository to keep the registry up to date * Automatic pull from the DN42 git repository to keep the registry up to date
- Included responsive web app for exploring the registry * Includes a responsive web app for exploring the registry
## Building ## Building
@ -27,8 +32,8 @@ Use --help to view configurable options
${GOPATH}/bin/dn42regsrv --help ${GOPATH}/bin/dn42regsrv --help
``` ```
The server requires access to a clone of the DN42 registry and for the git executable The server requires access to a clone of the DN42 registry and for
to be accessible. the git executable to be accessible.
If you want to use the auto pull feature then the registry must If you want to use the auto pull feature then the registry must
also be writable by the server. also be writable by the server.
@ -60,6 +65,5 @@ Please feel free to raise issues or create pull requests for the project git rep
### DN42 Registry Explorer Web App ### DN42 Registry Explorer Web App
- Add search history and fix going back
- Allow for attribute searches - Allow for attribute searches

View File

@ -2,51 +2,21 @@
// DN42 Registry Explorer // DN42 Registry Explorer
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////// // global store for data that is loaded once
// registry-stats component const GlobalStore = {
data: {
Vue.component('registry-stats', { RegStats: null,
template: '#registry-stats-template', Index: null
data() {
return {
state: "loading",
error: "",
types: null,
}
},
methods: {
updateSearch: function(str) {
vm.updateSearch(str)
},
reload: function(event) {
this.types = null,
this.state = "loading"
axios
.get('/api/registry/')
.then(response => {
this.types = response.data
this.state = 'complete'
})
.catch(error => {
this.error = error
this.state = 'error'
console.log(error)
})
}
},
mounted() {
this.reload()
} }
}) }
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
// registry object component // registry object component
Vue.component('reg-object', { Vue.component('reg-object', {
template: '#reg-object-template', template: '#reg-object-template',
props: [ 'link' ], props: [ 'link', 'content' ],
data() { data() {
return { } return { }
}, },
@ -97,201 +67,267 @@ Vue.component('reg-attribute', {
}) })
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
// construct a search URL from a search term // search input component
function matchObjects(objects, rtype, term) { Vue.component('search-input', {
template: '#search-input-template',
data() {
return {
search: '',
searchTimeout: 0
}
},
methods: {
var results = [ ] debounceSearch: function(value) {
if (this.searchTimeout) {
for (const obj in objects) { clearTimeout(this.searchTimeout)
var s = objects[obj].toLowerCase()
var pos = s.indexOf(term)
if (pos != -1) {
if ((pos == 0) && (s == term)) {
// exact match, return just this result
return [[ rtype, objects[obj] ]]
} }
results.push([ rtype, objects[obj] ])
}
}
return results // map an empty search box to the landing page
} if (value == "") { value = "/" }
this.searchTimeout = setTimeout(
function searchFilter(index, term) { this.$router.push.bind(this.$router, value), 500
)
var results = [ ]
// comparisons are lowercase
term = term.toLowerCase()
// includes a '/' ? search only in that type
var slash = term.indexOf('/')
if (slash != -1) {
var rtype = term.substring(0, slash)
var term = term.substring(slash + 1)
objects = index[rtype]
if (objects != null) {
results = matchObjects(objects, rtype, term)
}
}
else {
// walk though the entire index
for (const rtype in index) {
results = results.concat(matchObjects(index[rtype], rtype, term))
}
}
return results
}
//////////////////////////////////////////////////////////////////////////
// main application
// application data
var appData = {
searchInput: '',
searchTimeout: 0,
state: '',
debug: "",
index: null,
filtered: null,
result: null
}
// methods
var appMethods = {
loadIndex: function(event) {
this.state = 'loading'
this.searchInput = 'Initialisation ...'
axios
.get('/api/registry/*')
.then(response => {
this.index = response.data
// if a query parameter has been passed,
// update the search
if (window.location.search != "") {
var param = window.location.search.substr(1)
this.$nextTick(this.updateSearch(param))
}
else {
this.state = ''
this.searchInput = ''
}
})
.catch(error => {
// what to do here ?
console.log(error)
})
},
// called on every search input change
debounceSearchInput: function(value) {
if (this.search_timeout) {
clearTimeout(this.search_timeout)
}
// reset if searchbox is empty
if (value == "") {
this.state = ""
this.searchInput = ""
this.filtered = null
this.results = null
return
}
this.search_timeout =
setTimeout(this.updateSearch.bind(this,value),500)
},
// called after the search input has been debounced
updateSearch: function(value) {
this.searchInput = value
this.filtered = searchFilter(this.index, value)
if (this.filtered.length == 0) {
this.state = "noresults"
}
else if (this.filtered.length == 1) {
this.state = "loading"
var details = this.filtered[0]
query = '/api/registry/' + details[0] + '/' + details[1]
axios
.get(query)
.then(response => {
this.state = 'result'
this.result = response.data
})
.catch(error => {
this.error = error
this.state = 'error'
})
}
else {
this.state = "resultlist"
this.result = this.filtered
}
},
copyPermalink: function() {
// create a temporary textarea element off the page
var target = document.createElement("textarea")
target.style.position = "absolute"
target.style.left = "-9999px"
target.style.top = "0"
target.id = "_hidden_permalink_"
document.body.appendChild(target)
// set the text area content
target.textContent = this.permalink
// copy it to the clipboard
var currentFocus = document.activeElement
target.focus()
target.select()
document.execCommand('copy')
// and return to normal
if (currentFocus && typeof currentFocus.focus === "function") {
currentFocus.focus()
}
document.body.removeChild(target)
}
}
// intialise Vue instance
var vm = new Vue({
el: '#explorer',
data: appData,
methods: appMethods,
computed: {
permalink: function() {
return window.location.origin + '/?' + this.searchInput
} }
}, },
mounted() { mounted() {
this.loadIndex()
this.$nextTick(function() { // listen to search updates and set the search text appropriately
$('.popover-dismiss').popover({ this.$root.$on('SearchChanged', value => {
trigger: 'focus' this.search = value
})
}) })
} }
}) })
//////////////////////////////////////////////////////////////////////////
// registry-stats component
Vue.component('registry-stats', {
template: '#registry-stats-template',
data() {
return {
state: null,
error: null,
store: GlobalStore.data
}
},
methods: {
// just fetch the stats from the API server via axios
reload(event) {
this.store.RegStats = null
this.state = "loading"
axios
.get('/api/registry/')
.then(response => {
this.store.RegStats = response.data
this.state = 'complete'
})
.catch(error => {
this.error = error
this.state = 'error'
console.log(error)
})
}
},
mounted() {
if (this.store.RegStats == null) {
this.reload()
}
}
})
//////////////////////////////////////////////////////////////////////////
// root component
Vue.component('app-root', {
template: '#app-root-template',
mounted() {
this.$root.$emit('SearchChanged', '')
}
})
//////////////////////////////////////////////////////////////////////////
// search results
Vue.component('app-search', {
template: '#app-search-template',
data() {
return {
state: null,
search: null,
results: null,
store: GlobalStore.data
}
},
// trigger a search on route update and on mount
beforeRouteUpdate(to, from, next) {
this.search = to.params.pathMatch
this.doSearch()
next()
},
mounted() {
// store the search for later
this.search = this.$route.params.pathMatch
// the index must be loaded before any real work takes place
if (this.store.Index == null) {
this.loadIndex()
}
else {
// index was already loaded, go search
this.doSearch()
}
},
methods: {
// load the search index from the API
loadIndex() {
this.state = 'loading'
this.$root.$emit('SearchChanged', 'Initialising ...')
axios
.get('/api/registry/*')
.then(response => {
this.store.Index = response.data
// if a query parameter has been passed,
// then go search
if (this.search != null) {
this.$nextTick(this.doSearch())
}
})
.catch(error => {
this.state = 'error'
console.log(error)
})
},
// substring match object names against a search
matchObjects(objects, rtype, term) {
var results = [ ]
// check each object
for (const obj in objects) {
// matches are all lower case
var s = objects[obj].toLowerCase()
var pos = s.indexOf(term)
if (pos != -1) {
if ((pos == 0) && (s == term)) {
// exact match, return just this result
return [[ rtype, objects[obj] ]]
}
results.push([ rtype, objects[obj] ])
}
}
return results
},
// filter the index to find matches
searchFilter() {
var results = [ ]
var index = this.store.Index
// comparisons are lowercase
var term = this.search.toLowerCase()
// check if search includes a '/'
var slash = term.indexOf('/')
if (slash != -1) {
// match only on the specific type
var rtype = term.substring(0, slash)
var term = term.substring(slash + 1)
objects = index[rtype]
if (objects != null) {
results = this.matchObjects(objects, rtype, term)
}
}
else {
// walk though the entire index
for (const rtype in index) {
var objlist = this.matchObjects(index[rtype], rtype, term)
results = results.concat(objlist)
}
}
return results
},
// perform the search and present results
doSearch() {
// notify other components that the search is updated
this.$root.$emit('SearchChanged', this.search)
// filter matches against the index
filtered = this.searchFilter()
// got nothing ?
if (filtered.length == 0) {
this.state = "noresults"
}
// just one result
else if (filtered.length == 1) {
var objname = filtered[0]
// load the object from the API
this.state = 'loading'
query = '/api/registry/' + objname[0] + '/' + objname[1]
axios
.get(query)
.then(response => {
this.state = 'result'
this.result = response.data
})
.catch(error => {
this.error = error
this.state = 'error'
})
}
// lots of results
else {
this.state = "resultlist"
this.result = filtered
}
}
}
})
//////////////////////////////////////////////////////////////////////////
// main vue application starts here
// initialise the Vue Router
const router = new VueRouter({
routes: [
{ path: '/', component: Vue.component('app-root') },
{ path: '/*', component: Vue.component('app-search') }
]
})
// and the main app instance
const vm = new Vue({
el: '#explorer_app',
data: {
},
router
})
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////

View File

@ -1,46 +1,56 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<title>DN42 Registry Explorer</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="bootstrap.min.css"> <link rel="stylesheet" href="bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<!-- Style overrides -->
<style> <style>
.material-icons { display:inline-flex;vertical-align:middle } .material-icons { display:inline-flex;vertical-align:middle }
.table th, .table td { padding: 0.2rem; border: none } body { box-shadow: inset 0 2em 10em rgba(0,0,0,0.4); min-height: 100vh }
pre { margin-bottom: 0px }
.regref span {
padding: 0.3em 1em 0.3em 1em; margin: 0.4em;
white-space: nowrap; display:inline-block;
}
a { cursor: pointer }
</style> </style>
<title>DN42 Registry Explorer</title>
</head> </head>
<body style="box-shadow: inset 0 2em 10em rgba(0,0,0,0.4); min-height: 100vh">
<div id="explorer"> <body>
<div id="explorer_app">
<nav class="navbar navbar-fixed-top navbar-expand-md navbar-dark bg-dark"> <nav class="navbar navbar-fixed-top navbar-expand-md navbar-dark bg-dark">
<form class="form-inline" id="SearchForm"> <search-input></search-input>
<input v-bind:value="searchInput" <div class="collapse navbar-collapse w-100 ml-auto text-nowrap">
v-on:input="debounceSearchInput($event.target.value)" <div class="ml-auto"><router-link class="navbar-brand"
class="form-control-lg" size="30" type="search" to="/">Registry Explorer</router-link>&nbsp;<a class="pull-right navbar-brand"
placeholder="Search the registry" aria-label="Search"/> href="https://dn42.us/"><img src="/dn42_logo.png" width="173" height="60"/></a>
</form> </div>
<a tabindex="0" style="margin-left: 1em" class="popover-dismiss btn btn-sm btn-dark" role="button" </div>
data-toggle="popover" data-placement="bottom" data-trigger="focus"
title="Copied to clipboard" v-on:click="copyPermalink"
v-bind:data-content="permalink">link</a>
<div class="collapse navbar-collapse w-100 ml-auto">
<div class="ml-auto"><a class="navbar-brand"
href="/">Registry Explorer</a>&nbsp;<a class="pull-right navbar-brand"
href="https://dn42.us/"><img src="/dn42_logo.png" width="173"
height="60"/></a></div>
</nav> </nav>
<div style="padding: 1em">
<div v-show="searchInput == ''"> <router-view></router-view>
</div>
<footer class="page-footer font-small">
<div style="margin-top: 20px; padding: 5px">
<a href="https://git.dn42.us/burble/dn42regsrv">Source Code</a>.
Powered by
<a href="https://getbootstrap.com/">Bootstrap</a>,
<a href="https://vuejs.org">Vue.js</a>,
<a href="https://github.com/axios/axios">axios</a>,
<a href="http://alexcorvi.github.io/anchorme.js/">Anchorme</a>.
</div>
</footer>
<script type="text/x-template" id="search-input-template">
<form class="form-inline" id="SearchForm">
<input v-bind:value="search"
v-on:input="debounceSearch($event.target.value)"
class="form-control-lg" size="30" type="search"
placeholder="Search the registry" aria-label="Search"/>
</form>
</script>
<script type="text/x-template" id="app-root-template">
<div class="p-3">
<div class="jumbotron"> <div class="jumbotron">
<h1>DN42 Registry Explorer</h1> <h1>DN42 Registry Explorer</h1>
<p class="lead">Just start typing in the search box to start searching the registry</p> <p class="lead">Just start typing in the search box to start searching the registry</p>
@ -51,129 +61,137 @@
<li>Searches are case independent <li>Searches are case independent
<li>No need to hit enter, searches will start immediately <li>No need to hit enter, searches will start immediately
<li>Prefixing the search by a registry type followed by / will narrow the search to <li>Prefixing the search by a registry type followed by / will narrow the search to
just that type (e.g. <a v-on:click="updateSearch('domain/.dn42')" just that type (e.g. <router-link class="text-success"
class="text-success">domain/.dn42</a>&nbsp;) to="domain/.dn42">domain/.dn42</router-link>)</li>
<li>Searching for <b>type/</b> will return all the objects for that type (e.g. <li>Searching for <b>type/</b> will return all the objects for that type (e.g.
<a v-on:click="updateSearch('schema/')" class="text-success">schema/</a>&nbsp;) <router-link class="text-success" to="schema/">schema/</router-link>)</li>
<li>A blank search box will return you to these instructions <li>A blank search box will return you to these instructions</li>
<li>Just copy to the URL to link to search results</li>
<li>Searches are made on object names; searching the content of objects <li>Searches are made on object names; searching the content of objects
is not supported (yet!). is not supported (yet!).</li>
<li>Going back (or any search history) is also not supported yet.
</ul> </ul>
<hr/> <hr/>
<p>The registry explorer is a simple web app using <p>
The registry explorer is a simple web app using
<a href="https://git.dn42.us/burble/dn42regsrv">dn42regsrv</a>; <a href="https://git.dn42.us/burble/dn42regsrv">dn42regsrv</a>;
a REST API for the DN42 registry built with <a href="https://golang.org/">Go</a> a REST API for the DN42 registry built with <a href="https://golang.org/">Go</a>
</p>
</div> </div>
<registry-stats></registry-stats>
<registry-stats/>
</div> </div>
</script>
<script type="text/x-template" id="registry-stats-template">
<div class="container d-flex flex-column w-75">
<h5>Registry Stats</h5>
<section v-if="state == 'error'">
<div class="alert alert-warning clearfix" role="alert">
An error recurred whilst retrieving data from the API
<button type="button" class="float-right btn btn-primary"
v-on:click="reload"><span class="material-icons">refresh</span>
Refresh</button>
</div>
</section>
<section v-else-if="state == 'loading'">
<div class="alert alert-info" role="alert">Loading data ...</div>
</section>
<section v-else>
<p class="p-3 text-center">
<span v-for="(value, key) in store.RegStats"
class="mx-2 text-nowrap d-inline-block">
<router-link v-bind:to="'/'+key+'/'">{{ key }}</router-link>
: <b>{{ value }}</b>
</span>
</p>
</section>
</div>
</script>
<script type="text/x-template" id="app-search-template">
<div class="p-3">
<section v-if="state == 'loading'"> <section v-if="state == 'loading'">
<div class="alert alert-info" role="alert"> <div class="alert alert-info" role="alert">Loading data ...</div>
Loading data ...
</div>
</section> </section>
<section v-else-if="state == 'error'"> <section v-else-if="state == 'error'">
<div class="alert alert-primary clearfix" role="alert"> <div class="alert alert-warning clearfix" role="alert">
An error recurred whilst retrieving data <button type="button" class="float-right btn btn-warning" v-on:click="reload"><span class="material-icons">refresh</span>&nbsp;Refresh</button> An error recurred whilst retrieving search results
</div> </div>
</section> </section>
<section v-else-if="state == 'noresults'"> <section v-else-if="state == 'noresults'">
<h2>Searching for "{{ searchInput }}" ...</h2> <h2>Searching for "{{ search }}" ...</h2>
<div class="alert alert-dark" role="alert"> <div class="alert alert-dark" role="alert">
Sorry, no results found Sorry, no results found
</div> </div>
</section> </section>
<section v-else-if="state == 'resultlist'"> <section v-else-if="state == 'resultlist'">
<h2>Listing results for "{{ searchInput }}" ...</h2> <h2>Listing results for "{{ search }}" ...</h2>
<div class="container d-flex flex-row flex-wrap"> <div class="container d-flex flex-row flex-wrap">
<div style="text-align: center"> <div class="text-center">
<span v-for="value in result" style="margin: 0.5em 1em 0.5em 1em; display:inline-block"> <span v-for="ref in result">
<reg-object v-bind:link="value[0] + '/' + value[1]"></reg-object> <reg-object class="mx-4 my-2" v-bind:link="ref[0]+'/'+ref[1]"></reg-object>
</span> </span>
</div> </div>
</div> </div>
</section> </section>
<section v-else-if="state == 'result'"> <section v-else-if="state == 'result'">
<div v-for="(val, key) in result"> <div v-for="(val, key) in result">
<h2><reg-object v-bind:link="key"></reg-object></h2> <h2 class="mb-4"><reg-object v-bind:link="key"></reg-object></h2>
<div style="padding-left: 2em"> <div class="pl-4">
<table class="table"> <table class="table">
<thead> <thead>
<tr><th scope="col">Key</th><th scope="col">Value</th></tr> <tr><th scope="col">Key</th><th scope="col">Value</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="a in val.Attributes"> <tr v-for="a in val.Attributes">
<th scope="row" class="text-primary" style="white-space:nowrap">{{ a[0] }}</th> <th scope="row" class="border-0 py-1 pr-3 text-primary text-nowrap">
<td><reg-attribute v-bind:content="a[1]"></reg-attribute></td> {{ a[0] }}
</tr> </th>
</tbody> <td class="border-0 p-1">
</table> <reg-attribute v-bind:content="a[1]"></reg-attribute>
</div> </td>
<section v-if="val.Backlinks.length != 0"> </tr>
<p>Referenced by</p> </tbody>
<div style="padding-left: 2em"> </table>
<table class="table"> </div>
<tbody> <section v-if="val.Backlinks.length != 0">
<tr v-for="r in val.Backlinks"> <p>Referenced by</p>
<td><reg-object v-bind:link="r"></reg-object></td> <div class="pl-4">
</tr> <table class="table">
</tbody> <tbody>
</table> <tr v-for="rlink in val.Backlinks">
</div> <td class="border-0 p-1">
</section> <reg-object v-bind:link="rlink"></reg-object>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div> </div>
</section> </section>
<textarea class="d-none" ref="permalink">
{{ permalink }}
</textarea>
</div>
</div>
<footer class="page-footer font-small">
<div style="margin-top: 20px; padding: 5px">
<a href="https://git.dn42.us/burble/dn42regsrv">Source Code</a>.
Powered by
<a href="https://getbootstrap.com/">Bootstrap</a>,
<a href="https://vuejs.org">Vue.js</a>,
<a href="https://github.com/axios/axios">axios</a>,
<a href="http://alexcorvi.github.io/anchorme.js/">Anchorme</a>.
</div>
</footer>
</div>
<script type="text/x-template" id="registry-stats-template">
<div class="container d-flex flex-column w-75">
<h5>Registry Stats</h5>
<section v-if="state == 'error'">
<div class="alert alert-primary clearfix" role="alert">
An error recurred whilst retrieving data <button type="button" class="float-right btn btn-warning" v-on:click="reload"><span class="material-icons">refresh</span>&nbsp;Refresh</button>
</div>
</section>
<section v-else-if="state == 'loading'">
<div class="alert alert-info" role="alert">
Loading data ...
</div>
</section>
<section v-else> <section v-else>
<p style="padding:1em;text-align:center"> <div class="alert alert-warning" role="alert">
<span v-for="(value, key) in types" style="margin-left:0.5em;margin-right:0.5em;white-space:nowrap;display:inline-block"><a v-on:click="updateSearch(key + '/')" class="text-success">{{ key }}</a>:&nbsp;<b>{{ value }}</b>&nbsp;records</span> Something went wrong, an invalid search state was encountered ({{ state }})
</p> <p>{{ $route.params.pathMatch }}</p>
</div>
</section> </section>
</div> </div>
</script> </script>
<script type="text/x-template" id="reg-object-template"> <script type="text/x-template" id="reg-object-template">
<span class="regref"><a v-on:click="updateSearch(rtype + '/' + obj)" class="text-success" <span class="text-nowrap d-inline-block">
style="margin-right: 0.4em">{{ obj }}</a>&nbsp;<span <router-link class="text-success mr-2" v-bind:to="'/'+link">
class="badge badge-pill badge-dark text-muted">{{ rtype }}</span></span></span> {{ ( content ? content : obj ) }}
</router-link>
<span class="badge back-pill badge-dark test-muted">{{ rtype }}</span>
</span>
</script> </script>
<script type="text/x-template" id="reg-attribute-template"> <script type="text/x-template" id="reg-attribute-template">
@ -184,11 +202,12 @@ class="badge badge-pill badge-dark text-muted">{{ rtype }}</span></span></span>
</script> </script>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.2/dist/vue.js"></script> <script src="https://unpkg.com/vue"></script -->
<!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.2/dist/vue.min.js"></script> --> <script src="https://unpkg.com/vue-router"></script -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script>
<!-- script src="https://unpkg.com/vue@2.6.2/dist/vue.min.js"></script -->
<!-- script src="https://unpkg.com/vue-router@3.0.2/dist/vue-router.js"></script -->
<script src="anchorme.min.js"></script> <script src="anchorme.min.js"></script>
<script src="explorer.js"></script> <script src="explorer.js"></script>
</body> </body>

View File

@ -52,6 +52,7 @@ type RegAttributeSchema struct {
type RegTypeSchema struct { type RegTypeSchema struct {
Ref string Ref string
Attributes map[string]*RegAttributeSchema Attributes map[string]*RegAttributeSchema
KeyIndex map[string]map[*RegObject][]*RegAttribute
} }
// the registry itself // the registry itself
@ -78,10 +79,7 @@ func RegistryMakePath(t string, o string) string {
// attribute functions // attribute functions
// return a pointer to a RegType from a decorated attribute value // nothing here
func (*RegAttribute) ExtractRegType() *RegType {
return nil
}
// object functions // object functions
@ -144,6 +142,40 @@ func (schema *RegTypeSchema) validate(attributes []*RegAttribute) []*RegAttribut
return validated return validated
} }
// add an attribute to the key map
func (schema *RegTypeSchema) addKeyIndex(object *RegObject,
attribute *RegAttribute) {
objmap := schema.KeyIndex[attribute.Key]
// create a new object map if it didn't exist
if objmap == nil {
objmap = make(map[*RegObject][]*RegAttribute)
schema.KeyIndex[attribute.Key] = objmap
}
// add the object/attribute reference
objmap[object] = append(objmap[object], attribute)
}
// object functions
// add a backlink to an object
func (object *RegObject) addBacklink(ref *RegObject) {
// check if the backlink already exists, this could be the case
// if an object is referenced multiple times (e.g. admin-c & tech-c)
for _, blink := range object.Backlinks {
if blink == ref {
// already exists, just return as nothing to do
return
}
}
// didn't find a match, add the backlink
object.Backlinks = append(object.Backlinks, ref)
}
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
// reload the registry // reload the registry
@ -363,6 +395,7 @@ func (registry *Registry) parseSchema() {
typeSchema := &RegTypeSchema{ typeSchema := &RegTypeSchema{
Ref: typeName, Ref: typeName,
Attributes: make(map[string]*RegAttributeSchema), Attributes: make(map[string]*RegAttributeSchema),
KeyIndex: make(map[string]map[*RegObject][]*RegAttribute),
} }
// ensure the type exists // ensure the type exists
@ -455,11 +488,14 @@ func (registry *Registry) decorate() {
for _, attribute := range object.Data { for _, attribute := range object.Data {
cattribs += 1 cattribs += 1
// add this attribute to the key map
schema.addKeyIndex(object, attribute)
attribSchema := schema.Attributes[attribute.Key] attribSchema := schema.Attributes[attribute.Key]
// are there relations defined for this attribute ? // are there relations defined for this attribute ?
// attribSchema may be null if this attribute is user defined (x-*) // attribSchema may be null if this attribute is user defined (x-*)
if (attribSchema != nil) && attribute.matchRelation(object, if (attribSchema != nil) &&
attribSchema.Relations) { attribute.matchRelation(object, attribSchema.Relations) {
// matched // matched
cmatched += 1 cmatched += 1
} else { } else {
@ -500,7 +536,8 @@ func (attribute *RegAttribute) matchRelation(parent *RegObject,
attribute.RawValue, object.Ref) attribute.RawValue, object.Ref)
// and add a back reference to the related object // and add a back reference to the related object
object.Backlinks = append(object.Backlinks, parent) object.addBacklink(parent)
return true return true
} }