Skip to content

feat: add support for ListParam & MultiSelectInput #179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions samples/basic_params/functions/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@
default="/images/processed",
)

image_type = params.ListParam(
"IMAGE_TYPE",
label="convert image to preferred types",
description="The image types you'd like your source image to convert to.",
input=params.MultiSelectInput([
params.SelectOption(value="jpeg", label="jpeg"),
params.SelectOption(value="png", label="png"),
params.SelectOption(value="webp", label="webp"),
]),
default=["jpeg", "png"],
)

delete_original = params.BoolParam(
"DELETE_ORIGINAL_FILE",
label="delete the original file",
Expand Down Expand Up @@ -56,6 +68,7 @@ def resize_images(event: storage_fn.CloudEvent[storage_fn.StorageObjectData]):
This function will be triggered when a new object is created in the bucket.
"""
print("Got a new image:", event)
print("Selected image types:", image_type.value)
print("Selected bucket resource:", bucket.value)
print("Selected output location:", output_path.value)
print("Testing a not so secret api key:", image_resize_api_secret.value)
Expand Down
42 changes: 41 additions & 1 deletion src/firebase_functions/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""Module for params that can make Cloud Functions codebases generic."""

import abc as _abc
import json as _json
import dataclasses as _dataclasses
import os as _os
import re as _re
Expand Down Expand Up @@ -139,6 +140,18 @@ class SelectInput(_typing.Generic[_T]):
"""A list of user selectable options."""


@_dataclasses.dataclass(frozen=True)
class MultiSelectInput():
"""
Specifies that a Param's value should be determined by having the user select
a subset from a list of pre-canned options interactively at deploy-time.
Will result in errors if used on Params of type other than string[].
"""

options: list[SelectOption[str]]
"""A list of user selectable options."""


@_dataclasses.dataclass(frozen=True)
class TextInput:
"""
Expand Down Expand Up @@ -215,7 +228,8 @@ class Param(Expression[_T]):
deployments.
"""

input: TextInput | ResourceInput | SelectInput[_T] | None = None
input: TextInput | ResourceInput | SelectInput[
_T] | MultiSelectInput | None = None
"""
The type of input that is required for this param, e.g. TextInput.
"""
Expand Down Expand Up @@ -355,6 +369,32 @@ def value(self) -> bool:
return False


@_dataclasses.dataclass(frozen=True)
class ListParam(Param[list]):
"""A parameter as a list of strings."""

@property
def value(self) -> list[str]:
if _os.environ.get(self.name) is not None:
# If the environment variable starts with "[" and ends with "]",
# then assume it is a JSON array and try to parse it.
# (This is for Cloud Run (v2 Functions), the environment variable is a JSON array.)
if _os.environ[self.name].startswith("[") and _os.environ[
self.name].endswith("]"):
try:
return _json.loads(_os.environ[self.name])
except _json.JSONDecodeError:
return []
# Otherwise, split the string by commas.
# (This is for emulator & the Firebase CLI generated .env file, the environment
# variable is a comma-separated list.)
return list(filter(len, _os.environ[self.name].split(",")))
if self.default is not None:
return self.default.value if isinstance(
self.default, Expression) else self.default
return []


@_dataclasses.dataclass(frozen=True)
class _DefaultStringParam(StringParam):
"""
Expand Down
11 changes: 8 additions & 3 deletions src/firebase_functions/private/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ class ManifestStack:


def _param_input_to_spec(
param_input: _params.TextInput | _params.ResourceInput | _params.SelectInput
param_input: _params.TextInput | _params.ResourceInput |
_params.SelectInput | _params.MultiSelectInput
) -> dict[str, _typing.Any]:
if isinstance(param_input, _params.TextInput):
return {
Expand All @@ -204,9 +205,11 @@ def _param_input_to_spec(
},
}

if isinstance(param_input, _params.SelectInput):
if isinstance(param_input, (_params.MultiSelectInput, _params.SelectInput)):
key = "select" if isinstance(param_input,
_params.SelectInput) else "multiSelect"
return {
"select": {
key: {
"options": [{
key: value for key, value in {
"value": option.value,
Expand Down Expand Up @@ -242,6 +245,8 @@ def _param_to_spec(
spec_dict["type"] = "float"
elif isinstance(param, _params.SecretParam):
spec_dict["type"] = "secret"
elif isinstance(param, _params.ListParam):
spec_dict["type"] = "list"
elif isinstance(param, _params.StringParam):
spec_dict["type"] = "string"
else:
Expand Down
52 changes: 25 additions & 27 deletions tests/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,43 +68,41 @@
_params.FloatParam("FLOAT_TEST", immutable=True),
_params.SecretParam("SECRET_TEST"),
_params.StringParam("STRING_TEST"),
_params.ListParam("LIST_TEST", default=["1", "2", "3"]),
],
requiredAPIs=[{
"api": "test_api",
"reason": "testing"
}],
)
}])

full_stack_dict = {
"specVersion": "v1alpha1",
"endpoints": {
"test": full_endpoint_dict
},
"params": [
{
"name": "BOOL_TEST",
"type": "boolean",
"default": False,
},
{
"name": "INT_TEST",
"type": "int",
"description": "int_description"
},
{
"name": "FLOAT_TEST",
"type": "float",
"immutable": True,
},
{
"name": "SECRET_TEST",
"type": "secret"
},
{
"name": "STRING_TEST",
"type": "string"
},
],
"params": [{
"name": "BOOL_TEST",
"type": "boolean",
"default": False,
}, {
"name": "INT_TEST",
"type": "int",
"description": "int_description"
}, {
"name": "FLOAT_TEST",
"type": "float",
"immutable": True,
}, {
"name": "SECRET_TEST",
"type": "secret"
}, {
"name": "STRING_TEST",
"type": "string"
}, {
"default": ["1", "2", "3"],
"name": "LIST_TEST",
"type": "list"
}],
"requiredAPIs": [{
"api": "test_api",
"reason": "testing"
Expand Down
36 changes: 36 additions & 0 deletions tests/test_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,42 @@ def test_string_param_equality(self):
is False), "Failure, equality check returned False"


class TestListParams:
"""ListParam unit tests."""

def test_list_param_value(self):
"""Testing if list param correctly returns list values."""
environ["LIST_VALUE_TEST1"] = "item1,item2"
assert params.ListParam("LIST_VALUE_TEST1").value == ["item1","item2"], \
'Failure, params value != ["item1","item2"]'

def test_list_param_filter_empty_strings(self):
"""Testing if list param correctly returns list values wth empty strings excluded."""
environ["LIST_VALUE_TEST2"] = ",,item1,item2,,,item3,"
assert params.ListParam("LIST_VALUE_TEST2").value == ["item1","item2", "item3"], \
'Failure, params value != ["item1","item2", "item3"]'

def test_list_param_empty_default(self):
"""Testing if list param defaults to an empty list if no value and no default."""
assert params.ListParam("LIST_DEFAULT_TEST1").value == [], \
"Failure, params value is not an empty list"

def test_list_param_default(self):
"""Testing if list param defaults to the provided default value."""
assert (params.ListParam("LIST_DEFAULT_TEST2", default=["1", "2"]).value
== ["1", "2"]), \
'Failure, params default value != ["1", "2"]'

def test_list_param_equality(self):
"""Test list equality."""
assert (params.ListParam("LIST_TEST1",
default=["123"]).equals(["123"]).value
is True), "Failure, equality check returned False"
assert (params.ListParam("LIST_TEST2",
default=["456"]).equals(["123"]).value
is False), "Failure, equality check returned False"


class TestParamsManifest:
"""
Tests any created params are tracked for the purposes
Expand Down