Data Link for reading Enso_File (#9525)

- Closes #9282
This commit is contained in:
Radosław Waśko 2024-03-27 05:17:07 +01:00 committed by GitHub
parent 05715bdedf
commit af5354b869
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 137 additions and 25 deletions

View File

@ -640,6 +640,7 @@
dialect yet.][9435]
- [Make expand_to_rows, expand_column support Rows, Tables, Column data
types][9533]
- [Data Link for `Enso_File`.][9525]
[debug-shortcuts]:
https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug
@ -931,6 +932,7 @@
[9406]: https://github.com/enso-org/enso/pull/9406
[9435]: https://github.com/enso-org/enso/pull/9435
[9533]: https://github.com/enso-org/enso/pull/9533
[9525]: https://github.com/enso-org/enso/pull/9525
#### Enso Compiler

View File

@ -1,9 +1,8 @@
/** @file A dynamic wizard for creating an arbitrary type of Data Link. */
import * as React from 'react'
import Ajv from 'ajv/dist/2020'
import SCHEMA from '#/data/dataLinkSchema.json' assert { type: 'json' }
import * as dataLinkValidator from '#/data/dataLinkValidator'
import type * as jsonSchemaInput from '#/components/JSONSchemaInput'
import JSONSchemaInput from '#/components/JSONSchemaInput'
@ -15,9 +14,6 @@ import * as error from '#/utilities/error'
// =================
const DEFS: Record<string, object> = SCHEMA.$defs
// eslint-disable-next-line @typescript-eslint/naming-convention
const AJV = new Ajv({ formats: { 'enso-secret': true } })
AJV.addSchema(SCHEMA)
// ====================
// === getValidator ===
@ -26,7 +22,7 @@ AJV.addSchema(SCHEMA)
/** Get a known schema using a path.
* @throws {Error} when there is no schema present at the given path. */
function getValidator(path: string) {
return error.assert<(value: unknown) => boolean>(() => AJV.getSchema(path))
return error.assert<(value: unknown) => boolean>(() => dataLinkValidator.AJV.getSchema(path))
}
// =====================

View File

@ -6,12 +6,12 @@ import * as url from 'node:url'
import * as v from 'vitest'
import * as validateDataLink from '#/utilities/validateDataLink'
import * as dataLinkValidator from '#/data/dataLinkValidator'
v.test('correctly rejects invalid values as not matching the schema', () => {
v.expect(validateDataLink.validateDataLink({})).toBe(false)
v.expect(validateDataLink.validateDataLink('foobar')).toBe(false)
v.expect(validateDataLink.validateDataLink({ foo: 'BAR' })).toBe(false)
v.expect(dataLinkValidator.validateDataLink({})).toBe(false)
v.expect(dataLinkValidator.validateDataLink('foobar')).toBe(false)
v.expect(dataLinkValidator.validateDataLink({ foo: 'BAR' })).toBe(false)
})
/** Load and parse a data-link description. */
@ -22,7 +22,7 @@ function loadDataLinkFile(dataLinkPath: string): unknown {
/** Check if the given data-link description matches the schema, reporting any errors. */
function testSchema(json: unknown, fileName: string): void {
const validate = validateDataLink.validateDataLink
const validate = dataLinkValidator.validateDataLink
if (!validate(json)) {
v.assert.fail(`Failed to validate ${fileName}:\n${JSON.stringify(validate.errors, null, 2)}`)
}
@ -48,11 +48,19 @@ v.test('correctly validates example HTTP .datalink files with the schema', () =>
}
})
v.test('correctly validates example Enso_File .datalink files with the schema', () => {
const schemas = ['example-enso-file.datalink']
for (const schema of schemas) {
const json = loadDataLinkFile(path.resolve(BASE_DATA_LINKS_ROOT, schema))
testSchema(json, schema)
}
})
v.test('rejects invalid schemas (Base)', () => {
const invalidSchemas = ['example-http-format-invalid.datalink']
for (const schema of invalidSchemas) {
const json = loadDataLinkFile(path.resolve(BASE_DATA_LINKS_ROOT, schema))
v.expect(validateDataLink.validateDataLink(json)).toBe(false)
v.expect(dataLinkValidator.validateDataLink(json)).toBe(false)
}
})

View File

@ -5,6 +5,7 @@
"title": "Data Link",
"anyOf": [
{ "$ref": "#/$defs/S3DataLink" },
{ "$ref": "#/$defs/EnsoFileDataLink" },
{ "$ref": "#/$defs/HttpFetchDataLink" },
{ "$ref": "#/$defs/PostgresDataLink" },
{ "$ref": "#/$defs/SnowflakeDataLink" }
@ -110,6 +111,23 @@
},
"required": ["type", "libraryName", "uri", "auth"]
},
"EnsoFileDataLink": {
"title": "Enso File",
"type": "object",
"properties": {
"type": { "title": "Type", "const": "Enso_File", "type": "string" },
"libraryName": { "const": "Standard.Base" },
"path": {
"title": "Path",
"description": "Must start with \"enso://<organization-name>/\".",
"type": "string",
"pattern": "^enso://.+/.*$",
"format": "enso-file"
},
"format": { "title": "Format", "$ref": "#/$defs/Format" }
},
"required": ["type", "libraryName", "path"]
},
"HttpFetchDataLink": {
"$comment": "missing <headers with secrets> and <query string with secrets>",
"title": "HTTP Fetch",

View File

@ -1,4 +1,4 @@
/** @file Validation functions related to Data Links. */
/** @file AJV instance configured for data links. */
import type * as ajv from 'ajv/dist/2020'
import Ajv from 'ajv/dist/2020'
@ -7,8 +7,9 @@ import SCHEMA from '#/data/dataLinkSchema.json' assert { type: 'json' }
import * as error from '#/utilities/error'
// eslint-disable-next-line @typescript-eslint/naming-convention
const AJV = new Ajv({ formats: { 'enso-secret': true } })
export const AJV = new Ajv({ formats: { 'enso-secret': true, 'enso-file': true } })
AJV.addSchema(SCHEMA)
// This is a function, even though it does not contain function syntax.
// eslint-disable-next-line no-restricted-syntax
export const validateDataLink = error.assert<ajv.ValidateFunction>(() =>

View File

@ -3,6 +3,8 @@ import * as React from 'react'
import PenIcon from 'enso-assets/pen.svg'
import * as dataLinkValidator from '#/data/dataLinkValidator'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import * as authProvider from '#/providers/AuthProvider'
@ -25,7 +27,6 @@ import type AssetQuery from '#/utilities/AssetQuery'
import type AssetTreeNode from '#/utilities/AssetTreeNode'
import * as object from '#/utilities/object'
import * as permissions from '#/utilities/permissions'
import * as validateDataLink from '#/utilities/validateDataLink'
// =======================
// === AssetProperties ===
@ -60,7 +61,7 @@ export default function AssetProperties(props: AssetPropertiesProps) {
)
const [isDataLinkFetched, setIsDataLinkFetched] = React.useState(false)
const isDataLinkSubmittable = React.useMemo(
() => validateDataLink.validateDataLink(dataLinkValue),
() => dataLinkValidator.validateDataLink(dataLinkValue),
[dataLinkValue]
)
const setItem = React.useCallback(

View File

@ -2,6 +2,7 @@
import * as React from 'react'
import SCHEMA from '#/data/dataLinkSchema.json' assert { type: 'json' }
import * as dataLinkValidator from '#/data/dataLinkValidator'
import * as modalProvider from '#/providers/ModalProvider'
import * as textProvider from '#/providers/TextProvider'
@ -10,7 +11,6 @@ import DataLinkInput from '#/components/dashboard/DataLinkInput'
import Modal from '#/components/Modal'
import * as jsonSchema from '#/utilities/jsonSchema'
import * as validateDataLink from '#/utilities/validateDataLink'
// =================
// === Constants ===
@ -36,7 +36,7 @@ export default function UpsertDataLinkModal(props: UpsertDataLinkModalProps) {
const { getText } = textProvider.useText()
const [name, setName] = React.useState('')
const [value, setValue] = React.useState<NonNullable<unknown> | null>(INITIAL_DATA_LINK_VALUE)
const isValueSubmittable = React.useMemo(() => validateDataLink.validateDataLink(value), [value])
const isValueSubmittable = React.useMemo(() => dataLinkValidator.validateDataLink(value), [value])
const isSubmittable = name !== '' && isValueSubmittable
return (

View File

@ -1,3 +1,5 @@
private
from Standard.Base import all
import Standard.Base.Errors.Illegal_State.Illegal_State
import Standard.Base.System.Input_Stream.Input_Stream

View File

@ -126,7 +126,7 @@ type S3_File
with_input_stream self (open_options : Vector) action = if self.is_directory then Error.throw (Illegal_Argument.Error "S3 folders cannot be opened as a stream." self.uri) else
open_as_data_link = (open_options.contains Data_Link_Access.No_Follow . not) && (Data_Link.is_data_link self)
if open_as_data_link then Data_Link.read_data_link_as_stream self open_options action else
File_Access.ensure_only_allowed_options "with_output_stream" [File_Access.Read, Data_Link_Access.No_Follow] open_options <|
File_Access.ensure_only_allowed_options "with_input_stream" [File_Access.Read, Data_Link_Access.No_Follow] open_options <|
response_body = translate_file_errors self <| S3.get_object self.s3_path.bucket self.s3_path.key self.credentials delimiter=S3_Path.delimiter
response_body.with_stream action

View File

@ -0,0 +1,40 @@
private
import project.Any.Any
import project.Data.Text.Text
import project.Data.Vector.Vector
import project.Enso_Cloud.Enso_File.Enso_File
import project.Errors.Problem_Behavior.Problem_Behavior
import project.Nothing.Nothing
import project.System.File_Format.Auto_Detect
import project.System.Input_Stream.Input_Stream
from project.Enso_Cloud.Data_Link import Data_Link_With_Input_Stream, parse_format
from project.Enso_Cloud.Public_Utils import get_optional_field, get_required_field
## PRIVATE
type Enso_File_Data_Link
## PRIVATE
Value (path : Text) format_json
## PRIVATE
parse json -> Enso_File_Data_Link =
path = get_required_field "path" json expected_type=Text
format_json = get_optional_field "format" json
Enso_File_Data_Link.Value path format_json
## PRIVATE
read self (format = Auto_Detect) (on_problems : Problem_Behavior) =
effective_format = if format != Auto_Detect then format else parse_format self.format_json
self.as_file.read effective_format on_problems
## PRIVATE
as_file self -> Enso_File =
Enso_File.new self.path
## PRIVATE
with_input_stream self (open_options : Vector) (action : Input_Stream -> Any) -> Any =
self.as_file.with_input_stream open_options action
## PRIVATE
Data_Link_With_Input_Stream.from (that:Enso_File_Data_Link) = Data_Link_With_Input_Stream.Value that

View File

@ -1,3 +1,5 @@
private
import project.Any.Any
import project.Data.Text.Text
import project.Data.Vector.Vector
@ -5,12 +7,9 @@ import project.Errors.Problem_Behavior.Problem_Behavior
import project.Network.HTTP.HTTP
import project.Network.HTTP.HTTP_Method.HTTP_Method
import project.Network.HTTP.Request.Request
import project.Nothing.Nothing
import project.System.File.Data_Link_Access.Data_Link_Access
import project.System.File.File_Access.File_Access
import project.System.File_Format.Auto_Detect
import project.System.Input_Stream.Input_Stream
from project.Data.Boolean import Boolean, False, True
from project.Enso_Cloud.Data_Link import Data_Link_With_Input_Stream, parse_format, parse_secure_value
from project.Enso_Cloud.Public_Utils import get_optional_field, get_required_field
@ -38,7 +37,7 @@ type HTTP_Fetch_Data_Link
## PRIVATE
with_input_stream self (open_options : Vector) (action : Input_Stream -> Any) -> Any =
File_Access.ensure_only_allowed_options "with_output_stream" [File_Access.Read] open_options <|
File_Access.ensure_only_allowed_options "with_input_stream" [File_Access.Read] open_options <|
response = HTTP.new.request self.request
response.body.with_stream action

View File

@ -1,3 +1,5 @@
private
from Standard.Base import all
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Base.System.Input_Stream.Input_Stream

View File

@ -0,0 +1,19 @@
package org.enso.base.enso_cloud;
@org.openide.util.lookup.ServiceProvider(service = DataLinkSPI.class)
public class EnsoFileDataLinkSPI extends DataLinkSPI {
@Override
protected String getModuleName() {
return "Standard.Base.Enso_Cloud.Internal.Enso_File_Data_Link";
}
@Override
protected String getTypeName() {
return "Enso_File_Data_Link";
}
@Override
protected String getLinkTypeName() {
return "Enso_File";
}
}

View File

@ -6,7 +6,7 @@ import org.enso.base.enso_cloud.DataLinkSPI;
public class HTTPFetchDataLinkSPI extends DataLinkSPI {
@Override
protected String getModuleName() {
return "Standard.Base.Network.HTTP.HTTP_Fetch_Data_Link";
return "Standard.Base.Network.HTTP.Internal.HTTP_Fetch_Data_Link";
}
@Override

View File

@ -0,0 +1,5 @@
{
"type": "Enso_File",
"libraryName": "Standard.Base",
"path": "enso://PLACEHOLDER_ORG_NAME/test-directory/another.txt"
}

View File

@ -44,8 +44,27 @@ add_specs suite_builder setup:Cloud_Tests_Setup = setup.with_prepared_environmen
Test.expect_panic Illegal_Argument <|
Data_Link_Format.read_raw_config temp_file
group_builder.specify "should be able to read a local datalink to an Enso File" <|
datalink = replace_org_name_in_data_link (enso_project.data / "datalinks" / "example-enso-file.datalink")
datalink.read . should_equal "Hello Another!"
group_builder.specify "should be able to read a datalink in the Cloud to Enso File" <|
# TODO currently this link is created manually, later we should be generating it as part of the test
datalink = Enso_File.new "enso://"+Enso_User.current.name+"/TestDataLink-EnsoFile"
datalink.read . should_equal "Hello Another!"
main filter=Nothing =
setup = Cloud_Tests_Setup.prepare
suite = Test.build suite_builder->
add_specs suite_builder setup
suite.run_with_filter filter
## Reads the datalink as plain text and replaces the placeholder organization name.
replace_org_name_in_data_link base_file =
content = Data_Link_Format.read_raw_config base_file
org_name = Enso_User.current.name
new_content = content.replace "PLACEHOLDER_ORG_NAME" org_name
temp_file = File.create_temporary_file prefix=base_file.name suffix=base_file.extension
Data_Link_Format.write_raw_config temp_file new_content replace_existing=True . if_not_error temp_file