Skip to content

Instantly share code, notes, and snippets.

@jaw111
Last active August 29, 2015 14:03
Show Gist options
  • Save jaw111/46209e853f83d041a6dd to your computer and use it in GitHub Desktop.
Save jaw111/46209e853f83d041a6dd to your computer and use it in GitHub Desktop.
BreweryLD

BreweryLD

A Linked Data wrapper for the BreweryDB API

This Gist implements a prototype vocabulary and Linked Data representation for resources from the BreweryDB API.

The wrapper supports most endpoints from the BreweryDB API serving a JSON-LD response by default. The JSON-LD representation is generated from the original JSON using the index.php script. Only requests made using GET method are currently supported.

Examples:

For debugging purposes a ?json or ?debug parameter can be added to the request URI to see the original JSON object or a dump of the PHP array respectively.

To Do

  • Improve support subordinate resources like /beers/{beerId}/breweries
  • Full coverage for all possible embedded arrays and objects in all responses
  • Improved support for predefined values like serving temperature and availability
  • Extend vocabulary to cover all classes and properties from original data
  • Futher extend vocabulary using terms from the Hydra Core Vocabulary
  • Add support for more content types e.g. Turtle, RDF/XML, N-Triples
Header set Access-Control-Allow-Origin *
Options +FollowSymLinks
RewriteEngine on
RewriteRule ^vocab$ /vocab.jsonld [R=302,L]
# Serve existing files as before
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule .? - [L]
# Rewrite all other queries to index.php
# RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /index.php [L]
{
"@context": {
"@vocab": "http://breweryld.semaku.com/vocab#",
"dc": "http://purl.org/dc/terms/",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"hydra": "http://www.w3.org/ns/hydra/core#",
"data": "dc:subject",
"source": {
"@id": "dc:source",
"@type": "@id"
},
"totalItems": "hydra:totalItems",
"member": {
"@id": "hydra:member",
"@type": "@id"
},
"Resource": "hydra:Resource",
"Collection": "hydra:Collection",
"PagedCollection": "hydra:PagedCollection",
"itemsPerPage": "hydra:itemsPerPage",
"firstPage": {
"@id": "hydra:firstPage",
"@type": "@id"
},
"lastPage": {
"@id": "hydra:lastPage",
"@type": "@id"
},
"nextPage": {
"@id": "hydra:nextPage",
"@type": "@id"
},
"previousPage": {
"@id": "hydra:previousPage",
"@type": "@id"
},
"icon": {
"@type": "@id"
},
"large": {
"@type": "@id"
},
"medium": {
"@type": "@id"
},
"website": {
"@type": "@id"
},
"mailingListUrl": {
"@type": "@id"
},
"images": {
"@type": "@id"
},
"labels": {
"@type": "@id"
},
"glassware": {
"@type": "@id"
},
"style": {
"@type": "@id"
},
"available": {
"@type": "@id"
},
"beerVariation": {
"@type": "@id"
},
"socialmedia": {
"@type": "@id"
},
"srm": {
"@type": "@id"
},
"guild": {
"@type": "@id"
},
"brewery": {
"@type": "@id"
},
"category": {
"@type": "@id"
},
"createDate": {
"@type": "xsd:dateTime"
},
"updateDate": {
"@type": "xsd:dateTime"
},
"year": {
"@type": "xsd:gYear"
}
}
}
<?php
$contextUrl = 'http://breweryld.semaku.com/context.jsonld';
$vocabUrl = 'http://breweryld.semaku.com/vocab';
$key = 'YOUR_API_KEY';
$baseUrl = 'https://api.brewerydb.com/v2';
$acceptTypes = getAcceptTypes();
$test=true;
//print_r(array_keys($acceptTypes));
if (isset($_GET['json'])) {
$contentType = 'application/json';
$test=false;
} elseif (isset($_GET['debug'])) {
$contentType = 'text/php';
} else {
$contentType = 'application/ld+json';
header('Link: <' . $vocabUrl . '>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"');
header('Access-Control-Expose-Headers: Link');
}
header('Content-Type: ' . $contentType);
$request = strtok($_SERVER['REQUEST_URI'], "?");
$params = modifyParam("format","json");
$requestUrl = $baseUrl . $request . $params;
//echo $requestUrl;
$json = get_content($requestUrl."&key=".$key);
$data = json_decode($json, TRUE);
if ($test) {
// recursively walk through the data and ducktype any dateTime and other values
array_walk_recursive($data,"ducktype");
$data["@type"] = "Resource";
// if data contains an array of objects, make it a hydra:Collection with hydra:member links
if (array_key_exists(0,$data["data"])) {
$data["@type"] = "Collection";
$data["member"] = $data["data"];
unset($data["data"]);
switch ($request) {
case "/adjuncts":
$type='Adjunct';
break;
case "/beers":
$type='Beer';
break;
case "/breweries":
$type='Brewery';
break;
case "/categories":
$type='Category';
break;
case "/changes":
$type='Change';
break;
case "/events":
$type='Event';
break;
case "/features":
$type='Feature';
break;
case "/fermentables":
$type='Fermentable';
break;
case "/fluidsizes":
$type='Fluidsize';
break;
case "/glassware":
$type='Glass';
break;
case "/guilds":
$type='Guild';
break;
case "/hops":
$type='Hop';
break;
case "/ingredients":
$type='Ingredient';
break;
case "/locations":
$type='Location';
break;
case "/socialsites":
$type='Socialsite';
break;
case "/styles":
$type='Style';
break;
case "/yeasts":
$type='Yeast';
break;
default:
$type=NULL;
}
foreach($data["member"] as &$item){
cleanseItem($item, $type);
}
}
// if data is from a paginated list (has numberOfPages key), make it a hydra:PagedCollection with links to first, last, next and previous pages
if (array_key_exists("numberOfPages",$data)) {
$data["@type"] = "PagedCollection";
$data["totalItems"] = intval($data["totalResults"]);
$data["itemsPerPage"] = 50;
$data["firstPage"] = modifyParam("p",1);
$data["lastPage"] = modifyParam("p",$data["numberOfPages"]);
if ( $data["currentPage"] !== $data["numberOfPages"] ) {
$data["nextPage"] = modifyParam("p",$data["currentPage"] + 1);
}
if ( $data["currentPage"] !== "1" ) {
$data["previousPage"] = modifyParam("p",$data["currentPage"] - 1);
}
}
if (array_key_exists("id",$data["data"])) {
cleanseItem($data["data"], ucfirst(strtok($request, "/")));
}
$data["@context"] = $contextUrl;
$data["@id"] = "";
$data["source"] = $requestUrl;
}
if ( $contentType === 'text/php') {
var_dump($data);
} else {
$response = json_encode($data);
echo $response;
}
function cleanseItem(&$item, $type) {
//global $request;
if (array_key_exists("id",$item) && $type !== NULL) {
if ($type === 'Featured') {
$item["@id"] = "/feature/".$item["year"]."-".str_pad($item["week"], 2, "0", STR_PAD_LEFT)."#id";
} else {
$item["@id"] = "/".strtolower($type)."/".$item["id"]."#id";
}
}
if ($type !== NULL) {
$item["@type"] = $type;
}
if (array_key_exists("glass",$item)) {
$item["glass"]["@id"] = "/glass/".$item["glass"]["id"]."#id";
$item["glass"]["@type"] = "Glass";
}
if (array_key_exists("srm",$item)) {
$item["srm"]["@id"] = "/srm/".$item["srm"]["id"]."#id";
$item["srm"]["@type"] = "SRM";
}
if (array_key_exists("available",$item)) {
$item["available"]["@id"] = "/available/".$item["available"]["id"]."#id";
$item["available"]["@type"] = "Availability";
}
if (array_key_exists("labels",$item)) {
$item["labels"]["@id"] = "/".strtolower($type)."/".$item["id"]."#label";
$item["labels"]["@type"] = "LabelOrLogo";
}
if (array_key_exists("images",$item)) {
$item["images"]["@id"] = "/".strtolower($type)."/".$item["id"]."#logo";
$item["images"]["@type"] = "LabelOrLogo";
}
if (array_key_exists("style",$item)) {
$item["style"]["@id"] = "/style/".$item["style"]["id"]."#id";
$item["style"]["@type"] = "Style";
}
if (array_key_exists("category",$item["style"])) {
$item["style"]["category"]["@id"] = "/category/".$item["style"]["category"]["id"]."#id";
$item["style"]["category"]["@type"] = "Category";
}
if (array_key_exists("category",$item)) {
$item["category"]["@id"] = "/category/".$item["category"]["id"]."#id";
$item["category"]["@type"] = "Category";
}
if (array_key_exists("beerVariation",$item)) {
cleanseItem($item["beerVariation"], "Beer");
}
if (array_key_exists("breweries",$item)) {
foreach($item["breweries"] as &$brewery){
$brewery["@id"] = "/brewery/".$brewery["id"]."#id";
$brewery["@type"] = "Brewery";
if (array_key_exists("locations",$brewery)) {
foreach($brewery["locations"] as &$location){
$location["@id"] = "/location/".$location["id"]."#id";
$location["@type"] = "Location";
$location["country"]["@id"] = "/country/".$location["country"]["isoCode"]."#id";
$location["country"]["@type"] = "Country";
}
}
}
}
}
function ducktype(&$v,$key) {
if (is_numeric($v)) {
$v = (string)$v;
}
$regex = '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/';
if (preg_match($regex, $v)) {
$v = str_replace(' ','T',$v)."Z";
}
}
function get_content($URL){
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, $URL);
$data = curl_exec($ch);
curl_close($ch);
return $data;
}
function getParams(){
if(!empty($_SERVER['QUERY_STRING'])){
return ltrim($_SERVER['QUERY_STRING'],'?');
} else {
return '';
}
}
function getParamsArray(){
$param_str = getParams();
$params = explode("&",$param_str);
$param_array = array();
foreach($params as $param){
$parts = explode ("=",$param);
if(count($parts)>1){
$param_array[$parts[0]]=strval($parts[1]);
} else {
$param_array[$parts[0]]= NULL ;
}
}
return $param_array;
}
function modifyParam($p,$v) {
$param_array = getParamsArray();
$param_array[$p] = $v;
$params = array();
while (list($key, $val) = each($param_array)){
if ($val !== NULL) {
$params[] = $key."=".$val;
} else {
$params[] = $key;
}
}
return "?".implode('&',$params);
}
function getAcceptHeader(){
if(isset($_SERVER['HTTP_ACCEPT'])) return trim($_SERVER['HTTP_ACCEPT']);
else return null;
}
function getAcceptTypes(){
$header = getAcceptHeader();
$mimes = explode(',',$header);
$accept_mimetypes = array();
foreach($mimes as $mime){
$mime = trim($mime);
$parts = explode(';q=', $mime);
if(count($parts)>1){
$accept_mimetypes[$parts[0]]=strval($parts[1]);
}
else {
$accept_mimetypes[$mime]=1;
}
}
arsort($accept_mimetypes);
return $accept_mimetypes;
}
?>
{
"@context": {
"brew": "http://breweryld.semaku.com/vocab#",
"dc": "http://purl.org/dc/terms/",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"hydra": "http://www.w3.org/ns/hydra/core#",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"owl": "http://www.w3.org/2002/07/owl#",
"rdfs:range": { "@type": "@id" },
"rdfs:domain": { "@type": "@id" },
"hydra:supportedClass": { "@type": "@id" },
"hydra:supportedOperation": { "@type": "@id" },
"hydra:supportedProperty": { "@type": "@id" },
"hydra:returns": { "@type": "@id" },
"defines": {
"@reverse": "rdfs:isDefinedBy"
}
},
"@id": "http://breweryld.semaku.com/vocab",
"@type": [
"owl:Ontology",
"hydra:ApiDocumentation"
],
"hydra:title": "The BreweryLD Vocabulary",
"hydra:description": "A prototype web vocabulary for the BreweryDB API. Currently for demonstration purposes only.",
"hydra:supportedClass": [
"brew:Beer"
],
"defines": [
{
"@id": "brew:Beer",
"@type": "hydra:Class",
"rdfs:label": "Beer",
"rdfs:comment": "Beer is an alcoholic beverage produced by the saccharification of starch and fermentation of the resulting sugar.",
"dc:description": "A set of beers of which each can be described with the same group of properties.",
"hydra:supportedOperation": [
{
"@id": "_:beer_get",
"@type": "hydra:Operation",
"hydra:method": "GET",
"rdfs:label": "Retrieves a Beer entity",
"hydra:returns": "brew:Beer"
}
]
},
{
"@id": "brew:id",
"@type": "rdf:Property",
"rdfs:label": "id",
"rdfs:comment": "The unique id of the resource."
},
{
"@id": "brew:name",
"@type": "rdf:Property",
"rdfs:label": "name",
"rdfs:comment": "The name of the resource."
},
{
"@id": "brew:description",
"@type": "rdf:Property",
"rdfs:label": "description",
"rdfs:comment": "The description of the resource."
},
{
"@id": "brew:foodPairings",
"@type": "rdf:Property",
"rdfs:label": "food pairings",
"rdfs:comment": "A free text field containing any food pairing information for the beer.",
"rdfs:domain": "brew:Beer"
},
{
"@id": "brew:originalGravity",
"@type": "rdf:Property",
"rdfs:label": "original gravity",
"rdfs:comment": "The original gravity of the beer.",
"rdfs:domain": "brew:Beer"
},
{
"@id": "brew:abv",
"@type": "rdf:Property",
"rdfs:label": "alcohol by volume",
"rdfs:comment": "The ABV (alcohol by volume) of the beer (expressed as a percentage).",
"rdfs:domain": "brew:Beer"
},
{
"@id": "brew:ibu",
"@type": "rdf:Property",
"rdfs:label": "international bittering unit",
"rdfs:comment": "The IBU (international bittering unit) value is a measure of how bitter a beer is. The higher the number, the more bitter the beer.",
"rdfs:domain": "brew:Beer"
},
{
"@id": "brew:glasswareId",
"@type": "rdf:Property",
"rdfs:label": "glassware id",
"rdfs:comment": "The id corresponding to the glass object that is assigned to this beer. If this exists, then so will the glass object. See the glass object for more information.",
"rdfs:domain": "brew:Beer"
},
{
"@id": "brew:glass",
"@type": "hydra:Link",
"rdfs:label": "glass",
"rdfs:comment": "Contains the details about the assigned glass (id, name). The name of the glass is the type of glass in which the beer is best served.",
"rdfs:domain": "brew:Beer",
"rdfs:range": "brew:Glass",
"hydra:supportedOperation": [
{
"@id": "_:glass_get",
"@type": "hydra:Operation",
"hydra:method": "GET",
"rdfs:label": "Retrieves a Glass entity",
"hydra:returns": "brew:Glass"
}
]
},
{
"@id": "brew:styleId",
"@type": "rdf:Property",
"rdfs:label": "style id",
"rdfs:comment": "The id corresponding to the style object that is assigned to the beer. See the style object for more information.",
"rdfs:domain": "brew:Beer"
},
{
"@id": "brew:style",
"@type": "hydra:Link",
"rdfs:label": "style",
"rdfs:comment": "The style object contains details about the assigned style: id, categoryId, category (id, name), name, description, ibuMin, ibuMax, abvMax, srmMin, srmMax, ogMin, fgMin, fgMax).",
"rdfs:domain": "brew:Beer",
"rdfs:range": "brew:Style",
"hydra:supportedOperation": [
{
"@id": "_:style_get",
"@type": "hydra:Operation",
"hydra:method": "GET",
"rdfs:label": "Retrieves a Style entity",
"hydra:returns": "brew:Style"
}
]
},
{
"@id": "brew:isOrganic",
"@type": "rdf:Property",
"rdfs:label": "is organic",
"rdfs:comment": "Whether or not the beer is certified organic.",
"rdfs:domain": "brew:Beer"
},
{
"@id": "brew:labels",
"@type": "rdf:Property",
"rdfs:label": "labels",
"rdfs:comment": "If this object is set then labels exist and it will contain items icon, medium, and large that are URLs to the images.",
"rdfs:domain": "brew:Beer",
"rdfs:range": "brew:LabelOrLogo"
},
{
"@id": "brew:LabelOrLogo",
"@type": "rdfs:Class",
"rdfs:label": "Label or logo",
"rdfs:comment": "The label or logo of a beer or brewery.",
"dc:description": "A set of labels and logos of which each can be described with the same group of properties."
},
{
"@id": "brew:icon",
"@type": "hydra:Link",
"rdfs:label": "icon",
"rdfs:comment": "A link to the low-res (icon) image of the label or logo.",
"rdfs:domain": "brew:LabelOrLogo",
"rdfs:range": "hydra:Resource"
},
{
"@id": "brew:medium",
"@type": "hydra:Link",
"rdfs:label": "medium",
"rdfs:comment": "A link to the medium-res image of the label or logo.",
"rdfs:domain": "brew:LabelOrLogo",
"rdfs:range": "hydra:Resource"
},
{
"@id": "brew:large",
"@type": "hydra:Link",
"rdfs:label": "large",
"rdfs:comment": "A link to the high-res image of the label or logo.",
"rdfs:domain": "brew:LabelOrLogo",
"rdfs:range": "hydra:Resource"
},
{
"@id": "brew:servingTemperature",
"@type": "rdf:Property",
"rdfs:label": "serving temperature",
"rdfs:comment": "The key that corresponds to the serving temperature information. See the servingTemperatureDisplay field for the full details.",
"rdfs:domain": "brew:Beer"
},
{
"@id": "brew:servingTemperatureDisplay",
"@type": "rdf:Property",
"rdfs:label": "serving temperature display",
"rdfs:comment": "The serving temperature information. If the servingTemperature was \"cool\" then this field would be like \"Cool - (8-12C/45-54F)\".",
"rdfs:domain": "brew:Beer"
},
{
"@id": "brew:status",
"@type": "rdf:Property",
"rdfs:label": "status",
"rdfs:comment": "The status key for the object."
},
{
"@id": "brew:statusDisplay",
"@type": "rdf:Property",
"rdfs:label": "status display",
"rdfs:comment": "The display string corresponding to the status."
},
{
"@id": "brew:availableId",
"@type": "rdf:Property",
"rdfs:label": "available id",
"rdfs:comment": "The id corresponding to the assigned availability object.",
"rdfs:domain": "brew:Beer"
},
{
"@id": "brew:available",
"@type": "rdf:Property",
"rdfs:label": "available",
"rdfs:comment": "The object that provides details for the assigned availability. It contain: id, name, and description.",
"rdfs:domain": "brew:Beer",
"rdfs:range": "brew:Availability"
},
{
"@id": "brew:Availability",
"@type": "rdfs:Class",
"rdfs:label": "Availability",
"rdfs:comment": "The availability state of a beer.",
"dc:description": "A set of availability states of which each can be described with the same group of properties."
},
{
"@id": "brew:beerVariationId",
"@type": "rdf:Property",
"rdfs:label": "beer variation id",
"rdfs:comment": "If this beer is a variation of another beer, then the beerVariationId will be set to the id of the \"source\" beer. For example, if \"My IPA\" has id abc123 and you look up \"Oak Aged My IPA\" (id def456) then the beerVariationId of \"Oak Aged My IPA\" would be abc123.",
"rdfs:domain": "brew:Beer"
},
{
"@id": "brew:beerVariation",
"@type": "rdf:Property",
"rdfs:label": "beer variation",
"rdfs:comment": "If this object will be set if there is a beerVariationId set. This is an instance of a beer object that has all the information for the \"source\" beer if the beer is a variation.",
"rdfs:domain": "brew:Beer",
"rdfs:range": "brew:Beer",
"hydra:supportedOperation": [
"_:beer_get"
]
},
{
"@id": "brew:year",
"@type": "rdf:Property",
"rdfs:label": "year",
"rdfs:comment": "The year field is for vintages of a beer.",
"rdfs:domain": "brew:Beer",
"rdfs:range": "xsd:gYear"
},
{
"@id": "brew:createDate",
"@type": "rdf:Property",
"rdfs:label": "create date",
"rdfs:comment": "Date of creation of the resource.",
"rdfs:range": "xsd:dateTime"
},
{
"@id": "brew:updateDate",
"@type": "rdf:Property",
"rdfs:label": "update date",
"rdfs:comment": "Date on which the resource was changed.",
"rdfs:range": "xsd:dateTime"
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment