package io.trygvis.cj import scala.collection.JavaConversions._ import scala.xml.{Group, NodeSeq, Elem} import java.net.{MalformedURLException, URL, URI, URLEncoder} import net.hamnaberg.json.collection.{Property, Link, Json4sHelpers, JsonCollection} import net.hamnaberg.json.collection.Render.IMAGE import org.json4s.native.JsonMethods import java.io.{PrintWriter, StringWriter, Writer} class Views(baseUrl: String) { val notSet = Not set def tryLink(s: String) = try { val url = new URL(s) {s} } catch { case _: MalformedURLException => {s} } def render(uri: URI) = { val s = uri.toURL.toExternalForm baseUrl + "render?url=" + URLEncoder.encode(s, "utf-8") } def delete(uri: URI) = { val s = uri.toURL.toExternalForm baseUrl + "render?url=" + URLEncoder.encode(s, "utf-8") + "&action=delete" } def examples: Array[Elem] = Array("minimal", "collection", "item", "queries", "template", "error") map { name => val cj = baseUrl + "examples/from-spec/" + name + ".collection+json" {name} } def getStackTrace(aThrowable: Throwable) = { val result: Writer = new StringWriter() val printWriter: PrintWriter = new PrintWriter(result) aThrowable.printStackTrace(printWriter) result.toString } /* mixin get_name(link, prefix, i) - var name = typeof link.name == 'string' ? link.name : undefined - var prompt = typeof link.prompt == 'string' ? link.prompt : undefined - var prefix = typeof prefix == 'string' ? prefix + ': ' : '' |#{prefix + (name || prompt || '#' + i)} */ def getName(link: Link, prefix: String, i: Int) = { // var name = link.name val name = Json4sHelpers.getAsString(link.underlying, "name") prefix + name.orElse(link.prompt).getOrElse("#" + i) } def index = { def innerContent =

❤ Collection+JSON ❤

 
 

About

This is an interactive explorer for the Collection+JSON hypermedia. Give it an URL and it will render is as good as it can.

The purpose is twofold: it is a useful tool to inspect collections while developing or debugging an application. At the same time it's useful to show the power of hypermedia by showing how much a generic user agent can do by using only the generic Collection+JSON specification and not knowing anything about your application.

See also

There's a growing C+J community that's discussing on the Google Group.

Reading the (quite light) formal specification is useful. It also has a tutorial and some examples.

Using

Feel to use this service! However, note that it's running on a free Heroku instance so it might fail, be slow or otherwise useless.

If you want to run it against your own local servers you can either run it yourself, or use apps like localtunnel.com to make your application publicly available.

The Source Code

The source code is available here.

Examples

The Employee application is a set of resources with employees and departments. The application was made specifically for this explorer: explore now!

The specification contains a few example collections too which you can explore:

; def content =
{innerContent}
layout(content, None) } def data(url: URI, params: Map[String, Seq[String]], result: Either[Throwable, JsonCollection], res: CjResponse) = { def href(uri: URI) = { // val splits = uri.getPath.split('/') // for split in splits // a(href=urlgenerator.render(split[1]), title='Explore #{split[1]}') #{split[0]} uri.toURL.toExternalForm } def link(link: Link) = { val name = Json4sHelpers.getAsString(link.underlying, "name")
Explore Raw
href
{href(link.href)}
rel
{tryLink(link.rel)}
name
{name.getOrElse(notSet)}
prompt
{link.prompt.getOrElse(notSet)}
render
{link.render.map(_.name).getOrElse(notSet)}
{link.render match { case Some(IMAGE) =>
Image
{name.getOrElse("")}
case _ => NodeSeq.Empty }}
} def meta(implicit cj: JsonCollection) =
version
{cj.version.name}
href
{href(cj.href)}

Explore Raw Delete

{cj.links match { case Nil => NodeSeq.Empty case _ =>

Collection Links

{cj.links.zipWithIndex.map { case (l, i) => val name = Json4sHelpers.getAsString(l.underlying, "name") val title = l.prompt.orElse(name) match { case Some(t) => ": " + t case _ => "" } Group(Seq(, link(l))) }}
}}
// TODO: If the collection has prev/next links, add buttons to automaticaly navigate those. // TODO: Add ability to show the raw part of the collection. def items(cj: JsonCollection) = { def itemLinks(cj: JsonCollection) = { val first = cj.findLinkByRel("first") val prev = cj.findLinkByRel("prev") val next = cj.findLinkByRel("next") val last = cj.findLinkByRel("last") if(first.isDefined || prev.isDefined || next.isDefined || last.isDefined) {
{if(first.isDefined) First} {if(prev.isDefined) Previous} {if(next.isDefined) Next} {if(last.isDefined) Last}
} } {itemLinks(cj)} { cj.items.zipWithIndex.map { case (item, i) => val links = item.links

Item #{i + 1}

href
{href(item.href)}
{if(links.nonEmpty) {

Item Links

{links.zipWithIndex.map { case (l, i2) => Group(Seq(

Item Link #{i2 + 1}

, link(l))) }}
}}

Data

{item.data map { d => }}
{d.name}{d.value}
} } {itemLinks(cj)}
} def queries(implicit cj: JsonCollection) = { {cj.queries.zipWithIndex map { case (query, i) => val prompt = Json4sHelpers.getAsString(query.underlying, "prompt") val name = Json4sHelpers.getAsString(query.underlying, "name") val title = prompt.orElse(name).getOrElse("Unnamed query #" + (i + 1))

{title}

{query.data map { d: Property => val value = params(d.name).headOption.getOrElse(d.value.toString) }}
}} } def template(implicit cj: JsonCollection) =

The data will be submitted to {href(cj.href)}

{cj.template.get.data map { d => val value = params(d.name).headOption.getOrElse(d.value.toString) }}
/* block error div(class='row-fluid') dl dt title dd if collection.error.title | #{collection.error.title} else i Not set dt code dd if collection.error.code | #{collection.error.code} else i Not set dt message dd if collection.error.message - var lines = collection.error.message.split('\n') if lines.length > 1 for line in lines | #{line.replace(/ /g, ' ')} br else | #{collection.error.message} else i Not set */ def error(implicit cj: JsonCollection) = { val e = cj.error.get val message = e.message map { m => val lines = m.split('\n') lines.map(s => scala.xml.Text(s): NodeSeq).reduce(_ ++
++ _) }
title
{Option(e.title).filter(!_.isEmpty).getOrElse(notSet)}
code
{e.code.getOrElse(notSet)}
message
{message.getOrElse("")}
} def httpResponse = {
Request URL
{url}
{res.headers map { case (header, values) => { values map { value => }}}}
{res.code} {res.status}
{header} {tryLink(value)}
} def parsedContent(implicit cj: JsonCollection) =
{meta}
{if(cj.items.nonEmpty) {
{items(cj)}
}} {if(cj.queries.nonEmpty) {
{queries}
}} {if(cj.template.isDefined) {
{template}
}} {if(cj.error.isDefined) {
{error}
}}
def innerContent = {result match { case Left(ex) =>
{getStackTrace(ex)}
case Right(cj) => {try { parsedContent(cj) } catch { case ex: Exception =>
Unable to process model: {ex.getMessage}
}}
{JsonMethods.pretty(JsonMethods.render(cj.underlying))}
}}
{httpResponse}
def sidebar = def content =
{sidebar}
{innerContent}
layout(content, None) } def layout(content: Elem, headSnippet: Option[String]) = Collection+JSON Explorer
{content}
}