diff --git a/avrotize/dependency_resolver.py b/avrotize/dependency_resolver.py index bc06957..14d1fab 100644 --- a/avrotize/dependency_resolver.py +++ b/avrotize/dependency_resolver.py @@ -15,7 +15,21 @@ def inline_dependencies_of(avro_schema, record): def sort_messages_by_dependencies(avro_schema): - + """ + Sort the messages in avro_schema by their dependencies. Avro Schema requires + that type definitions must be defined before they are used. This method + ensures this. Types that have dependencies will be moved at the end of the list. + If necessary, it will also resolve circular dependencies by inlining the + dependent record. + + The method expects all types with dependencies to have a 'dependencies' key in their + dict that contains a list of types that they depend on. + + Args: + avro_schema: List of Avro schema records. + """ + + # if all are just strings, then it is already sorted if all(isinstance(record, str) for record in avro_schema): return avro_schema @@ -27,12 +41,8 @@ def sort_messages_by_dependencies(avro_schema): sorted_messages.append(record) avro_schema.remove(record) continue - for field in record.get('fields', []): - if isinstance(field['type'], dict): - field['type'] = sort_messages_by_dependencies([field['type']])[0] - elif isinstance(field['type'], list): - field['type'] = sort_messages_by_dependencies(field['type']) + # if this record is not a dependency of any other record, it can be safely emitted now if not any(record.get('name') in other_record.get('dependencies', []) or (record.get('namespace','')+'.'+record.get('name')) in other_record.get('dependencies', []) for other_record in [x for x in avro_schema if isinstance(x, dict) and 'name' in x]): if 'dependencies' in record: @@ -41,11 +51,15 @@ def sort_messages_by_dependencies(avro_schema): avro_schema.remove(record) found = True - # If there are no records without dependencies, so we just take one record and move on + # If there are no records without dependencies, we will grab the first + # record with dependencies and start resolving circular dependencies if len(avro_schema) > 0 and not found: - record = avro_schema[0] - if 'dependencies' in record: + record = next((x for x in avro_schema if isinstance(x, dict) and 'dependencies' in x), None) + if record: + avro_schema_len = len(avro_schema) swap_record_dependencies(avro_schema, record) + if len(avro_schema) == avro_schema_len: + inline_dependencies_of(avro_schema, record) sorted_messages.reverse() return sorted_messages @@ -61,6 +75,11 @@ def swap_record_dependencies(avro_schema, record): swap_dependency_type(avro_schema, field, dependency, dependency_type, deps) record['dependencies'] = [dep for dep in deps if dep != record['name'] and record.get('namespace','')+'.'+record['name'] != dep] +def strip_namespace(name): + if isinstance(name, str): + return name.split('.')[-1] + return name + def swap_dependency_type(avro_schema, field, dependency, dependency_type, dependencies): """ to break circular dependencies, we will inline the dependent record and remove the dependency """ if not dependency in dependencies: @@ -69,7 +88,7 @@ def swap_dependency_type(avro_schema, field, dependency, dependency_type, depend return # Replace the dependency type with the dependency_type in avro_schema. - if field['type'] == dependency: + if strip_namespace(field['type']) == dependency: field['type'] = dependency_type if dependency_type in avro_schema: avro_schema.remove(dependency_type) @@ -81,8 +100,8 @@ def swap_dependency_type(avro_schema, field, dependency, dependency_type, depend # type is a Union? elif type(field['type']) is list: for field_type in field['type']: - if field_type == dependency: - field['type'].remove(dependency) + if strip_namespace(field_type) == dependency: + field['type'].remove(field_type) field['type'].append(dependency_type) if dependency_type in avro_schema: avro_schema.remove(dependency_type) @@ -92,12 +111,14 @@ def swap_dependency_type(avro_schema, field, dependency, dependency_type, depend swap_record_dependencies(avro_schema, dependency_type) del dependency_type['dependencies'] # type is an object? - elif type(field_type) is dict and field_type.get('type') != None and field_type.get('name') == dependency: + elif isinstance(field_type, dict) and 'type' in field_type and field_type.get('type') == dependency or \ + 'items' in field_type and field_type.get('items') == dependency or \ + 'values' in field_type and field_type.get('values') == dependency: swap_dependency_type(avro_schema, field_type, dependency, dependency_type, dependencies) elif 'type' in field['type']: swap_dependency_type(avro_schema, field['type'], dependency, dependency_type, dependencies) elif field['type'] == 'array': - if field['items'] == dependency: + if strip_namespace(field['items']) == dependency: field['items'] = dependency_type if dependency_type in avro_schema: avro_schema.remove(dependency_type) @@ -109,7 +130,7 @@ def swap_dependency_type(avro_schema, field, dependency, dependency_type, depend elif 'type' in field['items']: swap_dependency_type(avro_schema, field['items'], dependency, dependency_type, dependencies) elif field['type'] == 'map': - if field['values'] == dependency: + if strip_namespace(field['values']) == dependency: field['values'] = dependency_type if dependency_type in avro_schema: avro_schema.remove(dependency_type) diff --git a/avrotize/jsonstoavro.py b/avrotize/jsonstoavro.py index 54476d0..e5a8630 100644 --- a/avrotize/jsonstoavro.py +++ b/avrotize/jsonstoavro.py @@ -159,8 +159,13 @@ def json_schema_primitive_to_avro_type(json_primitive: str | list, format: str, if enum: # replace white space with underscore - enum = [avro_name(e) for e in enum if isinstance(e, str)] - avro_primitive = {"type": "enum", "symbols": enum, "name": avro_name(field_name + "_enum")} + enum = [avro_name(e) for e in enum if isinstance(e, str) and e != ""] + # purge duplicates + enum = list(dict.fromkeys(enum)) + if len(enum) > 0: + avro_primitive = {"type": "enum", "symbols": enum, "name": avro_name(field_name + "_enum")} + else: + avro_primitive = "string" return avro_primitive @@ -238,7 +243,7 @@ def json_type_to_avro_type(json_type: str | dict, record_name: str, field_name: """Convert a JSON type to Avro type.""" avro_type = {} - qualified_name = record_name + '_' + field_name if field_name else record_name + qualified_name = avro_name(record_name + '_' + field_name if field_name else record_name) if isinstance(json_type, dict): @@ -308,13 +313,16 @@ def json_type_to_avro_type(json_type: str | dict, record_name: str, field_name: count = 1 type_deps = [] for json_type_option in json_types: - type_name = field_name + "_" + str(count) if field_name else str(count) - avro_subtype = json_type_to_avro_type(json_type_option, record_name, type_name, namespace, dependencies, json_schema, base_uri, avro_schema, record_stack) + sub_record_name = avro_name(qualified_name + "_" + str(count)) + avro_subtype = json_type_to_avro_type(json_type_option, sub_record_name, field_name, namespace, dependencies, json_schema, base_uri, avro_schema, record_stack) if 'dependencies' in avro_subtype: type_deps.extend(avro_subtype['dependencies']) del avro_subtype['dependencies'] if not is_empty_type(avro_subtype): - subtypes.append(avro_subtype) + if isinstance(avro_subtype, list): + subtypes.extend(avro_subtype) + else: + subtypes.append(avro_subtype) count += 1 if len(subtypes) == 1: return subtypes[0] @@ -333,7 +341,13 @@ def json_type_to_avro_type(json_type: str | dict, record_name: str, field_name: if '$ref' in json_type: ref = json_type['$ref'] if ref in imported_types: - return imported_types[ref] + type_ref = imported_types[ref] + if isinstance(type_ref, str): + if type_ref.startswith(namespace+'.'): + dependencies.append(type_ref[len(namespace)+1:]) + else: + dependencies.append(type_ref) + return type_ref else: new_base_uri = urljoin(base_uri, json_type['$ref']) resolved_json_type = resolve_reference(json_type, base_uri, json_schema) @@ -348,8 +362,13 @@ def json_type_to_avro_type(json_type: str | dict, record_name: str, field_name: # registering in imported_types ahead of resolving to prevent circular references imported_types[ref] = type_name # resolve type - avro_type = json_type_to_avro_type(resolved_json_type, type_name, field_name, namespace, dependencies, json_schema, new_base_uri, avro_schema, record_stack) - if isinstance(avro_type, list): + deps = [] + avro_type = json_type_to_avro_type(resolved_json_type, type_name, field_name, namespace, deps, json_schema, new_base_uri, avro_schema, record_stack) + if isinstance(avro_type, list) or (not isinstance(avro_type, dict) or not avro_type.get('type') == "record"): + if isinstance(avro_type, dict) and not 'type' in avro_type: + print(f"WARNING: no type definition for {ref} in record {record_name}: {json.dumps(avro_type)}") + avro_type = generic_type() + avro_type = { "type": "record", "name": type_name, @@ -361,12 +380,15 @@ def json_type_to_avro_type(json_type: str | dict, record_name: str, field_name: } ] } + + if isinstance(avro_type, dict) and len(deps) > 0: + avro_type['dependencies'] = deps imported_types[ref] = copy.deepcopy(avro_type) if isinstance(avro_type, dict) and 'name' in avro_type: existing_type = next((t for t in avro_schema if t.get('name') == avro_type['name'] and t.get('namespace') == avro_type.get('namespace') ), None) if not existing_type: avro_schema.append(avro_type) - full_name = avro_type['name'] #avro_type.get('namespace','')+'.'+avro_type['name'] if 'namespace' in avro_type else avro_type['name'] + full_name = avro_type.get('namespace','')+'.'+avro_type['name'] if 'namespace' in avro_type else avro_type['name'] imported_types[ref] = full_name dependencies.append(avro_type['name']) avro_type = full_name @@ -394,8 +416,13 @@ def json_type_to_avro_type(json_type: str | dict, record_name: str, field_name: avro_type = merge_schemas([avro_type, {"type": "array", "items": generic_type()}], avro_schema, avro_type.get('name', qualified_name)) elif json_object_type == 'object' or 'object' in json_object_type: avro_type = merge_schemas([avro_type, json_schema_object_to_avro_record(qualified_name, json_type, namespace, json_schema, base_uri, avro_schema, record_stack)], avro_schema, avro_type.get('name', qualified_name)) + if 'dependencies' in avro_type: + dependencies.extend(avro_type['dependencies']) + del avro_type['dependencies'] else: avro_type = json_schema_primitive_to_avro_type(json_object_type, json_type.get('format'), json_type.get('enum'), field_name, dependencies) + elif 'enum' in json_type: + avro_type = merge_schemas([avro_type,json_schema_primitive_to_avro_type("string", json_type.get('format'), json_type.get('enum'), field_name, dependencies)], avro_schema, avro_type.get('name', qualified_name)) else: avro_type = merge_schemas([avro_type,json_schema_primitive_to_avro_type(json_type, json_type.get('format'), json_type.get('enum'), field_name, dependencies)], avro_schema, avro_type.get('name', qualified_name)) @@ -418,8 +445,28 @@ def json_schema_object_to_avro_record(name: str, json_object: dict, namespace: s """Convert a JSON schema object declaration to an Avro record.""" dependencies = [] + + # handle top-level allOf, anyOf, oneOf + if isinstance(json_object, dict) and ('allOf' in json_object or 'oneOf' in json_object or 'anyOf' in json_object): + type = json_type_to_avro_type(json_object, name, "value", namespace, dependencies, json_schema, base_uri, avro_schema, record_stack) + if isinstance(type, list): + type = { + "type": "record", + "name": name, + "namespace": namespace, + "fields": [ + { + "name": "value", + "type": type + } + ] + } + if dependencies and isinstance(type, dict): + type['dependencies'] = dependencies + return type + title = json_object.get('title') - record_name = avro_name(title if title else name if name else None) + record_name = avro_name(name if name else title if title else None) if record_name == None: raise ValueError(f"Cannot determine record name for json_object {json_object}") if len(record_stack) > 0 and not record_name.startswith(record_stack[-1]): @@ -505,7 +552,7 @@ def json_schema_object_to_avro_record(name: str, json_object: dict, namespace: s raise ValueError(f"prop_type for pattern name {pattern_name} and record_name {record_name} is empty") avro_record['fields'].append( { - "name": pattern_name, + "name": avro_name(pattern_name), "namespace": namespace, "type": { "type": "map", @@ -518,12 +565,14 @@ def json_schema_object_to_avro_record(name: str, json_object: dict, namespace: s values_type = json_type_to_avro_type(additional_props, record_name, record_name + "_extensions", namespace, dependencies, json_schema, base_uri, avro_schema, record_stack) if is_empty_type(values_type): values_type = generic_type() - avro_record = { - "type": "map", - "name": record_name, - "namespace": namespace, - "values": values_type - } + avro_record['fields'].append( + { + "name": "values", + "type": { + "type": "map", + "values": copy.deepcopy(values_type) + } + }) elif 'patternProperties' in json_object and isinstance(json_object['patternProperties'], dict): pattern_props = json_object['patternProperties'] prop_types = [] @@ -531,43 +580,52 @@ def json_schema_object_to_avro_record(name: str, json_object: dict, namespace: s pattern_name = re.sub(r'[^a-zA-Z0-9_]', '_', pattern_name) if pattern_name == "": pattern_name = "extensions" - prop_types.append(ensure_type(json_type_to_avro_type(props, record_name, pattern_name, namespace, dependencies, json_schema, base_uri, avro_schema, record_stack))) - avro_record = { - "type": "map", - "name": record_name, - "namespace": namespace, - "values": prop_types[0] if len(prop_types) == 1 else prop_types - } + type = ensure_type(json_type_to_avro_type(props, record_name, pattern_name, namespace, dependencies, json_schema, base_uri, avro_schema, record_stack)) + avro_record['fields'].append( + { + "name": avro_name(pattern_name), + "namespace": namespace, + "type": { + "type": "map", + "values": copy.deepcopy(type) + } + }) elif 'type' in json_object and (json_object['type'] == 'object' or 'object' in json_object['type']) and \ not 'allOf' in json_object and not 'oneOf' in json_object and not 'anyOf' in json_object: - avro_record = { - "type": "map", - "name": record_name, - "namespace": namespace, - "values": generic_type() - } + avro_record['fields'].append( + { + "name": "values", + "type": { + "type": "map", + "values": generic_type() + } + }) elif 'type' in json_object and (json_object['type'] == 'array' or 'array' in json_object['type']) and \ not 'allOf' in json_object and not 'oneOf' in json_object and not 'anyOf' in json_object: if 'items' in json_object: avro_type = json_type_to_avro_type(json_object['items'], record_name, 'values', namespace, dependencies, json_schema, base_uri, avro_schema, record_stack) else: avro_type = generic_type() - avro_record = { - "type": "array", - "name": record_name, - "namespace": namespace, - "items": avro_type - } + avro_record['fields'].append( + { + "name": "values", + "type": { + "type": "array", + "items": copy.deepcopy(avro_type) + } + }) else: avro_record = { "name": record_name, - "namespace": namespace + "namespace": namespace, } if 'description' in json_object: avro_record['doc'] = json_object['description'] if len(dependencies) > 0: - avro_record['dependencies'] = dependencies + # dedupe the list + dependencies = list(set(dependencies)) + avro_record['dependencies'] = dependencies record_stack.pop() return avro_record @@ -621,11 +679,18 @@ def process_definition_list(json_schema, namespace, base_uri, avro_schema, recor process_definition_list(json_schema, namespace, base_uri, avro_schema, record_stack, schema_name, schema) def process_definition(json_schema, namespace, base_uri, avro_schema, record_stack, schema_name, schema): - avro_schema_item = json_schema_object_to_avro_record(schema_name, schema, namespace, json_schema, base_uri, avro_schema, record_stack) - existing_type = next((t for t in avro_schema if t.get('name') == avro_schema_item['name'] and t.get('namespace') == avro_schema_item.get('namespace') ), None) - if not existing_type: + """ Process a schema definition. """ + avro_schema_item_list = json_schema_object_to_avro_record(schema_name, schema, namespace, json_schema, base_uri, avro_schema, record_stack) + if not isinstance(avro_schema_item_list, list) and not isinstance(avro_schema_item_list, dict): + return + # the call above usually returns a single record, but we pretend it's normally a list to handle allOf/anyOf/oneOf cases + if not isinstance(avro_schema_item_list, list): + avro_schema_item_list = [avro_schema_item_list] + for avro_schema_item in avro_schema_item_list: avro_schema_item['name'] = avro_name(schema_name) - avro_schema.append(avro_schema_item) + existing_type = next((t for t in avro_schema if t.get('name') == avro_schema_item['name'] and t.get('namespace') == avro_schema_item.get('namespace') ), None) + if not existing_type: + avro_schema.append(avro_schema_item) def id_to_avro_namespace(id: str) -> str: diff --git a/test/jsons/travis.json b/test/jsons/travis.json new file mode 100644 index 0000000..0b5e0e6 --- /dev/null +++ b/test/jsons/travis.json @@ -0,0 +1,2264 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "allOf": [ + { + "$ref": "#/definitions/job" + }, + { + "type": "object", + "properties": { + "notifications": { + "type": "object", + "properties": { + "webhooks": { + "oneOf": [ + { + "$ref": "#/definitions/possiblySecretStringOrPossiblySecretStringTypeArrayUnique" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/notificationObject/webhooks" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/notificationObject/webhooks" + } + } + ] + }, + "slack": { + "oneOf": [ + { + "$ref": "#/definitions/slackRoom" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/notificationObject/slack" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/notificationObject/slack" + } + } + ] + }, + "email": { + "oneOf": [ + { + "$ref": "#/definitions/possiblySecretStringOrPossiblySecretStringTypeArrayUnique" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/notificationObject/email" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/notificationObject/email" + } + } + ] + }, + "irc": { + "oneOf": [ + { + "$ref": "#/definitions/possiblySecretStringOrPossiblySecretStringTypeArrayUnique" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/notificationObject/irc" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/notificationObject/irc" + } + } + ] + }, + "pushover": { + "oneOf": [ + { + "$ref": "#/definitions/nonEmptyStringOrArrayOfNonEmptyStrings" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/notificationObject/pushover" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/notificationObject/pushover" + } + } + ] + }, + "campfire": { + "oneOf": [ + { + "$ref": "#/definitions/possiblySecretStringOrPossiblySecretStringTypeArrayUnique" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/notificationObject/campfire" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/notificationObject/campfire" + } + } + ] + }, + "flowdock": { + "oneOf": [ + { + "$ref": "#/definitions/possiblySecretString" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/notificationObject/flowdock" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/notificationObject/flowdock" + } + } + ] + }, + "hipchat": { + "oneOf": [ + { + "$ref": "#/definitions/possiblySecretStringOrPossiblySecretStringTypeArrayUnique" + }, + { + "type": "boolean" + }, + { + "$ref": "#/definitions/notificationObject/hipchat" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/notificationObject/hipchat" + } + } + ] + } + }, + "additionalProperties": false + }, + "matrix": { + "type": "object", + "properties": { + "exclude": { + "type": "array", + "items": { + "$ref": "#/definitions/job" + } + }, + "include": { + "type": "array", + "items": { + "$ref": "#/definitions/job" + } + }, + "allow_failures": { + "type": "array", + "items": { + "$ref": "#/definitions/job" + } + }, + "fast_finish": { + "type": "boolean", + "description": "If some rows in the build matrix are allowed to fail, the build won't be marked as finished until they have completed. To mark the build as finished as soon as possible, add fast_finish: true" + } + }, + "additionalProperties": false + }, + "jobs": { + "type": "object", + "additionalProperties": false, + "properties": { + "include": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/job" + }, + { + "type": "object", + "properties": { + "stage": { + "type": "string", + "description": "The name of the build stage", + "default": "test" + } + } + } + ] + } + }, + "exclude": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/job" + }, + { + "type": "object", + "properties": { + "stage": { + "type": "string", + "description": "The name of the build stage", + "default": "test" + } + } + } + ] + } + }, + "allow_failures": { + "type": "array", + "items": { + "$ref": "#/definitions/job" + } + }, + "fast_finish": { + "type": "boolean", + "description": "If some rows in the build matrix are allowed to fail, the build won't be marked as finished until they have completed. To mark the build as finished as soon as possible, add fast_finish: true" + } + } + }, + "stages": { + "type": "array", + "description": "Specifies the order of build stages", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "if": { + "description": "Specifies a condition for the stage", + "type": "string" + } + } + } + ] + } + }, + "version": { + "type": "string", + "description": "Build config specification version", + "pattern": "^(~>|>|>=|=|<=|<) (\\d+(?:\\.\\d+)?(?:\\.\\d+)?)$" + }, + "import": { + "description": "Import YAML config snippets that can be shared across repositories.", + "oneOf": [ + { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/import" + } + }, + { + "$ref": "#/definitions/import" + } + ] + } + } + } + ], + "definitions": { + "nonEmptyString": { + "type": "string", + "minLength": 1 + }, + "notRequiredNonEmptyString": { + "oneOf": [ + { + "$ref": "#/definitions/nonEmptyString" + }, + { + "type": "null" + } + ] + }, + "arrayOfNonEmptyStrings": { + "type": "array", + "items": { + "$ref": "#/definitions/nonEmptyString" + } + }, + "nonEmptyStringOrArrayOfNonEmptyStrings": { + "oneOf": [ + { + "$ref": "#/definitions/nonEmptyString" + }, + { + "$ref": "#/definitions/arrayOfNonEmptyStrings" + } + ] + }, + "notRequiredNonEmptyStringOrArrayOfNonEmptyStrings": { + "oneOf": [ + { + "$ref": "#/definitions/nonEmptyStringOrArrayOfNonEmptyStrings" + }, + { + "type": "null" + } + ] + }, + "stringArrayUnique": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/nonEmptyString" + } + }, + "stringOrStringArrayUnique": { + "oneOf": [ + { + "$ref": "#/definitions/nonEmptyString" + }, + { + "$ref": "#/definitions/stringArrayUnique" + } + ] + }, + "stringOrNumber": { + "oneOf": [ + { + "$ref": "#/definitions/nonEmptyString" + }, + { + "type": "number" + } + ] + }, + "stringOrNumberAndBothAreTypeArrayUnique": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/stringOrNumber" + } + }, + "stringOrNumberOrAcceptBothTypeAsArrayUnique": { + "oneOf": [ + { + "$ref": "#/definitions/stringOrNumber" + }, + { + "$ref": "#/definitions/stringOrNumberAndBothAreTypeArrayUnique" + } + ] + }, + "secretString": { + "type": "object", + "additionalProperties": false, + "properties": { + "secure": { + "$ref": "#/definitions/nonEmptyString" + } + } + }, + "possiblySecretString": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "secure": { + "type": "string" + } + } + } + ] + }, + "possiblySecretStringOrPossiblySecretStringTypeArrayUnique": { + "oneOf": [ + { + "$ref": "#/definitions/possiblySecretString" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/possiblySecretString" + } + } + ] + }, + "slackRoom": { + "description": "Your account name, token and optional channel", + "oneOf": [ + { + "type": "string", + "pattern": ".+:.+(#.+)?" + }, + { + "$ref": "#/definitions/secretString" + } + ] + }, + "notificationFrequency": { + "enum": ["always", "never", "change"] + }, + "step": { + "anyOf": [ + { + "type": "boolean" + }, + { + "enum": ["skip", "ignore"] + }, + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "service": { + "enum": [ + "cassandra", + "couchdb", + "docker", + "elasticsearch", + "mariadb", + "memcached", + "mongodb", + "mysql", + "neo4j", + "postgresql", + "rabbitmq", + "redis", + "redis-server", + "rethinkdb", + "riak", + "xvfb" + ] + }, + "cache": { + "enum": [ + "apt", + "bundler", + "cargo", + "ccache", + "cocoapods", + "packages", + "pip", + "yarn", + "npm" + ] + }, + "xcodeVersions": { + "enum": [ + "xcode6.4", + "xcode7.3", + "xcode8", + "xcode8.3", + "xcode9", + "xcode9.1", + "xcode9.2", + "xcode9.3", + "xcode9.4", + "xcode10", + "xcode10.1", + "xcode10.2", + "xcode10.3", + "xcode11", + "xcode11.1", + "xcode11.2", + "xcode11.3", + "xcode11.4", + "xcode11.4.1", + "xcode11.5", + "xcode11.6", + "xcode12u", + "xcode12", + "xcode12.2", + "xcode12.3", + "xcode12.4", + "xcode12.5", + "xcode13.1", + "xcode13.2", + "xcode13.3", + "xcode13.4", + "xcode14", + "xcode14.1", + "xcode14.2" + ] + }, + "envVars": { + "oneOf": [ + { + "$ref": "#/definitions/envVar" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/envVar" + } + } + ] + }, + "envVar": { + "oneOf": [ + { + "type": "string", + "pattern": "[^=]+=.*" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "secure": { + "type": "string", + "pattern": "[^=]+=.*" + } + } + } + ] + }, + "job": { + "type": "object", + "properties": { + "language": { + "enum": [ + "android", + "bash", + "c", + "c++", + "clojure", + "cpp", + "crystal", + "csharp", + "d", + "dart", + "dartlang", + "elixir", + "elm", + "erlang", + "generic", + "go", + "golang", + "groovy", + "haskell", + "haxe", + "java", + "javascript", + "julia", + "jvm", + "matlab", + "minimal", + "nix", + "node", + "node.js", + "node_js", + "nodejs", + "obj-c", + "obj_c", + "objective-c", + "objective_c", + "perl", + "perl6", + "php", + "python", + "r", + "ruby", + "rust", + "scala", + "sh", + "shell", + "smalltalk" + ] + }, + "matlab": { + "$ref": "#/definitions/stringOrStringArrayUnique" + }, + "elm": { + "$ref": "#/definitions/stringOrStringArrayUnique" + }, + "elm-test": { + "$ref": "#/definitions/nonEmptyString" + }, + "elm-format": { + "$ref": "#/definitions/nonEmptyString" + }, + "haxe": { + "type": "array", + "items": { + "type": "string" + } + }, + "scala": { + "type": "array", + "items": { + "type": "string" + } + }, + "sbt_args": { + "type": "string" + }, + "crystal": { + "type": "array", + "items": { + "type": "string" + } + }, + "neko": { + "type": "string" + }, + "hxml": { + "type": "array", + "items": { + "type": "string" + } + }, + "smalltalk": { + "type": "array", + "items": { + "type": "string" + } + }, + "perl": { + "type": "array", + "items": { + "type": "string" + } + }, + "perl6": { + "type": "array", + "items": { + "type": "string" + } + }, + "d": { + "type": "array", + "items": { + "type": "string" + } + }, + "dart": { + "type": "array", + "items": { + "type": "string" + } + }, + "dart_task": { + "type": "array", + "items": { + "type": "object", + "properties": { + "test": { + "type": "string" + }, + "install_dartium": { + "type": "boolean" + }, + "xvfb": { + "type": "boolean" + }, + "dartanalyzer": { + "type": "boolean" + }, + "dartfmt": { + "type": "boolean" + } + } + } + }, + "ghc": { + "type": "array", + "items": { + "type": "string" + } + }, + "lein": { + "type": "string" + }, + "android": { + "type": "object", + "properties": { + "components": { + "type": "array", + "items": { + "type": "string" + } + }, + "licenses": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "node_js": { + "$ref": "#/definitions/stringOrNumberOrAcceptBothTypeAsArrayUnique" + }, + "compiler": { + "oneOf": [ + { + "type": "array", + "items": { + "enum": ["clang", "gcc"] + } + }, + { + "enum": ["clang", "gcc"] + } + ] + }, + "php": { + "$ref": "#/definitions/stringOrNumberOrAcceptBothTypeAsArrayUnique" + }, + "go": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "jdk": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "solution": { + "type": "string", + "description": "When the optional solution key is present, Travis will run NuGet package restore and build the given solution." + }, + "mono": { + "oneOf": [ + { + "enum": ["none"] + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "xcode_project": { + "type": "string" + }, + "xcode_workspace": { + "type": "string" + }, + "xcode_scheme": { + "type": "string" + }, + "xcode_sdk": { + "type": "string" + }, + "podfile": { + "type": "string", + "description": "By default, Travis CI will assume that your Podfile is in the root of the repository. If this is not the case, you can specify where the Podfile is" + }, + "python": { + "$ref": "#/definitions/stringOrNumberOrAcceptBothTypeAsArrayUnique" + }, + "elixir": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "rust": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "erlang": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "julia": { + "$ref": "#/definitions/stringOrNumberOrAcceptBothTypeAsArrayUnique" + }, + "opt_release": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "rvm": { + "$ref": "#/definitions/stringOrNumberOrAcceptBothTypeAsArrayUnique" + }, + "gemfile": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "bundler_args": { + "type": "string" + }, + "r": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "pandoc_version": { + "type": "string" + }, + "brew_packages": { + "type": "array", + "description": "A list of packages to install via brew. This option is ignored on non-OS X builds.", + "items": { + "type": "string" + } + }, + "r_binary_packages": { + "type": "array", + "items": { + "type": "string" + } + }, + "r_packages": { + "type": "array", + "items": { + "type": "string" + } + }, + "bioc_packages": { + "type": "array", + "items": { + "type": "string" + } + }, + "r_github_packages": { + "type": "array", + "items": { + "type": "string" + } + }, + "apt_packages": { + "type": "array", + "items": { + "type": "string" + } + }, + "cran": { + "type": "string", + "description": "CRAN mirror to use for fetching packages" + }, + "repos": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Dictionary of repositories to pass to options(repos)" + }, + "arch": { + "description": "The CPU Architecture to run the job on", + "oneOf": [ + { + "enum": [ + "amd64", + "x86_64", + "arm64", + "ppc64le", + "s390x", + "arm64-graviton2" + ] + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "enum": [ + "amd64", + "arm64", + "ppc64le", + "s390x", + "arm64-graviton2" + ] + } + } + ] + }, + "os": { + "description": "The operating system to run the job on", + "oneOf": [ + { + "enum": ["osx", "linux", "linux-ppc64le", "windows"] + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "enum": ["osx", "linux", "linux-ppc64le", "windows"] + } + } + ] + }, + "osx_image": { + "oneOf": [ + { + "$ref": "#/definitions/xcodeVersions" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/xcodeVersions" + } + } + ], + "default": "xcode9.4" + }, + "dist": { + "description": "The Ubuntu distribution to use", + "enum": ["precise", "trusty", "xenial", "bionic", "focal", "jammy"] + }, + "sudo": { + "enum": [true, false, "", "required", "enabled"], + "description": "sudo is deprecated" + }, + "addons": { + "type": "object", + "properties": { + "apt": { + "type": "object", + "description": "To install packages not included in the default container-based-infrastructure you need to use the APT addon, as sudo apt-get is not available", + "properties": { + "update": { + "type": "boolean", + "description": "To update the list of available packages" + }, + "sources": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "sourceline": { + "type": "string", + "description": "Key-value pairs which will be added to /etc/apt/sources.list" + }, + "key_url": { + "type": "string", + "description": "When APT sources require GPG keys, you can specify this with key_url" + } + }, + "required": ["sourceline"], + "additionalProperties": false + }, + { + "type": "string", + "description": "Alias defined in source whitelist" + } + ] + } + }, + "packages": { + "type": "array", + "description": "To install packages from the package whitelist before your custom build steps", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "hosts": { + "description": "If your build requires setting up custom hostnames, you can specify a single host or a list of them. Travis CI will automatically setup the hostnames in /etc/hosts for both IPv4 and IPv6.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "ssh_known_hosts": { + "$ref": "#/definitions/stringOrStringArrayUnique", + "description": "Travis CI can add entries to ~/.ssh/known_hosts prior to cloning your git repository, which is necessary if there are git submodules from domains other than github.com, gist.github.com, or ssh.github.com." + }, + "artifacts": { + "oneOf": [ + { + "enum": [true] + }, + { + "type": "object", + "properties": { + "s3_region": { + "type": "string" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "working_dir": { + "type": "string", + "description": "If you'd like to upload file from a specific directory, you can change your working directory " + }, + "debug": { + "type": "boolean", + "description": "If you'd like to see more detail about what the artifacts addon is doing" + } + } + } + ] + }, + "firefox": { + "description": "Firefox addon", + "anyOf": [ + { + "type": "string", + "enum": [ + "latest", + "latest-esr", + "latest-beta", + "latest-dev", + "latest-nightly", + "latest-unsigned" + ] + }, + { + "$ref": "#/definitions/nonEmptyString" + } + ] + }, + "chrome": { + "description": "Chrome addon", + "type": "string", + "enum": ["stable", "beta"] + }, + "rethinkdb": { + "description": "RethinkDB addon", + "type": "string" + }, + "postgresql": { + "description": "PostgreSQL addon", + "type": "string" + }, + "mariadb": { + "description": "MariaDB addon", + "type": "string" + }, + "sauce_connect": { + "description": "Sauce Connect addon", + "oneOf": [ + { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "access_key": { + "type": "string" + } + } + }, + { + "type": "boolean" + } + ] + }, + "sonarcloud": { + "description": "SonarCloud addon", + "type": "object", + "properties": { + "organization": { + "type": "string" + }, + "token": { + "$ref": "#/definitions/secretString" + } + } + }, + "coverity_scan": { + "description": "Coverity Scan addon", + "type": "object", + "properties": { + "project": { + "description": "GitHub project metadata", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "number" + }, + "description": { + "type": "string" + } + }, + "required": ["name"] + }, + "notification_email": { + "description": "Where email notification of build analysis results will be sent", + "type": "string" + }, + "build_command_prepend": { + "description": "Commands to prepare for build_command", + "type": "string" + }, + "build_command": { + "description": "The command that will be added as an argument to 'cov-build' to compile your project for analysis", + "type": "string" + }, + "branch_pattern": { + "description": "Pattern to match selecting branches that will run analysis. We recommend leaving this set to 'coverity_scan'", + "type": "string" + } + } + }, + "homebrew": { + "description": "Homebrew addon", + "type": "object", + "additionalProperties": false, + "properties": { + "taps": { + "$ref": "#/definitions/stringOrStringArrayUnique" + }, + "packages": { + "$ref": "#/definitions/stringOrStringArrayUnique" + }, + "casks": { + "$ref": "#/definitions/stringOrStringArrayUnique" + }, + "brewfile": { + "oneOf": [ + { + "$ref": "#/definitions/nonEmptyString" + }, + { + "type": "boolean", + "default": true + } + ] + }, + "update": { + "type": "boolean", + "default": true + } + } + }, + "srcclr": { + "description": "SourceClear addon", + "oneOf": [ + { + "type": "boolean", + "default": true + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "debug": { + "type": "boolean", + "default": true + } + } + } + ] + }, + "snaps": { + "description": "Snaps addon", + "oneOf": [ + { + "$ref": "#/definitions/nonEmptyString" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/nonEmptyString" + }, + { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "$ref": "#/definitions/nonEmptyString" + }, + "channel": { + "$ref": "#/definitions/nonEmptyString" + }, + "classic": { + "type": "boolean", + "description": "'classic:' is deprecated, use 'confinement:'" + }, + "confinement": { + "enum": ["classic", "devmode"] + } + } + } + ] + } + } + ] + }, + "browserstack": { + "description": "BrowserStack addon", + "type": "object", + "properties": { + "username": { + "$ref": "#/definitions/nonEmptyString" + }, + "access_key": { + "$ref": "#/definitions/possiblySecretString" + }, + "app_path": { + "$ref": "#/definitions/nonEmptyString" + }, + "proxyHost": { + "$ref": "#/definitions/nonEmptyString" + }, + "proxyPort": { + "$ref": "#/definitions/nonEmptyString" + }, + "proxyUser": { + "$ref": "#/definitions/nonEmptyString" + }, + "proxyPass": { + "$ref": "#/definitions/nonEmptyString" + }, + "forcelocal": { + "type": "boolean" + }, + "only": { + "$ref": "#/definitions/nonEmptyString" + } + } + } + }, + "additionalProperties": false + }, + "cache": { + "oneOf": [ + { + "enum": [false] + }, + { + "$ref": "#/definitions/cache" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/cache" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "directories": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, + { + "type": "object", + "properties": { + "directories": { + "type": "array", + "items": { + "type": "string" + } + }, + "timeout": { + "type": "number", + "description": "Upload timeout in seconds", + "default": 1800 + }, + "apt": { + "type": "boolean" + }, + "bundler": { + "type": "boolean" + }, + "cocoapods": { + "type": "boolean" + }, + "pip": { + "type": "boolean" + }, + "yarn": { + "type": "boolean" + }, + "ccache": { + "type": "boolean" + }, + "packages": { + "type": "boolean" + }, + "cargo": { + "type": "boolean" + }, + "npm": { + "type": "boolean" + } + }, + "additionalProperties": false + } + ] + }, + "services": { + "oneOf": [ + { + "$ref": "#/definitions/service" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/service" + } + } + ] + }, + "git": { + "type": "object", + "properties": { + "depth": { + "oneOf": [ + { + "type": "integer", + "description": "Set the git clone depth", + "default": 50 + }, + { + "enum": [false] + } + ] + }, + "quiet": { + "type": "boolean", + "description": "Travis CI clones repositories without the quiet flag (-q) by default. Enabling the quiet flag can be useful if you're trying to avoid log file size limits or even if you just don't need to include it." + }, + "submodules": { + "type": "boolean", + "description": "Control whether submodules should be cloned" + }, + "lfs_skip_smudge": { + "type": "boolean", + "description": "Skip fetching the git-lfs files during the initial git clone (equivalent to git lfs smudge --skip)," + }, + "clone": { + "type": "boolean", + "description": "In some work flows, like build stages, it might be beneficial to skip the automatic git clone step." + }, + "sparse_checkout": { + "$ref": "#/definitions/nonEmptyString", + "description": "Is a path to the existing file in the current repository with data you'd like to put into $GIT_DIR/info/sparse-checkout file of format described in Git documentation." + }, + "autocrlf": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": ["input"] + } + ], + "description": "Specify handling of line endings when cloning repository" + } + }, + "additionalProperties": false + }, + "branches": { + "type": "object", + "description": "Specify which branches to build", + "properties": { + "except": { + "type": "array", + "items": { + "type": "string" + } + }, + "only": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "env": { + "oneOf": [ + { + "$ref": "#/definitions/envVars" + }, + { + "type": "object", + "properties": { + "global": { + "$ref": "#/definitions/envVars" + }, + "matrix": { + "$ref": "#/definitions/envVars" + }, + "jobs": { + "$ref": "#/definitions/envVars" + } + }, + "additionalProperties": false + } + ] + }, + "before_install": { + "$ref": "#/definitions/step" + }, + "install": { + "$ref": "#/definitions/step" + }, + "before_script": { + "$ref": "#/definitions/step" + }, + "script": { + "$ref": "#/definitions/step" + }, + "before_cache": { + "$ref": "#/definitions/step" + }, + "after_success": { + "$ref": "#/definitions/step" + }, + "after_failure": { + "$ref": "#/definitions/step" + }, + "before_deploy": { + "$ref": "#/definitions/step" + }, + "deploy": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/deployment" + } + }, + { + "$ref": "#/definitions/deployment" + } + ] + }, + "after_deploy": { + "$ref": "#/definitions/step" + }, + "after_script": { + "$ref": "#/definitions/step" + } + } + }, + "deployment": { + "allOf": [ + { + "type": "object", + "properties": { + "on": { + "type": "object", + "properties": { + "tags": { + "description": "Tell Travis CI to only deploy on tagged commits", + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "branch": { + "type": "string" + }, + "all_branches": { + "type": "boolean" + }, + "skip_cleanup": { + "type": "boolean", + "description": "After your tests ran and before the release, Travis CI will clean up any additional files and changes you made. Maybe that is not what you want, as you might generate some artifacts that are supposed to be released, too." + }, + "repo": { + "type": "string" + }, + "condition": { + "type": "string", + "description": "if [[ ]]; then ; fi" + } + } + } + } + }, + { + "oneOf": [ + { + "type": "object", + "properties": { + "provider": { + "enum": ["script"] + }, + "script": { + "type": "string" + } + }, + "required": ["provider", "script"] + }, + { + "type": "object", + "properties": { + "provider": { + "enum": ["npm"] + }, + "email": { + "$ref": "#/definitions/possiblySecretString" + }, + "api_key": { + "$ref": "#/definitions/possiblySecretString" + }, + "api_token": { + "$ref": "#/definitions/possiblySecretString" + }, + "tag": { + "type": "string" + } + }, + "oneOf": [ + { + "required": ["provider", "email", "api_key"] + }, + { + "required": ["provider", "email", "api_token"] + } + ] + }, + { + "type": "object", + "properties": { + "provider": { + "enum": ["surge"] + }, + "project": { + "type": "string" + }, + "domain": { + "type": "string" + } + }, + "required": ["provider"] + }, + { + "type": "object", + "properties": { + "provider": { + "enum": ["releases"] + }, + "api_key": { + "$ref": "#/definitions/possiblySecretString" + }, + "user": { + "$ref": "#/definitions/possiblySecretString" + }, + "password": { + "$ref": "#/definitions/possiblySecretString" + }, + "file": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "file_glob": { + "type": "boolean" + }, + "overwrite": { + "type": "boolean", + "description": "If you need to overwrite existing files" + } + }, + "required": ["provider"] + }, + { + "type": "object", + "description": "deploy to heroku, to see https://docs.travis-ci.com/user/deployment/heroku/", + "properties": { + "provider": { + "enum": ["heroku"] + }, + "api_key": { + "description": "heroku auth token", + "anyOf": [ + { + "$ref": "#/definitions/possiblySecretString" + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/possiblySecretString" + } + } + ] + }, + "app": { + "oneOf": [ + { + "type": "string", + "description": "Deploy master branch to heroku app" + }, + { + "type": "object", + "description": "Deploy the different branch to the different heroku app", + "additionalProperties": { + "type": "string" + } + } + ] + }, + "run": { + "description": "to run a command on Heroku after a successful deploy", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "skip_cleanup": { + "type": "boolean", + "description": "Travis CI default will clean up any additional files and changes you made, you can by it to skip the clean up" + }, + "strategy": { + "enum": ["api", "git"], + "description": "Travis CI supports different mechanisms for deploying to Heroku: api is default" + } + }, + "required": ["provider", "api_key"] + }, + { + "type": "object", + "properties": { + "provider": { + "enum": ["s3"] + }, + "access_key_id": { + "$ref": "#/definitions/possiblySecretString" + }, + "secret_access_key": { + "$ref": "#/definitions/possiblySecretString" + }, + "bucket": { + "type": "string" + }, + "region": { + "type": "string" + }, + "skip_cleanup": { + "type": "boolean", + "default": false + }, + "acl": { + "enum": [ + "private", + "public_read", + "public_read_write", + "authenticated_read", + "bucket_owner_read", + "bucket_owner_full_control" + ] + }, + "local_dir": { + "type": "string" + }, + "upload-dir": { + "type": "string" + }, + "detect_encoding": { + "type": "boolean", + "default": false + }, + "default_text_charset": { + "type": "string" + }, + "cache_control": { + "type": "string" + }, + "expires": { + "type": "string" + }, + "endpoint": { + "type": "string" + } + }, + "required": [ + "provider", + "access_key_id", + "secret_access_key", + "bucket" + ] + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "not": { + "enum": [ + "script", + "npm", + "surge", + "releases", + "heroku", + "s3" + ] + } + } + }, + "required": ["provider"] + } + ] + } + ] + }, + "notificationObject": { + "webhooks": { + "type": "object", + "additionalProperties": false, + "properties": { + "disabled": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "urls": { + "oneOf": [ + { + "type": "string", + "format": "uri" + }, + { + "$ref": "#/definitions/secretString" + }, + { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "string", + "format": "uri" + }, + { + "$ref": "#/definitions/secretString" + } + ] + } + } + ] + }, + "on_success": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_failure": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_start": { + "$ref": "#/definitions/notificationFrequency", + "default": "never" + }, + "on_cancel": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_error": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + } + } + }, + "slack": { + "type": "object", + "additionalProperties": false, + "properties": { + "disabled": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "rooms": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/definitions/slackRoom" + } + }, + "on_pull_requests": { + "type": "boolean" + }, + "template": { + "$ref": "#/definitions/notRequiredNonEmptyStringOrArrayOfNonEmptyStrings" + }, + "on_success": { + "$ref": "#/definitions/notificationFrequency" + }, + "on_failure": { + "$ref": "#/definitions/notificationFrequency" + }, + "on_start": { + "$ref": "#/definitions/notificationFrequency", + "default": "never" + }, + "on_cancel": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_error": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + } + } + }, + "email": { + "type": "object", + "additionalProperties": false, + "properties": { + "disabled": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "recipients": { + "$ref": "#/definitions/possiblySecretStringOrPossiblySecretStringTypeArrayUnique" + }, + "on_success": { + "$ref": "#/definitions/notificationFrequency", + "default": "change" + }, + "on_failure": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_start": { + "$ref": "#/definitions/notificationFrequency", + "default": "never" + }, + "on_cancel": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_error": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + } + } + }, + "irc": { + "type": "object", + "additionalProperties": false, + "properties": { + "disabled": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "channels": { + "$ref": "#/definitions/possiblySecretStringOrPossiblySecretStringTypeArrayUnique" + }, + "channel_key": { + "$ref": "#/definitions/possiblySecretString" + }, + "nick": { + "$ref": "#/definitions/nonEmptyString" + }, + "password": { + "$ref": "#/definitions/possiblySecretString" + }, + "template": { + "$ref": "#/definitions/notRequiredNonEmptyStringOrArrayOfNonEmptyStrings" + }, + "on_success": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_failure": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_start": { + "$ref": "#/definitions/notificationFrequency", + "default": "never" + }, + "on_cancel": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_error": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "skip_join": { + "type": "boolean" + }, + "use_notice": { + "type": "boolean" + } + } + }, + "pushover": { + "type": "object", + "additionalProperties": false, + "properties": { + "disabled": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "api_key": { + "$ref": "#/definitions/possiblySecretString" + }, + "users": { + "$ref": "#/definitions/possiblySecretStringOrPossiblySecretStringTypeArrayUnique" + }, + "template": { + "$ref": "#/definitions/notRequiredNonEmptyStringOrArrayOfNonEmptyStrings" + }, + "on_success": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_failure": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_start": { + "$ref": "#/definitions/notificationFrequency", + "default": "never" + }, + "on_cancel": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_error": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + } + } + }, + "campfire": { + "type": "object", + "additionalProperties": false, + "properties": { + "disabled": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "rooms": { + "$ref": "#/definitions/possiblySecretStringOrPossiblySecretStringTypeArrayUnique" + }, + "template": { + "$ref": "#/definitions/nonEmptyStringOrArrayOfNonEmptyStrings" + }, + "on_success": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_failure": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_start": { + "$ref": "#/definitions/notificationFrequency", + "default": "never" + }, + "on_cancel": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_error": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + } + } + }, + "flowdock": { + "type": "object", + "additionalProperties": false, + "properties": { + "disabled": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "api_token": { + "$ref": "#/definitions/nonEmptyString" + }, + "on_success": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_failure": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_start": { + "$ref": "#/definitions/notificationFrequency", + "default": "never" + }, + "on_cancel": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_error": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + } + } + }, + "hipchat": { + "type": "object", + "additionalProperties": false, + "properties": { + "disabled": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "notify": { + "type": "boolean" + }, + "on_pull_requests": { + "type": "boolean" + }, + "rooms": { + "$ref": "#/definitions/possiblySecretStringOrPossiblySecretStringTypeArrayUnique" + }, + "format": { + "enum": ["html", "text"] + }, + "template": { + "$ref": "#/definitions/nonEmptyStringOrArrayOfNonEmptyStrings" + }, + "on_success": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_failure": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_start": { + "$ref": "#/definitions/notificationFrequency", + "default": "never" + }, + "on_cancel": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + }, + "on_error": { + "$ref": "#/definitions/notificationFrequency", + "default": "always" + } + } + } + }, + "import": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["source"], + "properties": { + "source": { + "$ref": "#/definitions/nonEmptyString", + "description": "The source to import build config from" + }, + "mode": { + "type": "string", + "enum": [ + "merge", + "deep_merge", + "deep_merge_append", + "deep_merge_prepend" + ], + "description": "How to merge the imported config into the target config (defaults to deep_merge_append)" + }, + "if": { + "$ref": "#/definitions/nonEmptyString", + "description": "Specifies a condition for the import" + } + } + }, + { + "$ref": "#/definitions/nonEmptyString" + } + ] + } + }, + "id": "https://json.schemastore.org/travis.json", + "title": "JSON schema for Travis CI configuration files" +} diff --git a/test/test_jsontoavro.py b/test/test_jsontoavro.py index 6efa8a2..3b93ad6 100644 --- a/test/test_jsontoavro.py +++ b/test/test_jsontoavro.py @@ -1,7 +1,7 @@ import os import sys from os import path, getcwd -import avro.schema +from fastavro.schema import load_schema import pytest from avrotize.jsonstoavro import convert_jsons_to_avro @@ -16,7 +16,7 @@ class TestJsonsToAvro(unittest.TestCase): def validate_avro_schema(self, avro_file_path): - avro.schema.parse(open(avro_file_path, "rb").read()) + load_schema(avro_file_path) def test_convert_address_jsons_to_avro(self): @@ -143,16 +143,16 @@ def test_convert_patternprops2_jsons_to_avro(self): self.validate_avro_schema(avro_path) # TODO: is causing a recursion error - # def test_convert_avro_avsc_jsons_to_avro(self): - # cwd = getcwd() - # jsons_path = path.join(cwd, "test", "jsons", "avro-avsc.json") - # avro_path = path.join(cwd, "test", "tmp", "avro-avsc.json.avsc") - # dir = os.path.dirname(avro_path) - # if not os.path.exists(dir): - # os.makedirs(dir) + def test_convert_avro_avsc_jsons_to_avro(self): + cwd = getcwd() + jsons_path = path.join(cwd, "test", "jsons", "avro-avsc.json") + avro_path = path.join(cwd, "test", "tmp", "avro-avsc.json.avsc") + dir = os.path.dirname(avro_path) + if not os.path.exists(dir): + os.makedirs(dir) - # convert_jsons_to_avro(jsons_path, avro_path) - #self.validate_avro_schema(avro_path) + convert_jsons_to_avro(jsons_path, avro_path) + self.validate_avro_schema(avro_path) def test_convert_clouidify_jsons_to_avro(self): cwd = getcwd() @@ -183,7 +183,7 @@ def test_convert_jfrog_pipelines_to_avro(self): if not os.path.exists(dir): os.makedirs(dir) convert_jsons_to_avro(jsons_path, avro_path) - #self.validate_avro_schema(avro_path) + self.validate_avro_schema(avro_path) def test_convert_kubernetes_definitions_jsons_to_avro(self): cwd = getcwd() @@ -195,6 +195,16 @@ def test_convert_kubernetes_definitions_jsons_to_avro(self): convert_jsons_to_avro(jsons_path, avro_path) #self.validate_avro_schema(avro_path) + def test_convert_travis_jsons_to_avro(self): + cwd = getcwd() + jsons_path = path.join(cwd, "test", "jsons", "travis.json") + avro_path = path.join(cwd, "test", "tmp", "travis.avsc") + dir = os.path.dirname(avro_path) + if not os.path.exists(dir): + os.makedirs(dir) + convert_jsons_to_avro(jsons_path, avro_path) + self.validate_avro_schema(avro_path) +