From aa324fd4345b7f9077a23805cc038b436007e658 Mon Sep 17 00:00:00 2001 From: Marc Wrobel Date: Sat, 17 Dec 2022 13:23:05 +0100 Subject: [PATCH] Add v1 API (#2062, #2066, #759, #394) Compared the the old v0 API : - v1 API is now generated using a Jekyll Generator (see https://jekyllrb.com/docs/plugins/generators/). - JSON files has been moved to the v1 subdirectory (#2066). This makes it easier to implement non-backward-compatible changes in API. - The all.json, that returned a simple array of product names in v0, now returns a JSON document in v1. The array of products can be found under the 'products' key. This enables us to add endoflife.date-level data (such as the number of products). - The "products" array in all.json now also contains JSON document instead of simple strings. This will make it easier in the future to expose new data (such as tags for products). - Product level information are now exposed in the API (#2062) at the "product" endpoint. Instead of a JSON array in v0, v1 returns a JSON document. The release cycles array can be found under the 'releases' key. - Cycles data now always contains all the release cycles key, even if they are null. - Individual cycles JSON files has been removed (we thought it was not used/useful enough to be kept). --- _layouts/v1_all_products.json | 12 ++++ _layouts/v1_product.json | 25 +++++++ _plugins/generate-api-v1.rb | 119 ++++++++++++++-------------------- 3 files changed, 86 insertions(+), 70 deletions(-) create mode 100644 _layouts/v1_all_products.json create mode 100644 _layouts/v1_product.json diff --git a/_layouts/v1_all_products.json b/_layouts/v1_all_products.json new file mode 100644 index 000000000000..91eb36313ff3 --- /dev/null +++ b/_layouts/v1_all_products.json @@ -0,0 +1,12 @@ +{ + "total": "{{ page.products | size }}", + "products": [ +{% for p in page.products %} + { + "name": "{{ p.id }}", + "category": "{{ p.category }}", + "permalink": "{{ site.url }}/{{ p.id }}" + }{% unless forloop.last %},{% endunless -%} +{%- endfor %} + ] +} diff --git a/_layouts/v1_product.json b/_layouts/v1_product.json new file mode 100644 index 000000000000..b51ed7400a29 --- /dev/null +++ b/_layouts/v1_product.json @@ -0,0 +1,25 @@ +{ + "title": "{{ page.title }}", + "category": "{{ page.category }}", + "iconSlug": "{{ page.iconSlug | default: page.id }}", + "permalink": "{{ site.url }}/{{ p.id }}", + "versionCommand": "{{ page.versionCommand }}", + "releasePolicyLink": "{{ page.releasePolicyLink }}", + "auto": {% if page.auto %}true{% else %}false{% endif %}, + "releases": [ +{% for r in page.releases %} + { + "cycle": "{{ r.releaseCycle }}", + "codename": {% if r.codename %}"{{ r.codename }}"{% else %}null{% endif %}, + "releaseDate": "{{ r.releaseDate }}", + "support": {% if r.support %}"{{ r.support }}"{% else %}null{% endif %}, + "eol": {% if r.eol %}"{{ r.eol }}"{% else %}null{% endif %}, + "latest": {% if r.latest %}"{{ r.latest }}"{% else %}null{% endif %}, + "latestReleaseDate": {% if r.latestReleaseDate %}"{{ r.latestReleaseDate }}"{% else %}null{% endif %}, + "lts": {% if r.lts %}{{ r.lts }}{% else %}false{% endif %}, + "discontinued": {% if r.diconstinued %}{{ r.discontinued }}{% else %}false{% endif %}, + "link": {% if r.link %}"{{ r.link }}"{% else %}null{% endif %} + }{% unless forloop.last %},{% endunless -%} +{%- endfor %} + ] +} diff --git a/_plugins/generate-api-v1.rb b/_plugins/generate-api-v1.rb index 2f93923af10f..5dacafc0090b 100755 --- a/_plugins/generate-api-v1.rb +++ b/_plugins/generate-api-v1.rb @@ -1,89 +1,68 @@ -#!/usr/bin/env ruby - # This script creates API files for version 1 of the endoflife.date API. # -# There are three kind of generated files : -# - all.json: contains the list of all the product names. -# - .json: contains a given product data ()including releases data). -# - /.json: contains a given product release data. +# There are two kind of generated files : +# - all.json: contains endoflife metadata, such as the list of all the products and the product count. +# - .json: contains a given product data (including releases data). -require 'fileutils' -require 'json' -require 'yaml' -require 'date' +require 'jekyll' module ApiV1 - # This API path - DIR = 'api/v1' + class ApiGenerator < Jekyll::Generator + safe true + priority :lowest - # Returns the path of a file inside the API namespace. - def self.file(name, *args) - File.join(DIR, name, *args) - end + def generate(site) + all_products = [] - # Holds information about a product. - Product = Class.new do - attr_accessor :data + site.pages.each do |page| + is_markdown = (page.name.end_with?("md")) + has_permalink = (page.data.has_key? 'permalink') + has_releases = (page.data.has_key? 'releases') + is_product_page = (is_markdown and has_permalink and has_releases) - # Initializes the product with the given product's markdown file. - # The markdown file is expected to contain a YAML front-matter with the appropriate properties. - # - # Copying the data makes it easier to process it. - def initialize(data) - @data = Hash.new - # The product name is derived from the product's permalink (ex. /debian => debian). - @data["name"] = data['permalink'][1..data['permalink'].length] - @data["title"] = data['title'] - @data["category"] = data['category'] - @data["iconSlug"] = data['iconSlug'] - @data["permalink"] = data['permalink'] - @data["versionCommand"] = data['versionCommand'] - @data["auto"] = data.has_key? 'auto' - @data["releasePolicyLink"] = data['releasePolicyLink'] - @data["releases"] = data['releases'].map do |release| - release_data = Hash.new - release_data["name"] = release['releaseCycle'] - release_data["codename"] = release['codename'] - release_data["releaseDate"] = release['releaseDate'] - release_data["support"] = release['support'] - release_data["eol"] = release['eol'] - release_data["discontinued"] = release['discontinued'] - release_data["lts"] = release['lts'] || false # lts is optional, make sure it always has a value - release_data["latest"] = release['latest'] - release_data["latestReleaseDate"] = release['latestReleaseDate'] - release_data + if is_product_page + product_page = ProductJson.new(site, page) + site.pages << product_page + all_products << product_page + end end - end - def name - data["name"] + site.pages << AllProductsJson.new(site, all_products) end end -end -product_names = [] -FileUtils.mkdir_p(ApiV1::file('.')) + class ProductJson < Jekyll::Page + def initialize(site, product) + id = product.data['permalink'][1..product.data['permalink'].length] -Dir['products/*.md'].each do |file| - # Load and prepare data - raw_data = YAML.safe_load(File.open(file), permitted_classes: [Date]) - product = ApiV1::Product.new(raw_data) - product_names.append(product.name) + @site = site + @base = site.source + @dir = 'api/v1' + @name = "#{id}.json" - # Write /.json - product_file = ApiV1::file("#{product.name}.json") - File.open(product_file, 'w') { |f| f.puts product.data.to_json } + @data = {}.merge(product.data) + @data.delete('permalink') # path already configured above, must not be overridden + @data.delete('alternate_urls') # no need for API + @data['layout'] = 'v1_product' + @data['id'] = id # name is already a page attribute - # Write all //.json - FileUtils.mkdir_p(ApiV1::file(product.name)) - product.data["releases"].each do |release| - # Any / characters in the name are replaced with - to avoid file errors. - release_file = ApiV1::file(product.name, "#{release['name'].to_s.tr('/', '-')}.json") - File.open(release_file, 'w') { |f| f.puts release.to_json } + self.process(@name) + end end -end -# Write /all.json -all_products_file = ApiV1::file('all.json') -File.open(all_products_file, 'w') { |f| f.puts product_names.sort.to_json } + class AllProductsJson < Jekyll::Page + def initialize(site, all_products) + @site = site + @base = site.source + @dir = 'api/v1' + @name = "all.json" + + @data = {} + @data['layout'] = 'v1_all_products' + @data['products'] = all_products ? all_products : [] + + self.process(@name) + end + end +end