Abstractions

In the Skills SDK, an abstraction is used to represent a concept in programming such as APIs, endpoints, requests, or forms. These concepts exist independent of any programming language or library and are discussed everyday by programmers.

Because Optic can be used in many different programming environments, it's useful to have shared, and explicit notions of the most common abstractions. This is because, similar to an interface/trait in OOP, an abstraction can be implemented by any Optic generator. As an example, let's imagine a Python skill for Flask (a popular API framework) that implements the common API abstractions in optic:rest. Whenever Optic sees a Flask endpoint in your code, it parses it as an object with shape {method, url, parameters[], headers[], responses[]}. There's a relationship already defined from optic:rest -> optic:http-request, so without any configuration, your Flask skill can inter-operate with all the generators that implement optic:http-request. Instead of creating a new relationship for each of the HTTP request libraries, you only need to write it once.

Using abstractions dramatically cuts down on the number of relationships that need to be defined across the Optic ecosystem by allowing Optic to infer them itself.

When to Create Your own Abstraction

Because using common abstractions across different skills benefits the entire ecosystem, your standard for making a new abstraction should be fairly high. You should only create a new abstraction if:

  1. An existing standard abstraction does not exist already
  2. You only want to use it internally (ie multiple generators sharing a private abstraction)

Defining a new abstraction

abstractions can be created by calling Abstraction in the SDK.

The first argument is the id for the abstraction. You can reference this abstraction internally with just its id example. To reference the abstraction from another package just prefix with the package your-username:package-name/{id}.

The second argument is the JSON Schema for the abstraction. If you are not familiar with JSON Schemas, you can sandbox them here. Currently we support Draft-4 with a few caveats: 1) we don't support internal/external refs and 2) the root type must always be an object.

This example abstraction is used to enforce a common structure among HTTP Parameters across Optic. Its shape looks like this {method: Enum["query", "body", "params", "header"], name: String}.

import {Abstraction} from "optic-skills-sdk";

export const parameterAabstraction = Abstraction('parameter', {
	"title": "Parameter",
	"type": "object",
	"required": ["in", "name"],
	"properties": {
		"in": {
			"type": "string",
			"enum": ["query", "body", "params", "header"]
		},
		"name": {
			"type": "string"
		}
	}
})

Nesting Abstraction Schemas

Currently we don't support JSON Schema refs so the best way to nest abstractions is copying them at runtime. This is done by setting a field to otherAbstraction.definition. Nesting an abstraction from another package is not currently supported.

export const parametersAbstraction = abstraction('parameter', {
	"title": "Parameter",
	"type": "object",
	"required": ["in", "name"],
	"properties": {
		"in": {
			"type": "string",
			"enum": ["query", "body", "params", "header"]
		},
		"name": {
			"type": "string"
		}
	}
})

export const routeAbstraction = abstraction('route', {
	"title": "Route",
	"type": "object",
	"required": ["method", "url"],
	"properties": {
		"method": {
			"type": "string",
			"enum": ["get", "post", "put", "delete", "head", "options"]
		},
		"url": {
			"type": "string"
		},
		"headers": {
			"type": "array",
			"items": headerAbstraction.definition,
		},
		"parameters": {
			"type": "array",
			"items": parametersAbstraction.definition,
		},
		"responses": {
			"type": "array",
			"items": responsesAbstraction.definition,
		}
	},
	"order": ["url", "method", "parameters", "headers", "responses"]
})

Testing

The Skills SDK makes it easy to write simple unit tests for your abstractions. To test a generator, first make sure it's registered withing the skill:

export default Skill('optic', 'testing', '0.4.0', {
	abstractions: [parameterAbstraction]
})

Then get the test kit for the skill and the generator you want to test:

const skillTestKit = SkillTestKit(exampleSkill)
const parameterAbstractionTestKit = skillTestKit.testAbstraction('express-abstraction')
describe('parameters abstraction', () => {
	it('matches valid parameter model', () => {
		assert(parameterAbstractionTestKit.test({in: 'query', name: 'field'}).isMatch)
	})

	it('does not match invalid parameter model', () => {
		assert(!parameterAbstractionTestKit.test({in: 'NOT_REAL', name: 'field'}).isMatch)
	})
})

Assuming your skill project was setup using opticsdk init, you can run npm run test to evaluate all the test suites in your project.

Next Steps