Browse Source

tool,spec_templates,specs: Stop generating template specs

pull/439/head
jld3103 1 year ago
parent
commit
bfa7836f5b
No known key found for this signature in database
GPG Key ID: 9062417B9E8EB7B3
  1. 10
      packages/spec_templates/.gitignore
  2. 1
      packages/spec_templates/LICENSE
  3. 1
      packages/spec_templates/analysis_options.yaml
  4. 500
      packages/spec_templates/bin/generate.dart
  5. 93
      packages/spec_templates/lib/method_parameter.dart
  6. 253
      packages/spec_templates/lib/openapi_spec.dart
  7. 16
      packages/spec_templates/pubspec.yaml
  8. 9
      specs/templates/appinfo_core.xml
  9. 2882
      specs/templates/core.json
  10. 2657
      specs/templates/news.json
  11. 821
      specs/templates/notes.json
  12. 401
      specs/templates/notifications.json
  13. 1522
      specs/templates/provisioning_api.json
  14. 316
      specs/templates/user_status.json
  15. 125
      tool/generate-specs.sh

10
packages/spec_templates/.gitignore vendored

@ -1,10 +0,0 @@
# Files and directories created by pub.
.dart_tool/
.packages
# Conventional directory for build outputs.
build/
# Omit committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock

1
packages/spec_templates/LICENSE

@ -1 +0,0 @@
../../LICENSE

1
packages/spec_templates/analysis_options.yaml

@ -1 +0,0 @@
include: package:nit_picking/dart.yaml

500
packages/spec_templates/bin/generate.dart

@ -1,500 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:spec_templates/method_parameter.dart';
import 'package:spec_templates/openapi_spec.dart';
import 'package:xml/xml.dart';
Future main(final List<String> args) async {
final tmpDirectory = Directory(p.join(Directory.systemTemp.path, 'nextcloud-neon'));
if (!tmpDirectory.existsSync()) {
tmpDirectory.createSync();
}
final path = args[0];
final isCore = args[1] == 'true';
final appDirectory = Directory(p.absolute(p.normalize(path)));
if (!appDirectory.existsSync()) {
throw Exception('App directory $appDirectory not found');
}
late String infoXmlPath;
if (isCore) {
infoXmlPath = p.join(
'specs',
'templates',
'appinfo_core.xml',
);
} else {
infoXmlPath = p.join(
appDirectory.path,
'appinfo',
'info.xml',
);
}
final document = XmlDocument.parse(File(infoXmlPath).readAsStringSync());
final info = document.findElements('info').toList().single;
final id = info.findElements('id').toList().single.innerText;
final name = info.findElements('name').toList().single.innerText;
final summary = info.findElements('summary').toList().single.innerText;
final version = info.findElements('version').toList().single.innerText;
final license = info.findElements('licence').toList().single.innerText;
late String routesPhpPath;
if (isCore) {
routesPhpPath = p.join(
appDirectory.path,
'routes.php',
);
} else {
routesPhpPath = p.join(
appDirectory.path,
'appinfo',
'routes.php',
);
}
final routes = await _parseRoutesFile(tmpDirectory, routesPhpPath);
final paths = <String, Path>{};
final hasRoutes = routes.keys.contains('routes');
final hasOCS = routes.keys.contains('ocs');
if (!hasRoutes && !hasOCS) {
throw Exception('One of ocs and routes is required, but only found: "${routes.keys.join('", "')}"');
}
final routesBasePath = '${isCore ? '' : '/apps'}/$id';
final ocsBasePath = '/ocs/v2.php$routesBasePath';
for (final k in routes.keys) {
for (final route in routes[k]!) {
final name = route['name'] as String;
var url = route['url'] as String;
// ignore: avoid_dynamic_calls
final requirements = route['requirements']?.cast<String, String>() as Map<String, String>?;
if (!url.startsWith('/')) {
url = '/$url';
}
if (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
}
if (k == 'routes') {
url = '$routesBasePath$url';
} else if (k == 'ocs') {
url = '$ocsBasePath$url';
}
final verb = route['verb'] as String? ?? 'GET';
if (name.startsWith('page#') || name.startsWith('admin#')) {
continue;
}
if (verb == 'GET' && url == '/') {
continue;
}
final methodName = _getMethodName(name.split('#')[1]);
final controllerName = _getControllerName(name.split('#')[0]);
late String controllerFilePath;
if (isCore) {
controllerFilePath = p.join(
appDirectory.path,
'Controller',
'$controllerName.php',
);
} else {
controllerFilePath = p.join(
appDirectory.path,
'lib',
'Controller',
'$controllerName.php',
);
}
final controllerContent = File(controllerFilePath).readAsStringSync().replaceAll('\n', '');
if (methodName == 'preflightedCors') {
continue;
}
final reg =
RegExp('\\/\\*\\*((?:(?!\\/\\*\\*).)*?)\\*\\/(?:(?!\\*\\/).)*?public function $methodName\\(([^\\)]*)\\)');
final match = reg.allMatches(controllerContent).single;
final docParameters = <String>[];
var current = '';
for (final docLine in match
.group(1)!
.split('*')
.map((final s) {
var r = s.trim();
while (r.contains(' ')) {
r = r.replaceAll(' ', ' ');
}
return r;
})
.where((final s) => s.isNotEmpty)
.toList()) {
if (docLine.startsWith('@')) {
if (current != '') {
docParameters.add(current);
}
}
if (docLine.startsWith('@return')) {
current = '';
break;
}
if (docLine.startsWith('@param')) {
current = docLine;
} else if (current != '') {
current += ' $docLine';
}
}
if (current != '') {
docParameters.add(current);
}
final methodParameters = _getMethodParameters(
controllerName,
methodName,
match.group(2)!.split(',').map((final s) => s.trim()).where((final s) => s.isNotEmpty).toList(),
docParameters,
);
final parameterNames = RegExp('{[^}]*}').allMatches(url).map((final m) {
final t = m.group(0)!;
return t.substring(1, t.length - 1);
}).toList();
final parameters = <Parameter>[];
for (final parameterName in parameterNames) {
MethodParameter? parameter;
for (final methodParameter in methodParameters) {
if (methodParameter.name == parameterName) {
parameter = methodParameter;
break;
}
}
if (parameter == null && (requirements == null || requirements[parameterName] == null)) {
throw Exception('Could not find parameter for $parameterName in $name');
}
parameters.add(
Parameter(
name: parameterName,
in_: 'path',
required: true,
description: parameter?.description,
schema: {
'type': parameter?.openAPIType ?? 'TODO',
if (parameter?.defaultValue != null) ...{
'default': parameter?.defaultValue,
},
},
),
);
}
final queryParameters = <MethodParameter>[];
for (final methodParameter in methodParameters) {
var found = false;
for (final parameter in parameters) {
if (parameter.name == methodParameter.name) {
found = true;
break;
}
}
if (!found) {
queryParameters.add(methodParameter);
}
}
if (paths[url] == null) {
paths[url] = Path(
parameters: parameters,
);
}
final operation = Operation(
operationID: '${name.replaceAll('#', '-').toLowerCase()}-TODO',
tags: [id],
parameters: queryParameters.isNotEmpty
? queryParameters
.map<Parameter>(
(final queryParameter) => Parameter(
name: queryParameter.name,
in_: 'query',
description: queryParameter.description,
required: !queryParameter.nullable && queryParameter.defaultValue == null,
schema: queryParameter.openAPIType == 'boolean'
? {
// This is a quirk in Nextcloud where sending literal booleans in query parameters doesn't work and only integers work.
// See https://github.com/nextcloud/server/issues/34226
'type': 'integer',
if (queryParameter.defaultValue != null) ...{
'default': queryParameter.defaultValue == 'true' ? 1 : 0,
},
}
: {
'type': queryParameter.openAPIType ?? 'TODO',
if (queryParameter.defaultValue != null) ...{
'default': queryParameter.defaultValue,
},
},
),
)
.toList()
: null,
responses: {
200: Response(
description: '',
content: {
'application/json': MediaType(
schema: {
'type': 'string',
},
),
},
),
},
);
switch (verb) {
case 'DELETE':
paths[url]!.delete = operation;
break;
case 'GET':
paths[url]!.get = operation;
break;
case 'POST':
paths[url]!.post = operation;
break;
case 'PUT':
paths[url]!.put = operation;
break;
case 'PATCH':
paths[url]!.patch = operation;
break;
case 'OPTIONS':
paths[url]!.options = operation;
break;
default:
throw Exception('Unsupported verb: $verb');
}
}
}
late String spdxIdentifier;
switch (license) {
case 'agpl':
spdxIdentifier = 'AGPL-3.0';
break;
default:
throw Exception('Can not convert license name "$license" to a SPDX identifier');
}
File(
p.join(
'specs',
'templates',
'$id.json',
),
).writeAsStringSync(
const JsonEncoder.withIndent(' ').convert(
Spec(
version: '3.1.0',
info: Info(
title: name,
version: version,
description: summary,
license: License(
name: license,
identifier: spdxIdentifier,
),
),
paths: paths,
).toMap(),
),
);
}
String _getControllerName(final String name) {
final result = StringBuffer();
final parts = name.split('');
for (var i = 0; i < parts.length; i++) {
var char = parts[i];
final prevChar = i > 0 ? parts[i - 1] : null;
if (char == '_') {
continue;
}
if (i == 0 || prevChar == '_') {
char = char.toUpperCase();
}
result.write(char);
}
result.write('Controller');
return result.toString();
}
String _getMethodName(final String name) {
final result = StringBuffer();
final parts = name.split('');
for (var i = 0; i < parts.length; i++) {
var char = parts[i];
final prevChar = i > 0 ? parts[i - 1] : null;
if (char == '_') {
continue;
}
if (prevChar == '_') {
char = char.toUpperCase();
}
result.write(char);
}
return result.toString();
}
List<MethodParameter> _getMethodParameters(
final String controllerName,
final String methodName,
final List<String> parameters,
final List<String> docs,
) {
var reg = RegExp(r'@param ((?:[a-z|\[\]]+ )?)(\$?)([a-zA-Z_]+)((?: .*)?)');
final docMatches = <RegExpMatch>[];
for (final doc in docs) {
reg.allMatches(doc).forEach(docMatches.add);
}
final result = <MethodParameter>[];
reg = RegExp(r'(\??)((?:[a-z-A-Z]+ )?)\$([a-zA-Z_]+)((?: = .*)?)');
for (final parameter in parameters) {
final match = reg.allMatches(parameter).single;
var nullable = match.group(1)!.isNotEmpty;
String? type = match.group(2)!.trim();
if (type.isEmpty) {
type = null;
}
final name = match.group(3)!;
final defaultValue = match.group(4)!.replaceAll('=', '').trim();
String? description;
for (final doc in docMatches) {
final docName = doc.group(3)!.trim();
if (docName == name) {
final docType = doc.group(1)!.trim();
final docDescription = doc.group(4)!.trim();
if (docDescription.isNotEmpty) {
description = docDescription;
}
if (type == null && docType.isNotEmpty) {
final parts = docType.split('|').where((final p) => p.isNotEmpty);
if (parts.contains('null')) {
nullable = true;
}
final nonNullableParts = parts.where((final p) => p != 'null');
if (nonNullableParts.length > 1) {
if (nonNullableParts.contains('string')) {
// Catch all
type = 'string';
continue;
}
throw Exception(
'Can not determine reliable type for "$docType" for parameter "$name" of method "$methodName" in controller "$controllerName"',
);
} else {
type = nonNullableParts.single;
}
}
}
}
result.add(
MethodParameter(
type: type,
nullable: nullable,
name: name,
defaultValue: defaultValue.isNotEmpty ? defaultValue : null,
description: description,
controllerName: controllerName,
methodName: methodName,
),
);
}
return result;
}
Future<Map<String, List<Map<String, dynamic>>>> _parseRoutesFile(
final Directory tmpDirectory,
final String path,
) async {
final content = File(path).readAsStringSync();
late String routes;
if (content.contains('registerRoutes')) {
routes = RegExp(r'registerRoutes\(\$this, (\[[^;]*)\);').firstMatch(content)!.group(1)!;
} else if (content.contains('return [')) {
routes = RegExp(r'return (\[[^;]*);').firstMatch(content)!.group(1)!;
} else if (content.contains('return array_merge_recursive(')) {
final includes = RegExp(r"include\(__DIR__ . '/([^']*)'\),")
.allMatches(RegExp(r'return array_merge_recursive\(\n(.*)\n\);', dotAll: true).firstMatch(content)!.group(1)!)
.map((final match) => match.group(1)!)
.toList();
final out = <String, List<Map<String, dynamic>>>{};
for (final include in includes) {
final routes = await _parseRoutesFile(tmpDirectory, p.join(File(path).parent.path, include));
for (final key in routes.keys) {
if (!out.containsKey(key)) {
out[key] = [];
}
out[key]!.addAll(routes[key]!);
}
}
return out;
} else {
throw Exception('Unsupported routes format');
}
final variables = RegExp(r'^(\$(requirements[a-zA-Z]*) =[^;]*;)$', multiLine: true)
.allMatches(content)
.map((final match) => match.group(1)!)
.toList();
final phpFile = File(p.join(tmpDirectory.path, p.basename(path)));
final jsonFile = File(p.join(tmpDirectory.path, p.basename(path).replaceAll('.php', '.json')));
phpFile.writeAsStringSync(
'''
<?php
${variables.join('\n')}
\$fp = fopen('${jsonFile.path}', 'w');
fwrite(\$fp, str_replace("\\/","/",json_encode($routes)));
fclose(\$fp);
?>
''',
);
final result = await Process.run('php', [phpFile.path]);
if (result.exitCode != 0) {
throw Exception('Failed to run php: ${result.stderr}');
}
return (json.decode(jsonFile.readAsStringSync()) as Map<String, dynamic>).map(
(final key, final value) => MapEntry<String, List<Map<String, dynamic>>>(
key,
(value as List).map((final a) => a as Map<String, dynamic>).toList(),
),
);
}

93
packages/spec_templates/lib/method_parameter.dart

@ -1,93 +0,0 @@
// ignore_for_file: public_member_api_docs
class MethodParameter {
MethodParameter({
required this.type,
required this.nullable,
required this.name,
required final String? defaultValue,
required this.description,
required this.controllerName,
required this.methodName,
}) {
if (defaultValue == 'null') {
nullable = true;
}
if (type == null && defaultValue != null && defaultValue != 'null') {
nullable = false;
if (int.tryParse(defaultValue) != null) {
type = 'int';
}
if (defaultValue == 'true' || defaultValue == 'false') {
type = 'bool';
}
if (defaultValue == "''" || defaultValue == '""') {
type = 'string';
}
if (defaultValue == '[]') {
type = 'array';
}
}
if (type == null) {
throw Exception(
'Unknown type for parameter "$name" with default value "$defaultValue" of method "$methodName" in controller "$controllerName"',
);
}
if (defaultValue != null && defaultValue != 'null') {
switch (type) {
case 'int':
this.defaultValue = int.tryParse(defaultValue);
break;
case 'bool':
this.defaultValue = defaultValue == 'true';
break;
case 'string':
this.defaultValue = defaultValue.substring(1, defaultValue.length - 1);
break;
case 'array':
break;
default:
throw Exception('Unknown way to parse default value for type "$type"');
}
}
}
String? type;
bool nullable;
final String name;
dynamic defaultValue;
final String? description;
final String controllerName;
final String methodName;
String? get openAPIType {
if (type != null) {
if (type == 'string') {
return 'string';
}
if (type == 'int' || type == 'integer') {
return 'integer';
}
if (type == 'bool' || type == 'boolean') {
return 'boolean';
}
if (type == 'array') {
return 'array';
}
if (type == 'Chain') {
// Unsupported
return null;
}
throw Exception(
'Could not infer OpenAPI type from type "$type" for parameter "$name" of method "$methodName" in controller "$controllerName"',
);
}
return null;
}
@override
String toString() =>
'MethodParameter(type: $type, nullable: $nullable, name: $name, defaultValue: $defaultValue, description: $description, controllerName: $controllerName, methodName: $methodName)';
}

253
packages/spec_templates/lib/openapi_spec.dart

@ -1,253 +0,0 @@
// ignore_for_file: public_member_api_docs
class Spec {
Spec({
required this.version,
required this.info,
this.tags,
this.paths,
});
Map<String, dynamic> toMap() => {
'openapi': version,
'info': info.toMap(),
if (tags != null) ...{
'tags': tags!.map((final tag) => <String, String>{'name': tag}).toList(),
},
if (paths != null) ...{
'paths': paths!.map((final key, final value) => MapEntry(key, value.toMap())),
},
};
final String version;
final Info info;
final List<String>? tags;
final Map<String, Path>? paths;
}
class Info {
Info({
required this.title,
required this.version,
this.description,
this.license,
});
Map<String, dynamic> toMap() => {
'title': title,
'version': version,
if (description != null) 'description': description,
if (license != null) 'license': license!.toMap(),
};
final String title;
final String version;
final String? description;
final License? license;
}
class License {
License({
required this.name,
this.identifier,
this.url,
}) : assert(
(identifier == null) != (url == null),
'Specify either identifier or url',
);
Map<String, dynamic> toMap() => {
'name': name,
if (identifier != null) 'identifier': identifier,
if (url != null) 'url': url,
};
final String name;
final String? identifier;
final String? url;
}
class Server {
Server({
required this.url,
this.description,
this.variables,
});
final String url;
final String? description;
final Map<String, ServerVariable>? variables;
Map<String, dynamic> toMap() => {
'url': url,
if (description != null) 'description': description,
if (variables != null)
'variables': variables!.map(
(final key, final value) => MapEntry(
key,
value.toMap(),
),
),
};
}
class ServerVariable {
ServerVariable({
required this.default_,
this.enum_,
this.description,
});
final String default_;
final List<String>? enum_;
final String? description;
Map<String, dynamic> toMap() => {
if (enum_ != null) 'enum': enum_,
'default': default_,
if (description != null) 'description': description,
};
}
class Path {
Path({
this.summary,
this.description,
this.servers,
this.parameters,
this.get,
this.put,
this.post,
this.delete,
this.options,
this.head,
this.patch,
this.trace,
});
Map<String, dynamic> toMap() => {
if (summary != null) 'summary': summary,
if (description != null) 'description': description,
if (servers != null) 'servers': servers!.map((final s) => s.toMap()).toList(),
if (parameters != null && parameters!.isNotEmpty)
'parameters': parameters!.map((final p) => p.toMap()).toList(),
if (get != null) 'get': get!.toMap(),
if (put != null) 'put': put!.toMap(),
if (post != null) 'post': post!.toMap(),
if (delete != null) 'delete': delete!.toMap(),
if (options != null) 'options': options!.toMap(),
if (head != null) 'head': head!.toMap(),
if (patch != null) 'patch': patch!.toMap(),
if (trace != null) 'trace': trace!.toMap(),
};
final String? summary;
final String? description;
final List<Server>? servers;
final List<Parameter>? parameters;
Operation? get;
Operation? put;
Operation? post;
Operation? delete;
Operation? options;
Operation? head;
Operation? patch;
Operation? trace;
}
class Parameter {
Parameter({
required this.name,
required this.in_,
this.description,
this.required,
this.deprecated,
this.allowEmptyValue,
this.schema,
});
Map<String, dynamic> toMap() => {
'name': name,
'in': in_,
if (description != null) 'description': description,
if (required != null) 'required': required,
if (deprecated != null) 'deprecated': deprecated,
if (allowEmptyValue != null) 'allowEmptyValue': allowEmptyValue,
if (schema != null) 'schema': schema,
};
final String name;
final String in_;
final String? description;
final bool? required;
final bool? deprecated;
final bool? allowEmptyValue;
final Map<String, dynamic>? schema;
}
class Operation {
Operation({
this.operationID,
this.tags,
this.parameters,
this.responses,
});
Map<String, dynamic> toMap() => {
if (operationID != null) ...{
'operationId': operationID,
},
if (tags != null) ...{
'tags': tags,
},
if (parameters != null) ...{
'parameters': parameters!.map((final p) => p.toMap()).toList(),
},
if (responses != null) ...{
'responses': responses!.map(
(final key, final value) => MapEntry(
key.toString(),
value.toMap(),
),
),
},
};
final String? operationID;
final List<String>? tags;
final List<Parameter>? parameters;
final Map<dynamic, Response>? responses;
}
class Response {
Response({
required this.description,
this.content,
});
Map<String, dynamic> toMap() => {
'description': description,
if (content != null)
'content': content!.map(
(final key, final value) => MapEntry(
key,
value.toMap(),
),
),
};
final String description;
final Map<String, MediaType>? content;
}
class MediaType {
MediaType({
this.schema,
});
Map<String, dynamic> toMap() => {
'schema': schema,
};
final Map<String, dynamic>? schema;
}

16
packages/spec_templates/pubspec.yaml

@ -1,16 +0,0 @@
name: spec_templates
version: 1.0.0
publish_to: 'none'
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
path: ^1.8.3
xml: ^6.3.0
dev_dependencies:
nit_picking:
git:
url: https://github.com/stack11/dart_nit_picking
ref: 0b2ee0d

9
specs/templates/appinfo_core.xml

@ -1,9 +0,0 @@
<?xml version="1.0"?>
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>core</id>
<name>Core</name>
<summary>Core functionality of Nextcloud</summary>
<description><![CDATA[Core functionality of Nextcloud]]></description>
<version>27.0.0</version>
<licence>agpl</licence>
</info>

2882
specs/templates/core.json

File diff suppressed because it is too large Load Diff

2657
specs/templates/news.json

File diff suppressed because it is too large Load Diff

821
specs/templates/notes.json

@ -1,821 +0,0 @@
{
"openapi": "3.1.0",
"info": {
"title": "Notes",
"version": "4.8.0",
"description": "Distraction-free notes and writing",
"license": {
"name": "agpl",
"identifier": "AGPL-3.0"
}
},
"paths": {
"/apps/notes/notes": {
"get": {
"operationId": "notes-index-TODO",
"tags": [
"notes"
],
"parameters": [
{
"name": "pruneBefore",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 0
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"post": {
"operationId": "notes-create-TODO",
"tags": [
"notes"
],
"parameters": [
{
"name": "category",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": ""
}
},
{
"name": "content",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": ""
}
},
{
"name": "title",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": ""
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/notes/dashboard": {
"get": {
"operationId": "notes-dashboard-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/notes/{id}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"get": {
"operationId": "notes-get-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"put": {
"operationId": "notes-update-TODO",
"tags": [
"notes"
],
"parameters": [
{
"name": "content",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"delete": {
"operationId": "notes-destroy-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/notes/undo": {
"post": {
"operationId": "notes-undo-TODO",
"tags": [
"notes"
],
"parameters": [
{
"name": "id",
"in": "query",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "title",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "content",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "category",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "modified",
"in": "query",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "favorite",
"in": "query",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/notes/{id}/autotitle": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"put": {
"operationId": "notes-autotitle-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/notes/{id}/{property}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "property",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"put": {
"operationId": "notes-updateproperty-TODO",
"tags": [
"notes"
],
"parameters": [
{
"name": "modified",
"in": "query",
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "title",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "category",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "favorite",
"in": "query",
"required": false,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/notes/{noteid}/attachment": {
"parameters": [
{
"name": "noteid",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"get": {
"operationId": "notes-getattachment-TODO",
"tags": [
"notes"
],
"parameters": [
{
"name": "path",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"post": {
"operationId": "notes-uploadfile-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/settings": {
"get": {
"operationId": "settings-get-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"put": {
"operationId": "settings-set-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/settings/migrate": {
"post": {
"operationId": "settings-migrate-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/api/{apiVersion}/notes": {
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "TODO"
}
}
],
"get": {
"operationId": "notes_api-index-TODO",
"tags": [
"notes"
],
"parameters": [
{
"name": "category",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "exclude",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": ""
}
},
{
"name": "pruneBefore",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 0
}
},
{
"name": "chunkSize",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 0
}
},
{
"name": "chunkCursor",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"post": {
"operationId": "notes_api-create-TODO",
"tags": [
"notes"
],
"parameters": [
{
"name": "category",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": ""
}
},
{
"name": "title",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": ""
}
},
{
"name": "content",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": ""
}
},
{
"name": "modified",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 0
}
},
{
"name": "favorite",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 0
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/api/{apiVersion}/notes/{id}": {
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "TODO"
}
},
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"get": {
"operationId": "notes_api-get-TODO",
"tags": [
"notes"
],
"parameters": [
{
"name": "exclude",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": ""
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"put": {
"operationId": "notes_api-update-TODO",
"tags": [
"notes"
],
"parameters": [
{
"name": "content",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "modified",
"in": "query",
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "title",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "category",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "favorite",
"in": "query",
"required": false,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"delete": {
"operationId": "notes_api-destroy-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/api/{apiVersion}/settings": {
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "TODO"
}
}
],
"get": {
"operationId": "notes_api-getsettings-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"put": {
"operationId": "notes_api-setsettings-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/apps/notes/api/{catchAll}": {
"parameters": [
{
"name": "catchAll",
"in": "path",
"required": true,
"schema": {
"type": "TODO"
}
}
],
"get": {
"operationId": "notes_api-fail-TODO",
"tags": [
"notes"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}

401
specs/templates/notifications.json

@ -1,401 +0,0 @@
{
"openapi": "3.1.0",
"info": {
"title": "Notifications",
"version": "2.15.0",
"description": "This app provides a backend and frontend for the notification API available in Nextcloud.",
"license": {
"name": "agpl",
"identifier": "AGPL-3.0"
}
},
"paths": {
"/ocs/v2.php/apps/notifications/api/{apiVersion}/notifications": {
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"get": {
"operationId": "endpoint-listnotifications-TODO",
"tags": [
"notifications"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"delete": {
"operationId": "endpoint-deleteallnotifications-TODO",
"tags": [
"notifications"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/notifications/api/{apiVersion}/notifications/{id}": {
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"get": {
"operationId": "endpoint-getnotification-TODO",
"tags": [
"notifications"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"delete": {
"operationId": "endpoint-deletenotification-TODO",
"tags": [
"notifications"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/notifications/api/{apiVersion}/notifications/exists": {
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"post": {
"operationId": "endpoint-confirmidsforuser-TODO",
"tags": [
"notifications"
],
"parameters": [
{
"name": "ids",
"in": "query",
"required": true,
"schema": {
"type": "array"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/notifications/api/{apiVersion}/push": {
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "TODO"
}
}
],
"post": {
"operationId": "push-registerdevice-TODO",
"tags": [
"notifications"
],
"parameters": [
{
"name": "pushTokenHash",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "devicePublicKey",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "proxyServer",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
},
"delete": {
"operationId": "push-removedevice-TODO",
"tags": [
"notifications"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/notifications/api/{apiVersion}/admin_notifications/{userId}": {
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "TODO"
}
},
{
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"post": {
"operationId": "api-generatenotification-TODO",
"tags": [
"notifications"
],
"parameters": [
{
"name": "shortMessage",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "longMessage",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": ""
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/notifications/api/{apiVersion}/settings": {
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "TODO"
}
}
],
"post": {
"operationId": "settings-personal-TODO",
"tags": [
"notifications"
],
"parameters": [
{
"name": "batchSetting",
"in": "query",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "soundNotification",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "soundTalk",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/notifications/api/{apiVersion}/settings/admin": {
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "TODO"
}
}
],
"post": {
"operationId": "settings-admin-TODO",
"tags": [
"notifications"
],
"parameters": [
{
"name": "batchSetting",
"in": "query",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "soundNotification",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "soundTalk",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}

1522
specs/templates/provisioning_api.json

File diff suppressed because it is too large Load Diff

316
specs/templates/user_status.json

@ -1,316 +0,0 @@
{
"openapi": "3.1.0",
"info": {
"title": "User status",
"version": "1.7.0",
"description": "User status",
"license": {
"name": "agpl",
"identifier": "AGPL-3.0"
}
},
"paths": {
"/ocs/v2.php/apps/user_status/api/v1/statuses": {
"get": {
"operationId": "statuses-findall-TODO",
"tags": [
"user_status"
],
"parameters": [
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "offset",
"in": "query",
"required": false,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/user_status/api/v1/statuses/{userId}": {
"parameters": [
{
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"get": {
"operationId": "statuses-find-TODO",
"tags": [
"user_status"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/user_status/api/v1/user_status": {
"get": {
"operationId": "userstatus-getstatus-TODO",
"tags": [
"user_status"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/user_status/api/v1/user_status/status": {
"put": {
"operationId": "userstatus-setstatus-TODO",
"tags": [
"user_status"
],
"parameters": [
{
"name": "statusType",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/user_status/api/v1/user_status/message/predefined": {
"put": {
"operationId": "userstatus-setpredefinedmessage-TODO",
"tags": [
"user_status"
],
"parameters": [
{
"name": "messageId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "clearAt",
"in": "query",
"required": false,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/user_status/api/v1/user_status/message/custom": {
"put": {
"operationId": "userstatus-setcustommessage-TODO",
"tags": [
"user_status"
],
"parameters": [
{
"name": "statusIcon",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "message",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "clearAt",
"in": "query",
"required": false,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/user_status/api/v1/user_status/message": {
"delete": {
"operationId": "userstatus-clearmessage-TODO",
"tags": [
"user_status"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/user_status/api/v1/user_status/revert/{messageId}": {
"parameters": [
{
"name": "messageId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"delete": {
"operationId": "userstatus-revertstatus-TODO",
"tags": [
"user_status"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/user_status/api/v1/predefined_statuses": {
"get": {
"operationId": "predefinedstatus-findall-TODO",
"tags": [
"user_status"
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/ocs/v2.php/apps/user_status/api/v1/heartbeat": {
"put": {
"operationId": "heartbeat-heartbeat-TODO",
"tags": [
"user_status"
],
"parameters": [
{
"name": "status",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}

125
tool/generate-specs.sh

@ -1,125 +0,0 @@
#!/bin/bash
set -euxo pipefail
cd "$(dirname "$0")/.."
rm -rf /tmp/nextcloud-neon
mkdir -p /tmp/nextcloud-neon
xmlstarlet \
edit \
--update "/info/version" \
--value "$(cd external/nextcloud-server && git describe --tags | sed "s/^v//")" \
specs/templates/appinfo_core.xml \
> /tmp/nextcloud-neon/appinfo_core1.xml
xmlstarlet \
format \
--indent-spaces 4 \
/tmp/nextcloud-neon/appinfo_core1.xml \
> /tmp/nextcloud-neon/appinfo_core2.xml
cp /tmp/nextcloud-neon/appinfo_core2.xml specs/templates/appinfo_core.xml
function generate_spec_templates() {
fvm dart packages/spec_templates/bin/generate.dart "$1" "$2"
}
generate_spec_templates external/nextcloud-news false
generate_spec_templates external/nextcloud-notes false
generate_spec_templates external/nextcloud-notifications false
generate_spec_templates external/nextcloud-server/apps/provisioning_api false
generate_spec_templates external/nextcloud-server/apps/user_status false
generate_spec_templates external/nextcloud-server/core true
codenames=(core news notes notifications provisioning_api user_status)
for codename in ${codenames[*]}; do
jq \
--arg codename "$codename" \
-s \
'{
openapi: .[0].openapi,
info: .[0].info,
servers: [
{
url: "https://{hostname}:{port}",
variables: {
hostname: {
default: "localhost"
},
port: {
default: "8080"
}
}
}
],
security: [
{
basic_auth: []
}
],
tags: .[1].tags,
components: {
schemas: .[1].components.schemas
},
paths: .[1].paths
} |
.components.securitySchemes = {
basic_auth: {
type: "http",
scheme: "basic",
},
} |
.components.schemas.OCSMeta =
{
type: "object",
required: [
"status",
"statuscode"
],
properties: {
status: {
type: "string"
},
statuscode: {
type: "integer"
},
message: {
type: "string"
},
totalitems: {
type: "string"
},
itemsperpage: {
type: "string"
}
}
} |
.components.schemas.EmptyOCS =
{
type: "object",
required: [
"ocs"
],
properties: {
ocs: {
type: "object",
required: [
"meta",
"data"
],
properties: {
meta: {
"$ref": "#/components/schemas/OCSMeta"
},
data: {
type: "array"
}
}
}
}
}
' \
specs/templates/"$codename".json \
specs/"$codename".json \
> /tmp/nextcloud-neon/"$codename".json
cp /tmp/nextcloud-neon/"$codename".json specs/"$codename".json
done
Loading…
Cancel
Save