diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..5df108f --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +# FROM mcr.microsoft.com/devcontainers/cpp:1-ubuntu-24.04 +FROM mcr.microsoft.com/devcontainers/cpp:dev-ubuntu-24.04 + +ARG REINSTALL_CMAKE_VERSION_FROM_SOURCE="none" + +# Optionally install the cmake for vcpkg +COPY ./reinstall-cmake.sh /tmp/ + +RUN if [ "${REINSTALL_CMAKE_VERSION_FROM_SOURCE}" != "none" ]; then \ + chmod +x /tmp/reinstall-cmake.sh && /tmp/reinstall-cmake.sh ${REINSTALL_CMAKE_VERSION_FROM_SOURCE}; \ + fi \ + && rm -f /tmp/reinstall-cmake.sh + +# [Optional] Uncomment this section to install additional vcpkg ports. +# RUN su vscode -c "${VCPKG_ROOT}/vcpkg install clangd" + +# [Optional] Uncomment this section to install additional packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends +RUN apt update && export DEBIAN_FRONTEND=noninteractive && apt -y install clangd-19 clang-tidy-19 python3-pip +RUN update-alternatives --install /usr/bin/clangd clangd /usr/bin/clangd-19 100 +RUN update-alternatives --install /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-19 100 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..8399eb1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/cpp +{ + "name": "C++", + "build": { + "dockerfile": "Dockerfile" + }, + "containerEnv": { + "CLIPPY_BACKEND_PATH": "${containerWorkspaceFolder}/build/test" + }, + "customizations": { + "vscode": { + "extensions": [ + "llvm-vs-code-extensions.vscode-clangd", + "ms-python.python" + // add other extensions as needed + ] + } + }, + //"postCreateCommand": "pip install --break-system-packages -r ${containerWorkspaceFolder}/test/requirements.txt" +} \ No newline at end of file diff --git a/.devcontainer/reinstall-cmake.sh b/.devcontainer/reinstall-cmake.sh new file mode 100644 index 0000000..408b81d --- /dev/null +++ b/.devcontainer/reinstall-cmake.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +set -e + +CMAKE_VERSION=${1:-"none"} + +if [ "${CMAKE_VERSION}" = "none" ]; then + echo "No CMake version specified, skipping CMake reinstallation" + exit 0 +fi + +# Cleanup temporary directory and associated files when exiting the script. +cleanup() { + EXIT_CODE=$? + set +e + if [[ -n "${TMP_DIR}" ]]; then + echo "Executing cleanup of tmp files" + rm -Rf "${TMP_DIR}" + fi + exit $EXIT_CODE +} +trap cleanup EXIT + + +echo "Installing CMake..." +apt-get -y purge --auto-remove cmake +mkdir -p /opt/cmake + +architecture=$(dpkg --print-architecture) +case "${architecture}" in + arm64) + ARCH=aarch64 ;; + amd64) + ARCH=x86_64 ;; + *) + echo "Unsupported architecture ${architecture}." + exit 1 + ;; +esac + +CMAKE_BINARY_NAME="cmake-${CMAKE_VERSION}-linux-${ARCH}.sh" +CMAKE_CHECKSUM_NAME="cmake-${CMAKE_VERSION}-SHA-256.txt" +TMP_DIR=$(mktemp -d -t cmake-XXXXXXXXXX) + +echo "${TMP_DIR}" +cd "${TMP_DIR}" + +curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_BINARY_NAME}" -O +curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_CHECKSUM_NAME}" -O + +sha256sum -c --ignore-missing "${CMAKE_CHECKSUM_NAME}" +sh "${TMP_DIR}/${CMAKE_BINARY_NAME}" --prefix=/opt/cmake --skip-license + +ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake +ln -s /opt/cmake/bin/ctest /usr/local/bin/ctest diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5822106..df8076a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,16 +17,18 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi + if [ -f py/requirements.txt ]; then pip install -r py/requirements.txt; fi + if [ -f py/requirements-dev.txt ]; then pip install -r py/requirements-dev.txt; fi - - name: Lint with flake8 + - name: Lint with ruff run: | - flake8 src/clippy --count --show-source --statistics --max-line-length=120 + cd py + ruff check src/clippy --output-format=concise --statistics --line-length=120 - name: MyPy run: | + cd py mypy src/clippy --ignore-missing-imports - name: Install Boost @@ -40,33 +42,35 @@ jobs: # OPTIONAL: Specify a platform version # platform_version: 18.04 # OPTIONAL: Specify a custom install location - boost_install_dir: /home/runner/work/boost + # boost_install_dir: /home/runner/work/boost # OPTIONAL: Specify a toolset toolset: gcc # OPTIONAL: Specify an architecture # arch: x86 + # - name: Setup ccache + # uses: hendrikmuhs/ccache-action@v1.2 + # with: + # key: ${{ github.job }}-${{ runner.os }} + # max-size: 500M # limit cache size - name: Build backend id: build-backend env: BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} run: | echo BOOST_ROOT is $BOOST_ROOT /end/ - sudo apt install doxygen - TMPDIR=$(mktemp -d) - git clone https://github.com/LLNL/clippy-cpp --branch ${{ github.head_ref }} $TMPDIR || git clone https://github.com/LLNL/clippy-cpp --branch master $TMPDIR - mkdir -p $TMPDIR/build - cd $TMPDIR/build && cmake -DMODERN_CMAKE_BUILD_TESTING=ON -DBUILD_TESTING=ON -DBOOST_ROOT=$BOOST_ROOT .. && make - ls -l $TMPDIR/build/test - BACKEND=$TMPDIR/build/test - echo "BACKEND=$BACKEND" >> $GITHUB_ENV + sudo apt install -y doxygen + CPPDIR=cpp + mkdir -p $CPPDIR/build + cd $CPPDIR/build && cmake -DMODERN_CMAKE_BUILD_TESTING=ON -DBUILD_TESTING=ON -DBOOST_ROOT=$BOOST_ROOT .. && make + ls -l examples + BACKEND=$CPPDIR/build/examples + # echo "BACKEND=$BACKEND" >> $GITHUB_ENV - name: Pytest - env: - CLIPPY_BACKEND_PATH: ${{ env.BACKEND }} run: | - echo "backend = $BACKEND" - pytest . + cd test + ./run_tests.sh # - name: Pytest # run: | # coverage run --source clippy/ -m pytest && coverage report -m --fail-under 99 diff --git a/.gitignore b/.gitignore index e2bc832..f744d7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ __pycache__ **/build venv -*.core -build setup.cfg .vscode *.egg-info .coverage +**/attic +build +*/.cache +.cache/ \ No newline at end of file diff --git a/README.md b/README.md index bf45f98..25d35f9 100644 --- a/README.md +++ b/README.md @@ -23,51 +23,50 @@ environment – at the REPL, for example, or within a notebook – without the n complex HPC behavior and arcane job submission commands. ## Installation of Python Code +There are three ways to use the Python code: +1. Install from PyPI: ```console -$ pip install . +$ pip install llnl-clippy ``` -## Building C++ Examples on LC +2. Install from the cloned repository: +```console +$ cd py/src && pip install . +``` + +3. Via `PYTHONPATH` (see below) + +## Building C++ Examples ```console -$ . /usr/workspace/llamag/spack/share/spack/setup-env.sh -$ git clone https://github.com/LLNL/clippy-cpp.git -$ spack load gcc -$ spack load boost -$ cd clippy-cpp -$ mkdir build -$ cd build -$ cmake ../ +$ cd cpp && mkdir build && cd build +$ cmake .. $ make -$ cd ../.. #back to root project directory ``` -## Running Current C++ Examples +## Running Current C++ Examples (after building) +### From the repository root (using `PYTHONPATH`): ```python -$ CLIPPY_BACKEND_PATH=/path/to/binaries ipython3 +$ PYTHONPATH=py/src:$PYTHONPATH CLIPPY_BACKEND_PATH=$(pwd)/cpp/build/examples ipython -In [1]: from clippy import * +In [1]: from clippy import ExampleBag - ╭────────────────────────────────────╮ - │ It looks like you want to use HPC. │ - │ Would you like help with that? │ - ╰────────────────────────────────────╯ - ╲ - ╲ - ╭──╮ - ⊙ ⊙│╭ - ││ ││ - │╰─╯│ - ╰───╯ +In [2]: b = ExampleBag() + +In [3]: b.insert(5).insert(6).insert(7) # can chain methods +Out[3]: + +In [4]: b.insert(5).insert(8) +Out[4]: -In [2]: c = ClippyBag() # creates a bag datastructure. +In [5]: b.size() +Out[5]: 5 -In [3]: c.insert("foo").insert("bar") # mutating methods can be chained - -Out[3]: foo bar +In [6]: b.remove_if(b.value > 6) # removes 2 elements +Out[6]: -In [4]: c.size() # nonmutating methods return the appropriate output. -Out[4]: 2 +In [7]: b.size() +Out[7]: 3 ``` ## Authors diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt new file mode 100644 index 0000000..3579868 --- /dev/null +++ b/cpp/CMakeLists.txt @@ -0,0 +1,96 @@ +# Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +# Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: MIT + + +cmake_minimum_required(VERSION 3.26) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +set(ALLOW_DUPLICATE_CUSTOM_TARGETS TRUE) + +# Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24: +if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") +cmake_policy(SET CMP0135 NEW) +endif() + +project(CLIPPy + VERSION 0.5 + DESCRIPTION "Command Line Interface Plus Python" + LANGUAGES CXX) + +include(FetchContent) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Only do these if this is the main project, and not if it is included through add_subdirectory +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + + # Let's ensure -std=c++xx instead of -std=g++xx + set(CMAKE_CXX_EXTENSIONS OFF) + + # Let's nicely support folders in IDE's + set_property(GLOBAL PROPERTY USE_FOLDERS ON) + + # Testing only available if this is the main app + # Note this needs to be done in the main CMakeLists + # since it calls enable_testing, which must be in the + # main CMakeLists. + # include(CTest) + + # Docs only available if this is the main app + find_package(Doxygen) + if(Doxygen_FOUND) + #add_subdirectory(docs) + else() + message(STATUS "Doxygen not found, not building docs") + endif() +endif() + + +# +# Boost +# Download and build Boost::json +set(BOOST_URL + "https://github.com/boostorg/boost/releases/download/boost-1.87.0/boost-1.87.0-cmake.tar.gz" + CACHE STRING "URL to fetch Boost tarball") + + +set(BOOST_INCLUDE_LIBRARIES json lexical_cast range) +set(BUILD_SHARED_LIBS ON) +FetchContent_Declare( + Boost + URL ${BOOST_URL}) +FetchContent_MakeAvailable(Boost) + + +# +# JSONLogic +set(Boost_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/boost-install) # needed for jsonlogic + +FetchContent_Declare(jsonlogic + GIT_REPOSITORY https://github.com/LLNL/jsonlogic.git + GIT_TAG v0.2.0 + SOURCE_SUBDIR cpp +) +# set(jsonlogic_INCLUDE_DIR ${jsonlogic_SOURCE_DIR}/cpp/include/jsonlogic) +FetchContent_MakeAvailable(jsonlogic) +message(STATUS "jsonlogic source dir: ${jsonlogic_SOURCE_DIR}") + + + +### Require out-of-source builds +file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" LOC_PATH) +if(EXISTS "${LOC_PATH}") + message(FATAL_ERROR "You cannot build in a source directory (or any directory with a CMakeLists.txt file). Please make a build subdirectory. Feel free to remove CMakeCache.txt and CMakeFiles.") +endif() + +include_directories("${PROJECT_SOURCE_DIR}/include") + +option(TEST_WITH_SLURM "Run tests with Slurm" OFF) + +# Testing & examples are only available if this is the main app +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + add_subdirectory(examples) +endif() diff --git a/cpp/CMakePresets.json b/cpp/CMakePresets.json new file mode 100644 index 0000000..12fc7ce --- /dev/null +++ b/cpp/CMakePresets.json @@ -0,0 +1,11 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "cfg", + "generator": "Ninja", + "sourceDir": "src", + "binaryDir": "build" + } + ] +} \ No newline at end of file diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt new file mode 100644 index 0000000..39c6759 --- /dev/null +++ b/cpp/examples/CMakeLists.txt @@ -0,0 +1,30 @@ +# Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +# Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: MIT + +# +# This function adds a test. +# +function ( add_test class_name method_name ) + set(source "${method_name}.cpp") + set(target "${class_name}_${method_name}") + add_executable(${target} ${source}) + # target_include_directories(${target} PRIVATE ${Boost_INCLUDE_DIRS}) + include_directories(${CMAKE_CURRENT_SOURCE_DIR} include/) + set_target_properties(${target} PROPERTIES OUTPUT_NAME "${method_name}" ) + target_include_directories(${target} PRIVATE + ${PROJECT_SOURCE_DIR}/include + ${BOOST_INCLUDE_DIRS} + include/ + ${jsonlogic_SOURCE_DIR}/cpp/include + ) + target_link_libraries(${target} PRIVATE Boost::json) +endfunction() + + +add_subdirectory(ExampleBag) +add_subdirectory(ExampleSet) +add_subdirectory(ExampleFunctions) +add_subdirectory(ExampleSelector) +add_subdirectory(ExampleGraph) diff --git a/cpp/examples/ExampleBag/CMakeLists.txt b/cpp/examples/ExampleBag/CMakeLists.txt new file mode 100644 index 0000000..5343b74 --- /dev/null +++ b/cpp/examples/ExampleBag/CMakeLists.txt @@ -0,0 +1,11 @@ +add_test(ExampleBag __init__) +add_test(ExampleBag __str__) +add_test(ExampleBag insert) +add_test(ExampleBag remove) +add_test(ExampleBag remove_if) +add_test(ExampleBag size) +add_custom_command( + TARGET ExampleBag_size POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/meta.json + ${CMAKE_CURRENT_BINARY_DIR}/meta.json) \ No newline at end of file diff --git a/cpp/examples/ExampleBag/__init__.cpp b/cpp/examples/ExampleBag/__init__.cpp new file mode 100644 index 0000000..7d7bd85 --- /dev/null +++ b/cpp/examples/ExampleBag/__init__.cpp @@ -0,0 +1,27 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__init__"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Initializes an ExampleBag of strings"}; + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + std::vector the_bag; + clip.set_state(state_name, the_bag); + + return 0; +} diff --git a/cpp/examples/ExampleBag/__str__.cpp b/cpp/examples/ExampleBag/__str__.cpp new file mode 100644 index 0000000..f618bf7 --- /dev/null +++ b/cpp/examples/ExampleBag/__str__.cpp @@ -0,0 +1,38 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__str__"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Str method for ExampleBag"}; + + clip.add_required_state>(state_name, + "Internal container"); + + clip.returns("String of data."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_bag = clip.get_state>(state_name); + clip.set_state(state_name, the_bag); + + std::stringstream sstr; + for (auto item : the_bag) { + sstr << item << " "; + } + clip.to_return(sstr.str()); + + return 0; +} diff --git a/cpp/examples/ExampleBag/insert.cpp b/cpp/examples/ExampleBag/insert.cpp new file mode 100644 index 0000000..b84cbd8 --- /dev/null +++ b/cpp/examples/ExampleBag/insert.cpp @@ -0,0 +1,31 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include + +static const std::string class_name = "ExampleBag"; +static const std::string method_name = "insert"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Inserts a string into an ExampleBag"}; + clip.add_required("item", "Item to insert"); + clip.add_required_state>(state_name, + "Internal container"); + clip.returns_self(); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto item = clip.get("item"); + auto the_bag = clip.get_state>(state_name); + the_bag.push_back(item); + clip.set_state(state_name, the_bag); + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleBag/meta.json b/cpp/examples/ExampleBag/meta.json new file mode 100644 index 0000000..6992f16 --- /dev/null +++ b/cpp/examples/ExampleBag/meta.json @@ -0,0 +1,6 @@ +{ + "__doc__" : "A bag data structure", + "initial_selectors" : { + "value" : "A value in the container" + } +} \ No newline at end of file diff --git a/cpp/examples/ExampleBag/remove.cpp b/cpp/examples/ExampleBag/remove.cpp new file mode 100644 index 0000000..450f53e --- /dev/null +++ b/cpp/examples/ExampleBag/remove.cpp @@ -0,0 +1,44 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "remove"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + + clippy::clippy clip{method_name, "Removes a string from a ExampleBag"}; + clip.add_required("item", "Item to remove"); + clip.add_optional("all", "Remove all?", false); + clip.add_required_state>(state_name, + "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto item = clip.get("item"); + bool all = clip.get("all"); + auto the_bag = clip.get_state>(state_name); + if (all) { + the_bag.remove(item); + } else { + auto itr = std::find(the_bag.begin(), the_bag.end(), item); + if (itr != the_bag.end()) { + the_bag.erase(itr); + } + } + clip.set_state(state_name, the_bag); + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleBag/remove_if.cpp b/cpp/examples/ExampleBag/remove_if.cpp new file mode 100644 index 0000000..c2ed32b --- /dev/null +++ b/cpp/examples/ExampleBag/remove_if.cpp @@ -0,0 +1,47 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +// #include + +namespace boostjsn = boost::json; + +static const std::string class_name = "nClippyExample"; +static const std::string method_name = "remove_if"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Removes a string from an ExampleBag"}; + clip.add_required("expression", "Remove If Expression"); + clip.add_required_state>(state_name, "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto expression = clip.get("expression"); + auto the_bag = clip.get_state>(state_name); + + // + // Expression here + jsonlogic::logic_rule jlrule = jsonlogic::create_logic(expression["rule"]); + + auto apply_jl = [&jlrule](int value) { + boostjsn::object data; + data["value"] = value; + auto res = jlrule.apply(jsonlogic::json_accessor(data)); + return jsonlogic::unpack_value(res); + }; + + the_bag.remove_if(apply_jl); + + clip.set_state(state_name, the_bag); + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleBag/size.cpp b/cpp/examples/ExampleBag/size.cpp new file mode 100644 index 0000000..2ab35ab --- /dev/null +++ b/cpp/examples/ExampleBag/size.cpp @@ -0,0 +1,30 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "size"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Returns the size of the bag"}; + + clip.returns("Size of bag."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_bag = clip.get_state>(state_name); + + clip.to_return(the_bag.size()); + return 0; +} diff --git a/cpp/examples/ExampleFunctions/CMakeLists.txt b/cpp/examples/ExampleFunctions/CMakeLists.txt new file mode 100644 index 0000000..9a4e746 --- /dev/null +++ b/cpp/examples/ExampleFunctions/CMakeLists.txt @@ -0,0 +1,12 @@ +add_test(ExampleFunctions missing_version) +add_test(ExampleFunctions old_version) +add_test(ExampleFunctions returns_string) +add_test(ExampleFunctions returns_int) +add_test(ExampleFunctions returns_bool) +add_test(ExampleFunctions returns_vec_int) +add_test(ExampleFunctions returns_dict) +add_test(ExampleFunctions call_with_string) +add_test(ExampleFunctions call_with_optional_string) +add_test(ExampleFunctions pass_by_reference_dict) +add_test(ExampleFunctions pass_by_reference_vector) +add_test(ExampleFunctions throws_error) diff --git a/cpp/examples/ExampleFunctions/call_with_optional_string.cpp b/cpp/examples/ExampleFunctions/call_with_optional_string.cpp new file mode 100644 index 0000000..596de4d --- /dev/null +++ b/cpp/examples/ExampleFunctions/call_with_optional_string.cpp @@ -0,0 +1,20 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("call_with_optional_string", "Call With Optional String"); + clip.add_optional("name", "Optional String", "World"); + clip.returns("Returns a string"); + if (clip.parse(argc, argv)) { + return 0; + } + + auto name = clip.get("name"); + + clip.to_return(std::string("Howdy, ") + name); + return 0; +} diff --git a/cpp/examples/ExampleFunctions/call_with_string.cpp b/cpp/examples/ExampleFunctions/call_with_string.cpp new file mode 100644 index 0000000..830d718 --- /dev/null +++ b/cpp/examples/ExampleFunctions/call_with_string.cpp @@ -0,0 +1,18 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("call_with_string", "Call With String"); + clip.add_required("name", "Required String"); + clip.returns("Returns a string"); + if (clip.parse(argc, argv)) { return 0; } + + auto name = clip.get("name"); + + clip.to_return(std::string("Howdy, ") + name); + return 0; +} diff --git a/cpp/examples/ExampleFunctions/missing_version.cpp b/cpp/examples/ExampleFunctions/missing_version.cpp new file mode 100644 index 0000000..c10d972 --- /dev/null +++ b/cpp/examples/ExampleFunctions/missing_version.cpp @@ -0,0 +1,7 @@ +#include + +int main(int argc, char **arv) { + std::cout + << R"json({"method_name":"missing_version","desc":"Tests returning a string - missing a version spec","returns":{"desc":"The string"}})json"; + return 0; +} diff --git a/cpp/examples/ExampleFunctions/old_version.cpp b/cpp/examples/ExampleFunctions/old_version.cpp new file mode 100644 index 0000000..92de8e8 --- /dev/null +++ b/cpp/examples/ExampleFunctions/old_version.cpp @@ -0,0 +1,7 @@ +#include + +int main(int argc, char **arv) { + std::cout + << R"json({"method_name":"old_version","desc":"Tests returning a string - outdated version spec","version":"0.1.0","returns":{"desc":"The string"}})json"; + return 0; +} diff --git a/cpp/examples/ExampleFunctions/pass_by_reference_dict.cpp b/cpp/examples/ExampleFunctions/pass_by_reference_dict.cpp new file mode 100644 index 0000000..983d2f0 --- /dev/null +++ b/cpp/examples/ExampleFunctions/pass_by_reference_dict.cpp @@ -0,0 +1,18 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("pass_by_reference_dict", "Call with dict"); + clip.add_required>("debug_info", "Required dict"); + + if (clip.parse(argc, argv)) { return 0; } + + std::map m = {{"dummy_key", "dummy_value"}}; + + clip.overwrite_arg("debug_info", m); + return 0; +} diff --git a/cpp/examples/ExampleFunctions/pass_by_reference_vector.cpp b/cpp/examples/ExampleFunctions/pass_by_reference_vector.cpp new file mode 100644 index 0000000..3d96a6b --- /dev/null +++ b/cpp/examples/ExampleFunctions/pass_by_reference_vector.cpp @@ -0,0 +1,18 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("pass_by_reference_vector", "Call with vector"); + clip.add_required>("vec", "Required vector"); + + if (clip.parse(argc, argv)) { return 0; } + + std::vector vec = {5,4,3,2,1}; + + clip.overwrite_arg("vec", vec); + return 0; +} diff --git a/cpp/examples/ExampleFunctions/returns_bool.cpp b/cpp/examples/ExampleFunctions/returns_bool.cpp new file mode 100644 index 0000000..bd12e6c --- /dev/null +++ b/cpp/examples/ExampleFunctions/returns_bool.cpp @@ -0,0 +1,16 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("returns_bool", "Tests returning a bool"); + clip.returns("The bool"); + if (clip.parse(argc, argv)) { return 0; } + + + clip.to_return(true); + return 0; +} diff --git a/cpp/examples/ExampleFunctions/returns_dict.cpp b/cpp/examples/ExampleFunctions/returns_dict.cpp new file mode 100644 index 0000000..f8cf485 --- /dev/null +++ b/cpp/examples/ExampleFunctions/returns_dict.cpp @@ -0,0 +1,21 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include + +int main(int argc, char **argv) { + clippy::clippy clip("returns_dict", "Tests returning a dict"); + clip.returns>("The Dict"); + if (clip.parse(argc, argv)) { + return 0; + } + + std::unordered_map m1{{"a", 1}, {"b", 2}, {"c", 3}}; + // std::unordered_map> + // big_m; + clip.to_return(m1); + return 0; +} diff --git a/cpp/examples/ExampleFunctions/returns_int.cpp b/cpp/examples/ExampleFunctions/returns_int.cpp new file mode 100644 index 0000000..ba2a9cb --- /dev/null +++ b/cpp/examples/ExampleFunctions/returns_int.cpp @@ -0,0 +1,16 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("returns_int", "Tests returning a int"); + clip.returns("The Int"); + if (clip.parse(argc, argv)) { return 0; } + + + clip.to_return(size_t(42)); + return 0; +} diff --git a/cpp/examples/ExampleFunctions/returns_string.cpp b/cpp/examples/ExampleFunctions/returns_string.cpp new file mode 100644 index 0000000..6a09206 --- /dev/null +++ b/cpp/examples/ExampleFunctions/returns_string.cpp @@ -0,0 +1,16 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("returns_string", "Tests returning a string"); + clip.returns("The string"); + if (clip.parse(argc, argv)) { return 0; } + + + clip.to_return(std::string("asdf")); + return 0; +} diff --git a/cpp/examples/ExampleFunctions/returns_vec_int.cpp b/cpp/examples/ExampleFunctions/returns_vec_int.cpp new file mode 100644 index 0000000..be61e94 --- /dev/null +++ b/cpp/examples/ExampleFunctions/returns_vec_int.cpp @@ -0,0 +1,16 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("returns_vec_int", "Tests returning a vector of int"); + clip.returns>("The vec"); + if (clip.parse(argc, argv)) { return 0; } + + std::vector to_return = {0,1,2,3,4,5}; + clip.to_return(to_return); + return 0; +} diff --git a/cpp/examples/ExampleFunctions/throws_error.cpp b/cpp/examples/ExampleFunctions/throws_error.cpp new file mode 100644 index 0000000..596e1a1 --- /dev/null +++ b/cpp/examples/ExampleFunctions/throws_error.cpp @@ -0,0 +1,16 @@ +// Copyright 2019 Lawrence Livermore National Security, LLC and other Clippy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("throws_error", "Always throws errors"); + if (clip.parse(argc, argv)) { + return 0; + } + + throw std::runtime_error("I'm Grumpy!"); + return 0; +} diff --git a/cpp/examples/ExampleGraph/CMakeLists.txt b/cpp/examples/ExampleGraph/CMakeLists.txt new file mode 100644 index 0000000..594c5ab --- /dev/null +++ b/cpp/examples/ExampleGraph/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_BUILD_TYPE Debug) + + +add_test(ExampleGraph __init__) +add_test(ExampleGraph __str__) +# add_test(ExampleGraph assign) +# add_test(ExampleGraph dump) +add_test(ExampleGraph dump2) +add_test(ExampleGraph add_edge) +add_test(ExampleGraph add_node) +add_test(ExampleGraph nv) +add_test(ExampleGraph ne) +add_test(ExampleGraph degree) +add_test(ExampleGraph add_series) +add_test(ExampleGraph connected_components) +add_test(ExampleGraph drop_series) +add_test(ExampleGraph copy_series) +add_test(ExampleGraph series_str) +add_test(ExampleGraph extrema) +add_test(ExampleGraph count) +add_custom_command( + TARGET ExampleGraph_nv POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/meta.json + ${CMAKE_CURRENT_BINARY_DIR}/meta.json) diff --git a/cpp/examples/ExampleGraph/__init__.cpp b/cpp/examples/ExampleGraph/__init__.cpp new file mode 100644 index 0000000..5b8f8c0 --- /dev/null +++ b/cpp/examples/ExampleGraph/__init__.cpp @@ -0,0 +1,31 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "examplegraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__init__"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Initializes an ExampleGraph"}; + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + // examplegraph needs to be convertible to json + examplegraph::examplegraph the_graph; + clip.set_state(state_name, the_graph); + std::map selectors; + clip.set_state(sel_state_name, selectors); + + return 0; +} diff --git a/cpp/examples/ExampleGraph/__str__.cpp b/cpp/examples/ExampleGraph/__str__.cpp new file mode 100644 index 0000000..7a6a895 --- /dev/null +++ b/cpp/examples/ExampleGraph/__str__.cpp @@ -0,0 +1,39 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "examplegraph.hpp" +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__str__"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Str method for ExampleGraph"}; + + clip.add_required_state(state_name, + "Internal container"); + + clip.returns("String of data."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_graph = clip.get_state(state_name); + clip.set_state(state_name, the_graph); + + std::stringstream sstr; + sstr << "Graph with " << the_graph.nv() << " nodes and " << the_graph.ne() + << " edges"; + + clip.to_return(sstr.str()); + + return 0; +} diff --git a/cpp/examples/ExampleGraph/add.cpp b/cpp/examples/ExampleGraph/add.cpp new file mode 100644 index 0000000..481b290 --- /dev/null +++ b/cpp/examples/ExampleGraph/add.cpp @@ -0,0 +1,57 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Adds a subselector"}; + clip.add_required("selector", "Parent Selector"); + clip.add_required("subname", "Description of new selector"); + clip.add_optional("desc", "Description", "EMPTY DESCRIPTION"); + clip.add_required_state>( + "selector_state", "Internal container"); + + if (clip.parse(argc, argv)) { + return 0; + } + + std::map sstate; + if (clip.has_state("selector_state")) { + sstate = + clip.get_state>("selector_state"); + } + + auto jo = clip.get("selector"); + std::string subname = clip.get("subname"); + std::string desc = clip.get("desc"); + + std::string parentname; + try { + if (jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + parentname = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + sstate[parentname + "." + subname] = desc; + + clip.set_state("selector_state", sstate); + clip.update_selectors(sstate); + clip.return_self(); + + return 0; +} diff --git a/cpp/examples/ExampleGraph/add_edge.cpp b/cpp/examples/ExampleGraph/add_edge.cpp new file mode 100644 index 0000000..18a11b2 --- /dev/null +++ b/cpp/examples/ExampleGraph/add_edge.cpp @@ -0,0 +1,35 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "examplegraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add_edge"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Inserts (src, dst) into an ExampleGraph"}; + clip.add_required("src", "source node"); + clip.add_required("dst", "dest node"); + clip.add_required_state(state_name, + "Internal container"); + clip.returns_self(); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto src = clip.get("src"); + auto dst = clip.get("dst"); + auto the_graph = clip.get_state(state_name); + the_graph.add_edge(src, dst); + clip.set_state(state_name, the_graph); + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleGraph/add_node.cpp b/cpp/examples/ExampleGraph/add_node.cpp new file mode 100644 index 0000000..b03638d --- /dev/null +++ b/cpp/examples/ExampleGraph/add_node.cpp @@ -0,0 +1,33 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "examplegraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add_node"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Inserts a node into an ExampleGraph"}; + clip.add_required("node", "node to insert"); + clip.add_required_state(state_name, + "Internal container"); + clip.returns_self(); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto node = clip.get("node"); + auto the_graph = clip.get_state(state_name); + the_graph.add_node(node); + clip.set_state(state_name, the_graph); + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleGraph/add_series.cpp b/cpp/examples/ExampleGraph/add_series.cpp new file mode 100644 index 0000000..9c58ff0 --- /dev/null +++ b/cpp/examples/ExampleGraph/add_series.cpp @@ -0,0 +1,79 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include + +#include "clippy/clippy.hpp" +#include "clippy/selector.hpp" +#include "examplegraph.hpp" + +namespace boostjsn = boost::json; + +static const std::string method_name = "add_series"; +static const std::string graph_state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Adds a subselector"}; + clip.add_required("parent_sel", "Parent Selector"); + clip.add_required("sub_sel", "Name of new selector"); + clip.add_optional("desc", "Description of new selector", ""); + + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.add_required_state( + graph_state_name, "Internal state for the graph"); + + if (clip.parse(argc, argv)) { + return 0; + } + + std::string parstr = clip.get("parent_sel"); + auto parsel = selector{parstr}; + auto subsel = clip.get("sub_sel"); + auto desc = clip.get("desc"); + + std::string fullname = parstr + "." + subsel; + + // std::map selectors; + auto the_graph = clip.get_state(graph_state_name); + + if (parsel.headeq("edge")) { + if (the_graph.has_edge_series(subsel)) { + std::cerr << "!! ERROR: Selector name already exists in edge table !!" + << std::endl; + exit(-1); + } + } else if (parsel.headeq("node")) { + if (the_graph.has_node_series(subsel)) { + std::cerr << "!! ERROR: Selector name already exists in node table !!" + << std::endl; + exit(-1); + } + } else { + std::cerr + << "((!! ERROR: Parent must be either \"edge\" or \"node\" (received " + << parstr << ") !!)"; + exit(-1); + } + + if (clip.has_state(sel_state_name)) { + auto selectors = + clip.get_state>(sel_state_name); + if (selectors.contains(fullname)) { + std::cerr << "Warning: Selector name already exists; ignoring" + << std::endl; + } else { + selectors[fullname] = desc; + clip.set_state(sel_state_name, selectors); + clip.update_selectors(selectors); + clip.return_self(); + } + } + + return 0; +} diff --git a/cpp/examples/ExampleGraph/assign.cpp b/cpp/examples/ExampleGraph/assign.cpp new file mode 100644 index 0000000..9e958d4 --- /dev/null +++ b/cpp/examples/ExampleGraph/assign.cpp @@ -0,0 +1,341 @@ + + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "examplegraph.hpp" +#include "where.cpp" + +static const std::string method_name = "assign"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +static const std::string always_true = R"({"rule":{"==":[1,1]}})"; +static const std::string never_true = R"({"rule":{"==":[2,1]}})"; + +static const boost::json::object always_true_obj = + boost::json::parse(always_true).as_object(); + +using variants = + std::variant; + +std::optional obj_to_val(boost::json::object expr, + std::string extract = "var") { + if (expr["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + return std::nullopt; + } + + boost::json::value v; + v = expr["rule"].as_object()[extract]; + return v; +} +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Populates a column with a value"}; + clip.add_required( + "selector", + "Existing selector name into which the value will be written"); + + clip.add_required("value", "the value to assign"); + + clip.add_optional("where", "where filter", + always_true_obj); + + clip.add_optional("desc", "Description of the series", ""); + + clip.add_required_state(state_name, + "Internal container"); + clip.add_required_state>( + sel_state_name, "Internal container for selectors"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto sel_json = clip.get("selector"); + auto sel_str_opt = obj_to_val(sel_json); + if (!sel_str_opt.has_value()) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + std::string sel_str = sel_str_opt.value().as_string().c_str(); + + auto sel = selector(sel_str); + bool is_node_sel = sel.headeq("node"); + if (!is_node_sel && !sel.headeq("edge")) { + std::cerr << "Selector must start with \"node\" or \"edge\"" << std::endl; + return 1; + } + + auto sel_tail_opt = sel.tail(); + if (!sel_tail_opt.has_value()) { + std::cerr << "Selector must have a tail" << std::endl; + return 1; + } + + selector subsel = sel_tail_opt.value(); + auto the_graph = clip.get_state(state_name); + + auto selectors = + clip.get_state>(sel_state_name); + if (!selectors.contains(sel)) { + std::cerr << "Selector not found" << std::endl; + return 1; + } + + auto desc = clip.get("desc"); + + auto val = clip.get("value"); + + auto where_exp = clip.get("where"); + + if (where_exp["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << "!! ERROR in where statement !!" << std::endl; + exit(-1); + } + + boost::json::object submission_data; + + std::cerr << "val = " << val << ", val.kind() = " << val.kind() << std::endl; + if (is_node_sel) { + if (the_graph.has_node_series(subsel)) { + std::cerr << "Selector already populated" << std::endl; + return 1; + } + + auto nodemap = the_graph.nodemap(); + auto where = parse_where_expression(nodemap, where_exp, submission_data); + if (std::holds_alternative(val)) { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = std::get(val); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + } else if (std::holds_alternative(val)) { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = std::get(val); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + } else if (std::holds_alternative(val)) { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = std::get(val); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + } else if (std::holds_alternative(val)) { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = std::get(val).c_str(); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + } else if (std::holds_alternative(val)) { + auto col_opt = + the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + case boost::json::kind::bool_: { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_bool(); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + + break; + } + case boost::json::kind::double_: { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_double(); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + + break; + } + + case boost::json::kind::int64: { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_int64(); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + break; + } + + case boost::json::kind::string: { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_string().c_str(); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + break; + } + default: + std::cerr << "Unsupported type" << std::endl; + return 1; + } + } else { // edge map + if (the_graph.has_edge_series(subsel)) { + std::cerr << "Selector already populated" << std::endl; + return 1; + } + + auto edgemap = the_graph.edgemap(); + auto where = parse_where_expression(edgemap, where_exp, submission_data); + switch (val.kind()) { + case boost::json::kind::bool_: { + auto col_opt = the_graph.add_edge_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest edge series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_bool(); + the_graph.for_all_edges( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + break; + } + case boost::json::kind::double_: { + auto col_opt = the_graph.add_edge_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest edge series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_double(); + the_graph.for_all_edges( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + break; + } + + case boost::json::kind::int64: { + auto col_opt = the_graph.add_edge_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest edge series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_int64(); + the_graph.for_all_edges( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + + break; + } + + case boost::json::kind::string: { + auto col_opt = the_graph.add_edge_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest edge series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_string().c_str(); + the_graph.for_all_edges( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + break; + } + default: + std::cerr << "Unsupported type" << std::endl; + return 1; + } + } + + clip.set_state(state_name, the_graph); + clip.set_state(sel_state_name, selectors); + clip.update_selectors(selectors); + + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleGraph/bfs b/cpp/examples/ExampleGraph/bfs new file mode 100755 index 0000000..fad136a Binary files /dev/null and b/cpp/examples/ExampleGraph/bfs differ diff --git a/cpp/examples/ExampleGraph/bfs.cpp b/cpp/examples/ExampleGraph/bfs.cpp new file mode 100644 index 0000000..5b29f3e --- /dev/null +++ b/cpp/examples/ExampleGraph/bfs.cpp @@ -0,0 +1,34 @@ +#include +#include +#include + +int main() { + std::vector> adj{{1}, {0, 2}, {1}, {4}, {3}}; + std::vector components(adj.size()); + std::iota(components.begin(), components.end(), 0); + + std::vector curr_level{0}; + std::vector next_level{}; + std::vector visited(adj.size(), false); + + long int u{}; + + while (!curr_level.empty()) { + u = curr_level.back(); + std::cout << "u = " << u << "\n"; + curr_level.pop_back(); + for (long int v : adj[u]) { + std::cout << " testing " << v << "\n"; + if (!visited[v]) { + std::cout << " visiting " << v << "\n"; + visited[v] = true; + components[v] = components[u]; + std::cout << " components[" << v << "] = " << components[u] << "\n"; + next_level.push_back(v); + } + std::cout << "swapping\n"; + std::swap(next_level, curr_level); + next_level.clear(); + } + } +} diff --git a/cpp/examples/ExampleGraph/connected_components.cpp b/cpp/examples/ExampleGraph/connected_components.cpp new file mode 100644 index 0000000..cb8c6f9 --- /dev/null +++ b/cpp/examples/ExampleGraph/connected_components.cpp @@ -0,0 +1,124 @@ + + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +#include "examplegraph.hpp" + +static const std::string method_name = "connected_components"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{ + method_name, + "Populates a column containing the component id of each node in a graph"}; + clip.add_required( + "selector", + "Existing selector name into which the component id will be written"); + clip.add_required_state(state_name, + "Internal container"); + clip.add_required_state>( + sel_state_name, "Internal container for selectors"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto sel_json = clip.get("selector"); + + std::string sel; + try { + if (sel_json["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + sel = sel_json["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + if (!sel.starts_with("node.")) { + std::cerr << "Selector must be a node subselector" << std::endl; + return 1; + } + auto the_graph = clip.get_state(state_name); + + auto selectors = + clip.get_state>(sel_state_name); + if (!selectors.contains(sel)) { + std::cerr << "Selector not found" << std::endl; + return 1; + } + auto subsel = sel.substr(5); + if (the_graph.has_node_series(subsel)) { + std::cerr << "Selector already populated" << std::endl; + return 1; + } + + auto cc_o = the_graph.add_node_series(subsel, selectors.at(sel)); + if (!cc_o) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + + auto cc = cc_o.value(); + std::map ccmap; + + int64_t i = 0; + for (auto &node : the_graph.nodes()) { + ccmap[node] = i++; + } + std::vector> adj(the_graph.nv()); + the_graph.for_all_edges([&adj, &ccmap](auto edge, mvmap::locator /*unused*/) { + long i = ccmap[edge.first]; + long j = ccmap[edge.second]; + adj[i].push_back(j); + adj[j].push_back(i); + }); + + std::vector visited(the_graph.nv(), false); + std::vector components(the_graph.nv()); + std::iota(components.begin(), components.end(), 0); + + for (int64_t i = 0; i < the_graph.nv(); ++i) { + if (!visited[i]) { + std::queue q; + q.push(i); + while (!q.empty()) { + int64_t v = q.front(); + q.pop(); + visited[v] = true; + for (int64_t u : adj[v]) { + if (!visited[u]) { + q.push(u); + components[u] = components[i]; + } + } + } + } + } + + the_graph.for_all_nodes( + [&components, &ccmap, &cc](auto node, mvmap::locator /*unused*/) { + int64_t i = ccmap[node]; + cc[node] = components[i]; + }); + + clip.set_state(state_name, the_graph); + // clip.set_state(sel_state_name, selectors); + // clip.update_selectors(selectors); + + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleGraph/copy_series.cpp b/cpp/examples/ExampleGraph/copy_series.cpp new file mode 100644 index 0000000..922760b --- /dev/null +++ b/cpp/examples/ExampleGraph/copy_series.cpp @@ -0,0 +1,92 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +#include "clippy/clippy.hpp" +#include "clippy/selector.hpp" +#include "examplegraph.hpp" + +namespace boostjsn = boost::json; + +static const std::string method_name = "copy_series"; +static const std::string graph_state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Copies a subselector to a new subselector"}; + clip.add_required("from_sel", "Source Selector"); + clip.add_required("to_sel", "Target Selector"); + + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.add_required_state( + graph_state_name, "Internal state for the graph"); + + if (clip.parse(argc, argv)) { + return 0; + } + + auto from_selector = clip.get("from_sel"); + auto to_selector = clip.get("to_sel"); + + auto the_graph = clip.get_state(graph_state_name); + + bool edge_sel = false; // if false, then node selector + if (examplegraph::examplegraph::is_edge_selector(from_selector)) { + edge_sel = true; + } else if (!examplegraph::examplegraph::is_node_selector(from_selector)) { + std::cerr << "!! ERROR: from_selector must start with either \"edge\" or " + "\"node\" (received " + << from_selector << ") !!"; + exit(-1); + } + + if (the_graph.has_series(to_selector)) { + std::cerr << "!! ERROR: Selector name " << to_selector + << " already exists in graph !!" << std::endl; + exit(-1); + } + + if (clip.has_state(sel_state_name)) { + auto selectors = + clip.get_state>(sel_state_name); + if (selectors.contains(to_selector)) { + std::cerr << "Warning: Using unmanifested selector." << std::endl; + selectors.erase(to_selector); + } + auto from_selector_tail = from_selector.tail(); + auto to_selector_tail = to_selector.tail(); + if (!from_selector_tail.has_value() || !to_selector_tail.has_value()) { + std::cerr + << "!! ERROR: from_selector and to_selector must have content !!" + << std::endl; + exit(1); + } + from_selector = from_selector_tail.value(); + to_selector = to_selector_tail.value(); + if (edge_sel) { + if (!the_graph.copy_edge_series(from_selector, to_selector)) { + std::cerr << "!! ERROR: copy failed from " << from_selector << " to " + << to_selector << "!!" << std::endl; + exit(1); + }; + } else { + if (!the_graph.copy_node_series(from_selector, to_selector)) { + std::cerr << "!! ERROR: copy failed from " << from_selector << " to " + << to_selector << "!!" << std::endl; + exit(1); + }; + } + } + + clip.set_state(graph_state_name, the_graph); + clip.return_self(); + + return 0; +} diff --git a/cpp/examples/ExampleGraph/count.cpp b/cpp/examples/ExampleGraph/count.cpp new file mode 100644 index 0000000..e5ee5db --- /dev/null +++ b/cpp/examples/ExampleGraph/count.cpp @@ -0,0 +1,124 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "examplegraph.hpp" + +static const std::string method_name = "count"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "returns a map containing the count of values in a " + "series based on selector"}; + clip.add_required("selector", + "Existing selector name to calculate extrema"); + clip.add_required_state(state_name, + "Internal container"); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + auto sel_str = clip.get("selector"); + selector sel{sel_str}; + + auto the_graph = clip.get_state(state_name); + + bool is_edge_sel = examplegraph::examplegraph::is_edge_selector(sel); + bool is_node_sel = examplegraph::examplegraph::is_node_selector(sel); + + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + auto tailsel_opt = sel.tail(); + if (!tailsel_opt) { + std::cerr << "no tail" << std::endl; + return 1; + } + + auto tail_sel = tailsel_opt.value(); + if (is_edge_sel) { + if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else { + std::cerr << "UNKNOWN TYPE" << std::endl; + return 1; + } + } else if (is_node_sel) { + if (the_graph.has_series(sel)) { + sel = tailsel_opt.value(); + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else { + std::cerr << "UNKNOWN TYPE" << std::endl; + return 1; + } + } + clip.set_state(state_name, the_graph); + return 0; +} diff --git a/cpp/examples/ExampleGraph/degree.cpp b/cpp/examples/ExampleGraph/degree.cpp new file mode 100644 index 0000000..5713f1d --- /dev/null +++ b/cpp/examples/ExampleGraph/degree.cpp @@ -0,0 +1,78 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "examplegraph.hpp" + +static const std::string method_name = "degree"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{ + method_name, + "Populates a column containing the degree of each node in a graph"}; + clip.add_required( + "selector", + "Existing selector name into which the degree will be written"); + clip.add_required_state(state_name, + "Internal container"); + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + selector sel = clip.get("selector"); + + if (!sel.headeq("node")) { + std::cerr << "Selector must be a node subselector" << std::endl; + return 1; + } + auto the_graph = clip.get_state(state_name); + + auto selectors = + clip.get_state>(sel_state_name); + if (!selectors.contains(sel)) { + std::cerr << "Selector not found" << std::endl; + return 1; + } + auto subsel = sel.tail().value(); + if (the_graph.has_node_series(subsel)) { + std::cerr << "Selector already populated" << std::endl; + return 1; + } + + auto deg_o = the_graph.add_node_series(subsel, "Degree"); + if (!deg_o) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + + auto deg = deg_o.value(); + + the_graph.for_all_edges([°](auto edge, mvmap::locator /*unused*/) { + deg[edge.first]++; + if (edge.first != edge.second) { + deg[edge.second]++; + } + }); + + clip.set_state(state_name, the_graph); + clip.set_state(sel_state_name, selectors); + clip.update_selectors(selectors); + + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleGraph/drop_series.cpp b/cpp/examples/ExampleGraph/drop_series.cpp new file mode 100644 index 0000000..e783ae1 --- /dev/null +++ b/cpp/examples/ExampleGraph/drop_series.cpp @@ -0,0 +1,66 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include + +#include "clippy/clippy.hpp" +#include "clippy/selector.hpp" +#include "examplegraph.hpp" + +namespace boostjsn = boost::json; + +static const std::string method_name = "drop_series"; +static const std::string graph_state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; +static const std::string sel_name = "selector"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Drops a selector"}; + clip.add_required(sel_name, "Selector to drop"); + + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.add_required_state( + graph_state_name, "Internal state for the graph"); + + if (clip.parse(argc, argv)) { + return 0; + } + + auto jo = clip.get(sel_name); + + selector sel = clip.get(sel_name); + + auto sel_state = + clip.get_state>(sel_state_name); + if (!sel_state.contains(sel)) { + std::cerr << "Selector name not found!" << std::endl; + exit(-1); + } + auto the_graph = clip.get_state(graph_state_name); + auto subsel = sel.tail().value(); + if (sel.headeq("edge")) { + if (the_graph.has_edge_series(subsel)) { + the_graph.drop_edge_series(sel); + } + } else if (sel.headeq("node")) { + if (the_graph.has_node_series(subsel)) { + the_graph.drop_node_series(subsel); + } + } else { + std::cerr << "Selector name must start with either \"edge.\" or \"node.\"" + << std::endl; + exit(-1); + } + sel_state.erase(sel); + clip.set_state(graph_state_name, the_graph); + clip.set_state(sel_state_name, sel_state); + clip.update_selectors(sel_state); + + return 0; +} diff --git a/cpp/examples/ExampleGraph/dump.cpp b/cpp/examples/ExampleGraph/dump.cpp new file mode 100644 index 0000000..cc64c8f --- /dev/null +++ b/cpp/examples/ExampleGraph/dump.cpp @@ -0,0 +1,128 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "examplegraph.hpp" + +static const std::string method_name = "dump"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +static const std::string always_true = R"({"rule":{"==":[1,1]}})"; +static const std::string never_true = R"({"rule":{"==":[2,1]}})"; + +static const boost::json::object always_true_obj = + boost::json::parse(always_true).as_object(); + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "returns a map of key: value " + "corresponding to the selector."}; + clip.add_required("selector", + "Existing selector name to obtain values"); + + clip.add_optional("where", "where filter", + always_true_obj); + + clip.add_required_state(state_name, + "Internal container"); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto sel_str = clip.get("selector"); + selector sel{sel_str}; + + auto the_graph = clip.get_state(state_name); + + bool is_edge_sel = examplegraph::examplegraph::is_edge_selector(sel); + bool is_node_sel = examplegraph::examplegraph::is_node_selector(sel); + + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + auto tailsel_opt = sel.tail(); + if (!tailsel_opt) { + std::cerr << "no tail" << std::endl; + return 1; + } + + auto expression = clip.get("where"); + // auto expression = boost::json::parse(always_true).as_object(); + std::cerr << "expression: " << expression << std::endl; + + // we need to make a copy here because translateNode modifies the object + boost::json::object exp2(expression); + auto [_a /*unused*/, vars, _b /*unused*/] = + jsonlogic::translateNode(exp2["rule"]); + std::cerr << "post-translate expression: " << expression << std::endl; + std::cerr << "post-translate expression['rule']: " << expression["rule"] + << std::endl; + auto apply_jl = [&expression, &vars](int value) { + boost::json::object data; + boost::json::value val = value; + std::cerr << " apply_jl expression: " << expression << std::endl; + for (auto var : vars) { + data[var] = val; + std::cerr << " apply_jl: var: " << var << " val: " << val << std::endl; + } + + jsonlogic::any_expr res = jsonlogic::apply(expression["rule"], data); + std::cerr << " apply_jl: res: " << res << std::endl; + return jsonlogic::unpack_value(res); + }; + + std::string tail_sel = tailsel_opt.value(); + if (is_node_sel) { + if (the_graph.has_node_series(tail_sel)) { + auto pxy = the_graph.get_node_series(tail_sel).value(); + std::map filtered_data; + pxy.for_all( + [&filtered_data, &apply_jl](const auto &key, auto, const auto &val) { + std::cerr << "key: " << key << " val: " << val << std::endl; + if (apply_jl(val)) { // apply the where clause + filtered_data[key] = val; + std::cerr << "applied!" << std::endl; + } + }); + + clip.returns>( + "map of key: value corresponding to the selector"); + clip.to_return(filtered_data); + return 0; + } else if (the_graph.has_node_series(tail_sel)) { + // clip.returns>( + // "map of key: value corresponding to the selector"); + } else if (the_graph.has_node_series(tail_sel)) { + // clip.returns>( + // "map of key: value corresponding to the selector"); + } else if (the_graph.has_node_series(tail_sel)) { + // clip.returns>( + // "map of key: value corresponding to the selector"); + } else { + std::cerr << "Node series is an invalid type" << std::endl; + return 1; + } + } + + clip.returns>( + "map of key: value corresponding to the selector"); + + clip.to_return(std::map{}); + clip.set_state(state_name, the_graph); + return 0; +} diff --git a/cpp/examples/ExampleGraph/dump2.cpp b/cpp/examples/ExampleGraph/dump2.cpp new file mode 100644 index 0000000..399c6a3 --- /dev/null +++ b/cpp/examples/ExampleGraph/dump2.cpp @@ -0,0 +1,87 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +// #include +#include + +#include "clippy/selector.hpp" +#include "examplegraph.hpp" +#include "where.cpp" + +static const std::string method_name = "dump2"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +static const std::string always_true = R"({"rule":{"==":[1,1]}})"; +static const std::string never_true = R"({"rule":{"==":[2,1]}})"; + +static const boost::json::object always_true_obj = + boost::json::parse(always_true).as_object(); + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "returns a map of key: value " + "corresponding to the selector."}; + clip.add_required("selector", + "Existing selector name to obtain values"); + + clip.add_optional("where", "where filter", + always_true_obj); + + clip.add_required_state(state_name, + "Internal container"); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + std::cerr << "past parse" << std::endl; + + auto sel_str = clip.get("selector"); + selector sel{sel_str}; + + auto the_graph = clip.get_state(state_name); + + bool is_edge_sel = examplegraph::examplegraph::is_edge_selector(sel); + bool is_node_sel = examplegraph::examplegraph::is_node_selector(sel); + + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + std::cerr << "before tailsel_opt" << std::endl; + auto tailsel_opt = sel.tail(); + if (!tailsel_opt) { + std::cerr << "no tail" << std::endl; + return 1; + } + + auto expression = clip.get("where"); + // auto expression = boost::json::parse(always_true).as_object(); + std::cerr << "expression: " << expression << std::endl; + + std::string tail_sel = tailsel_opt.value(); + if (is_node_sel) { + clip.returns>( + "vector of node keys that match the selector"); + auto filtered_data = where_nodes(the_graph, expression); + clip.to_return(filtered_data); + return 0; + } + + clip.returns>( + "map of key: value corresponding to the selector"); + + clip.to_return(std::map{}); + clip.set_state(state_name, the_graph); + return 0; +} diff --git a/cpp/examples/ExampleGraph/examplegraph.cpp b/cpp/examples/ExampleGraph/examplegraph.cpp new file mode 100644 index 0000000..065c5a8 --- /dev/null +++ b/cpp/examples/ExampleGraph/examplegraph.cpp @@ -0,0 +1,48 @@ +#include "examplegraph.hpp" + +#include + +int main() { + auto g = examplegraph::examplegraph{}; + + g.add_node("a"); + g.add_node("b"); + g.add_edge("a", "b"); + g.add_edge("b", "c"); + + assert(g.has_node("a")); + assert(g.has_node("b")); + assert(g.has_node("c")); + assert(!g.has_node("d")); + + assert(g.has_edge({"a", "b"})); + assert(g.has_edge({"b", "c"})); + assert(!g.has_edge({"a", "c"})); + + assert(g.out_degree("a") == 1); + assert(g.in_degree("a") == 0); + assert(g.in_degree("b") == 1); + assert(g.out_degree("b") == 1); + + auto colref = + g.add_node_series("color", "The color of the nodes"); + colref.value()["a"] = "blue"; + colref.value()["c"] = "red"; + + auto weightref = g.add_edge_series("weight", "edge weights"); + + weightref.value()[std::pair("a", "b")] = 5.5; + weightref.value()[std::pair("b", "c")] = 3.3; + + std::cout << "g.nv: " << g.nv() << ", g.ne: " << g.ne() << "\n"; + std::cout << boost::json::value_from(g) << "\n"; + + auto val = boost::json::value_from(g); + auto str = boost::json::serialize(val); + std::cout << "here it is: " << str << "\n"; + + auto g2 = boost::json::value_to(val); + auto e2 = weightref.value().extrema(); + std::cout << "extrema: " << std::get<0>(e2.first.value()) << ", " + << std::get<0>(e2.second.value()) << "\n"; +} diff --git a/cpp/examples/ExampleGraph/examplegraph.hpp b/cpp/examples/ExampleGraph/examplegraph.hpp new file mode 100644 index 0000000..ffaf84a --- /dev/null +++ b/cpp/examples/ExampleGraph/examplegraph.hpp @@ -0,0 +1,233 @@ +#pragma once +#include "mvmap.hpp" +#include +#include +#include +#include +#include +#include + +namespace examplegraph { +// map of (src, dst) : weight +using node_t = std::string; +using edge_t = std::pair; + +template using sparsevec = std::map; + +enum series_type { ser_bool, ser_int64, ser_double, ser_string, ser_invalid }; + +using variants = std::variant; +class examplegraph { + using edge_mvmap = mvmap::mvmap; + using node_mvmap = mvmap::mvmap; + template using edge_series_proxy = edge_mvmap::series_proxy; + template using node_series_proxy = node_mvmap::series_proxy; + node_mvmap node_table; + edge_mvmap edge_table; + +public: + const mvmap::mvmap & + nodemap() const { + return node_table; + } + + const mvmap::mvmap & + edgemap() const { + return edge_table; + } + static inline bool is_edge_selector(const std::string &sel) { + return sel.starts_with("edge."); + } + + static inline bool is_node_selector(const std::string &sel) { + return sel.starts_with("node."); + } + + static inline bool is_valid_selector(const std::string &sel) { + return is_edge_selector(sel) || is_node_selector(sel); + } + + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, examplegraph const &g) { + v = {{"node_table", boost::json::value_from(g.node_table)}, + {"edge_table", boost::json::value_from(g.edge_table)}}; + } + + friend examplegraph + tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v) { + const auto &obj = v.as_object(); + auto nt = boost::json::value_to(obj.at("node_table")); + auto et = boost::json::value_to(obj.at("edge_table")); + return {nt, et}; + } + examplegraph() = default; + examplegraph(node_mvmap nt, edge_mvmap et) + : node_table(std::move(nt)), edge_table(std::move(et)) {}; + + // this function requires that the "edge." prefix be removed from the name. + template + std::optional> + add_edge_series(const std::string &sel, const std::string &desc = "") { + return edge_table.add_series(sel, desc); + } + + template + std::optional> + add_edge_series(const std::string &sel, const edge_series_proxy &from, + const std::string &desc = "") { + return edge_table.add_series(sel, from, desc); + } + + void drop_edge_series(const std::string &sel) { edge_table.drop_series(sel); } + + // this function requires that the "node." prefix be removed from the name. + void drop_node_series(const std::string &sel) { node_table.drop_series(sel); } + + // this function requires that the "node." prefix be removed from the name. + template + std::optional> + add_node_series(const std::string &sel, const std::string &desc = "") { + return node_table.add_series(sel, desc); + } + + template + std::optional> + add_node_series(const std::string &sel, const node_series_proxy &from, + const std::string &desc = "") { + return node_table.add_series(sel, from, desc); + } + template + std::optional> get_edge_series(const std::string &sel) { + return edge_table.get_series(sel); + } + + bool copy_edge_series(const std::string &from, const std::string &to, + const std::optional &desc = std::nullopt) { + return edge_table.copy_series(from, to, desc); + } + + bool copy_node_series(const std::string &from, const std::string &to, + const std::optional &desc = std::nullopt) { + return node_table.copy_series(from, to, desc); + } + + template + std::optional> get_node_series(const std::string &sel) { + return node_table.get_series(sel); + } + + [[nodiscard]] size_t nv() const { return node_table.size(); } + [[nodiscard]] size_t ne() const { return edge_table.size(); } + + template void for_all_edges(F f) { edge_table.for_all(f); } + template void for_all_nodes(F f) { node_table.for_all(f); } + + [[nodiscard]] std::vector edges() const { + auto kv = edge_table.keys(); + return {kv.begin(), kv.end()}; + } + [[nodiscard]] std::vector nodes() const { + auto kv = node_table.keys(); + return {kv.begin(), kv.end()}; + } + + bool add_node(const node_t &node) { return node_table.add_key(node); }; + bool add_edge(const node_t &src, const node_t &dst) { + node_table.add_key(src); + node_table.add_key(dst); + return edge_table.add_key({src, dst}); + } + + bool has_node(const node_t &node) { return node_table.contains(node); }; + bool has_edge(const edge_t &edge) { return edge_table.contains(edge); }; + bool has_edge(const node_t &src, const node_t &dst) { + return edge_table.contains({src, dst}); + }; + + // strips the head off the std::string and passes the tail to the appropriate + // method. + [[nodiscard]] bool has_series(const std::string &sel) const { + auto tail = sel.substr(5); + + if (is_node_selector(sel)) { + return has_node_series(tail); + } + if (is_edge_selector(sel)) { + return has_edge_series(tail); + } + return false; + } + + template + [[nodiscard]] bool has_series(const std::string &sel) const { + auto tail = sel.substr(5); + + if (is_node_selector(sel)) { + return has_node_series(tail); + } + if (is_edge_selector(sel)) { + return has_edge_series(tail); + } + return false; + } + + // assumes sel has already been tail'ed. + [[nodiscard]] bool has_node_series(const std::string &sel) const { + return node_table.has_series(sel); + } + + template + [[nodiscard]] bool has_node_series(const std::string &sel) const { + return node_table.has_series(sel); + } + + // assumes sel has already been tail'ed. + [[nodiscard]] bool has_edge_series(const std::string &sel) const { + return edge_table.has_series(sel); + } + template + [[nodiscard]] bool has_edge_series(const std::string &sel) const { + return edge_table.has_series(sel); + } + + [[nodiscard]] std::vector out_neighbors(const node_t &node) const { + std::vector neighbors; + for (const auto &[src, dst] : edge_table.keys()) { + if (src == node) { + neighbors.emplace_back(dst); + } + } + return neighbors; + } + + [[nodiscard]] std::vector in_neighbors(const node_t &node) const { + std::vector neighbors; + for (const auto &[src, dst] : edge_table.keys()) { + if (dst == node) { + neighbors.emplace_back(src); + } + } + return neighbors; + } + + [[nodiscard]] size_t in_degree(const node_t &node) const { + return in_neighbors(node).size(); + } + [[nodiscard]] size_t out_degree(const node_t &node) const { + return out_neighbors(node).size(); + } + + std::string str_edge_col(const std::string &col) { + std::vector cols{col}; + return edge_table.str_cols(cols); + } + + std::string str_node_col(const std::string &col) { + std::vector cols{col}; + return node_table.str_cols(cols); + } + +}; // class examplegraph + +} // namespace examplegraph diff --git a/cpp/examples/ExampleGraph/extrema.cpp b/cpp/examples/ExampleGraph/extrema.cpp new file mode 100644 index 0000000..2dbbf90 --- /dev/null +++ b/cpp/examples/ExampleGraph/extrema.cpp @@ -0,0 +1,157 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "examplegraph.hpp" + +static const std::string method_name = "extrema"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "returns the extrema of a series based on selector"}; + clip.add_required("selector", + "Existing selector name to calculate extrema"); + clip.add_required_state(state_name, + "Internal container"); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + auto sel_str = clip.get("selector"); + selector sel{sel_str}; + + bool is_edge_sel = examplegraph::examplegraph::is_edge_selector(sel); + bool is_node_sel = examplegraph::examplegraph::is_node_selector(sel); + + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + auto tail_opt = sel.tail(); + if (!tail_opt) { + std::cerr << "Selector must have a tail" << std::endl; + return 1; + } + auto tail_sel = tail_opt.value(); + + auto the_graph = clip.get_state(state_name); + + if (is_edge_sel) { + clip.returns< + std::map>>( + "min and max keys and values of the series"); + if (the_graph.has_edge_series(tail_sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (!series) { + std::cerr << "Edge series not found" << std::endl; + return 1; + } + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + clip.to_return(extrema); + } else if (the_graph.has_edge_series(tail_sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (!series) { + std::cerr << "Edge series not found" << std::endl; + return 1; + } + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + clip.to_return(extrema); + } else { + std::cerr << "Edge series is an invalid type" << std::endl; + return 1; + } + } else if (is_node_sel) { + if (the_graph.has_edge_series(tail_sel)) { + clip.returns< + std::map>>( + "min and max keys and values of the series"); + + auto series = the_graph.get_node_series(tail_sel); + if (!series) { + std::cerr << "Edge series not found" << std::endl; + return 1; + } + + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + + clip.to_return(extrema); + } else if (the_graph.has_node_series(tail_sel)) { + clip.returns< + std::map>>( + "min and max keys and values of the series"); + + auto series = the_graph.get_node_series(tail_sel); + if (!series) { + return 1; + } + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + + clip.to_return(extrema); + } else { + std::cerr << "Node series is an invalid type" << std::endl; + return 1; + } + } + + clip.set_state(state_name, the_graph); + return 0; +} diff --git a/cpp/examples/ExampleGraph/for_all_edges.cpp b/cpp/examples/ExampleGraph/for_all_edges.cpp new file mode 100644 index 0000000..9688f84 --- /dev/null +++ b/cpp/examples/ExampleGraph/for_all_edges.cpp @@ -0,0 +1,52 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include + +#include "examplegraph.hpp" + +static const std::string method_name = "add_node"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "Adds a new column to a graph based on a lambda"}; + clip.add_required("name", "New column name"); + clip.add_required("expression", "Lambda Expression"); + clip.add_required_state(state_name, + "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto name = clip.get("name"); + auto expression = clip.get("expression"); + auto the_graph = clip.get_state(state_name); + + // + // Expression here + auto apply_jl = [&expression](const examplegraph::edge_t &value, + mvmap::locator loc) { + boost::json::object data; + data["src"] = value.first; + data["dst"] = value.second; + data["loc"] = boost::json::value_from(loc); + jsonlogic::any_expr res = jsonlogic::apply(expression["rule"], data); + return jsonlogic::unpack_value(res); + }; + + the_graph.for_all_edges(apply_jl); + + clip.set_state(state_name, the_graph); + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleGraph/meta.json b/cpp/examples/ExampleGraph/meta.json new file mode 100644 index 0000000..0af2980 --- /dev/null +++ b/cpp/examples/ExampleGraph/meta.json @@ -0,0 +1,7 @@ +{ + "__doc__" : "A graph data structure", + "initial_selectors" : { + "edge" : "The edges of the graph", + "node": "The nodes of the graph" + } +} diff --git a/cpp/examples/ExampleGraph/mvmap.hpp b/cpp/examples/ExampleGraph/mvmap.hpp new file mode 100644 index 0000000..2643888 --- /dev/null +++ b/cpp/examples/ExampleGraph/mvmap.hpp @@ -0,0 +1,609 @@ +#pragma once +#include +// #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +template +std::ostream &operator<<(std::ostream &os, const std::pair &p) { + os << "(" << p.first << ", " << p.second << ")"; + return os; +} + +namespace mvmap { +using index = uint64_t; + +class locator { + static const index INVALID_LOC = std::numeric_limits::max(); + index loc; + locator(index loc) : loc(loc) {}; + + public: + template + friend class mvmap; + friend std::ostream &operator<<(std::ostream &os, const locator &l) { + if (l.is_valid()) { + os << "locator: " << l.loc; + } else { + os << "locator: invalid"; + } + return os; + } + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, locator l); + friend locator tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v); + locator() : loc(INVALID_LOC) {}; + [[nodiscard]] bool is_valid() const { return loc != INVALID_LOC; } + + void print() const { + if (is_valid()) { + std::cout << "locator: " << loc << std::endl; + } else { + std::cout << "locator: invalid" << std::endl; + } + } +}; +void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, + locator l) { + v = l.loc; +} + +locator tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v) { + return boost::json::value_to(v); +} +template +class mvmap { + template + using series = std::map; + using key_to_idx = std::map; + using idx_to_key = std::map; + using variants = std::variant; + + template + static void print_series(const series &s) { + for (auto el : s) { + std::cout << el.first << " -> " << el.second << std::endl; + } + } + + // A locator is an opaque handle to a key in a series. + + idx_to_key itk; + key_to_idx kti; + std::map...>> data; + std::map series_desc; + + public: + // A series_proxy is a reference to a series in an mvmap. + template + class series_proxy { + std::string m_id; + std::string m_desc; + key_to_idx &kti_r; + idx_to_key &itk_r; + series &series_r; + + using series_type = V; + + // returns true if there is an index assigned to a given key + bool has_idx_at_key(K k) const { return kti_r.contains(k); } + // returns true if there is a key assigned to a given locator + bool has_key_at_index(locator l) const { return itk_r.contains(l.loc); } + + // returns or creates the index for a key. + index get_idx(K k) { + if (!has_idx_at_key(k)) { + index i{kti_r.size()}; + kti_r[k] = i; + itk_r[i] = k; + return i; + } + return kti_r[k]; + } + + public: + series_proxy(std::string id, series &ser, mvmap &m) + : m_id(std::move(id)), kti_r(m.kti), itk_r(m.itk), series_r(ser) {} + + series_proxy(std::string id, const std::string &desc, series &ser, + mvmap &m) + : m_id(std::move(id)), + m_desc(desc), + kti_r(m.kti), + itk_r(m.itk), + series_r(ser) {} + + bool is_string_v() const { return std::is_same_v; } + bool is_double_v() const { return std::is_same_v; } + bool is_int64_t_v() const { return std::is_same_v; } + bool is_bool_v() const { return std::is_same_v; } + + std::string id() const { return m_id; } + std::string desc() const { return m_desc; } + V &operator[](K k) { return series_r[get_idx(k)]; } + const V &operator[](K k) const { return series_r[get_idx(k)]; } + + // this assumes the key exists. + V &operator[](locator l) { return series_r[l.loc]; } + const V &operator[](locator l) const { return series_r[l.loc]; } + + std::optional> at(locator l) { + if (!has_key_at_index(l) || !series_r.contains(l.loc)) { + return std::nullopt; + } + return series_r[l.loc]; + }; + std::optional> at(locator l) const { + if (!has_key_at_index(l) || !series_r.contains(l.loc)) { + return std::nullopt; + } + return series_r[l.loc]; + }; + + std::optional> at(K k) { + if (!has_idx_at_key(k) || !series_r.contains(get_idx(k))) { + return std::nullopt; + } + return series_r[get_idx(k)]; + }; + + std::optional> at(K k) const { + if (!has_idx_at_key(k) || !series_r.contains(get_idx(k))) { + return std::nullopt; + } + return series_r[get_idx(k)]; + }; + + // this will create the key/index if it doesn't exist. + locator get_loc(K k) { return locator(get_idx(k)); } + + // F takes (K key, locator, V value) + template + void for_all(F f) { + for (auto el : series_r) { + f(itk_r[el.first], locator(el.first), el.second); + } + }; + + template + void for_all(F f) const { + for (auto el : series_r) { + f(itk_r[el.first], locator(el.first), el.second); + } + }; + + // F takes (K key, locator, V value) + template + void remove_if(F f) { + auto indices_to_delete = std::vector{}; + for (auto el : series_r) { + if (f(itk_r[el.first], locator(el.first), el.second)) { + indices_to_delete.emplace_back(el.first); + } + } + + for (auto ltd : indices_to_delete) { + erase(locator(ltd)); + } + }; + + void erase(const locator &l) { + auto i = l.loc; + kti_r.erase(itk_r[i]); + itk_r.erase(i); + series_r.erase(i); + } + + // if the key doesn't exist, do nothing. + void erase(const K &k) { + if (!has_idx_at_key(k)) { + return; + } + auto i = kti_r[k]; + erase(locator(i)); + } + + // this returns the key for a given locator in a series, or nullopt if the + // locator is invalid. + std::optional> get_key( + const locator &l) const { + if (!has_key_at_index(l.loc)) { + return std::nullopt; + } + return itk_r[l.loc]; + } + + std::pair>, + std::optional>> + extrema() { + V min = std::numeric_limits::max(); + V max = std::numeric_limits::min(); + bool found_min = false; + bool found_max = false; + locator min_loc; + locator max_loc; + K min_key; + K max_key; + for_all([&min, &max, &found_min, &found_max, &min_loc, &max_loc, &min_key, + &max_key](auto k, auto l, auto v) { + if (v < min) { + min = v; + min_loc = l; + min_key = k; + found_min = true; + } + if (v > max) { + max = v; + max_loc = l; + max_key = k; + found_max = true; + } + }); + std::optional> min_opt, max_opt; + if (found_min) { + min_opt = std::make_tuple(min, min_key, min_loc); + } else { + min_opt = std::nullopt; + } + if (found_max) { + max_opt = std::make_tuple(max, max_key, max_loc); + } else { + max_opt = std::nullopt; + } + return std::make_pair(min_opt, max_opt); + } + + std::map count() { + std::map ct; + for_all([&ct](auto /*unused*/, auto /*unused*/, auto v) { ct[v]++; }); + return ct; + } + + void print() { + std::cout << "id: " << m_id << ", "; + std::cout << "desc: " << m_desc << ", "; + std::string dtype = "unknown"; + if (is_string_v()) { + dtype = "string"; + } else if (is_double_v()) { + dtype = "double"; + } else if (is_int64_t_v()) { + dtype = "int64_t"; + } else if (is_bool_v()) { + dtype = "bool"; + } + std::cout << "dtype: " << dtype << ", "; + // std::cout << "kti_r.size(): " << kti_r.size() << std::endl; + // std::cout << "itk_r.size(): " << itk_r.size() << std::endl; + std::cout << series_r.size() << " entries" << std::endl; + // std::cout << "elements: " << std::endl; + for (auto el : series_r) { + std::cout << " " << itk_r[el.first] << " -> " << el.second + << std::endl; + } + } + + }; // end of series + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + mvmap(const idx_to_key &itk, const key_to_idx &kti, + const std::map...>> &data) + : itk(itk), kti(kti), data(data) {} + + mvmap() = default; + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, const mvmap &m) { + v = {{"itk", boost::json::value_from(m.itk)}, + {"kti", boost::json::value_from(m.kti)}, + {"data", boost::json::value_from(m.data)}}; + } + + friend mvmap tag_invoke( + boost::json::value_to_tag> /*unused*/, + const boost::json::value &v) { + const auto &obj = v.as_object(); + using index = uint64_t; + // template using series = std::map; + using key_to_idx = std::map; + using idx_to_key = std::map; + return {boost::json::value_to(obj.at("itk")), + boost::json::value_to(obj.at("kti")), + boost::json::value_to< + std::map...>>>( + obj.at("data"))}; + } + + [[nodiscard]] size_t size() const { return kti.size(); } + bool add_key(const K &k) { + if (kti.count(k) > 0) { + return false; + } + auto i = kti.size(); + kti[k] = i; + itk[i] = k; + return true; + } + + [[nodiscard]] std::vector> list_series() { + std::vector> ser_pairs; + for (auto el : series_desc) { + ser_pairs.push_back(el); + } + return ser_pairs; + } + + [[nodiscard]] bool has_series(const std::string &id) const { + return data.contains(id); + } + + template + [[nodiscard]] bool has_series(const std::string &id) const { + // std::cerr << "has_series: " << id << std::endl; + // std::cerr << "V = " << typeid(V).name() << std::endl; + // std::cerr << "data.contains(id): " << data.contains(id) << std::endl; + // if (data.contains(id)) { + // std::cerr << "std::holds_alternative>(data.at(id)): " + // << std::holds_alternative>(data.at(id)) << + // std::endl; + // } + return data.contains(id) && std::holds_alternative>(data.at(id)); + } + bool contains(const K &k) { return kti.contains(k); } + auto keys() const { return std::views::keys(kti); } + + void add_row(const K &key, + const std::map> &row) { + auto loc = locator(kti.size()); + for (const auto &el : row) { + if (!has_series(el.first)) { + continue; + } + std::visit( + [&el, &key, this](auto &ser) { + std::cout << "adding to series " << el.first << std::endl; + using variant_type = std::decay_tsecond)>; + auto sproxy = get_series>(el.first) + .value(); // this is a series_proxy + // TODO: make sure this actually sets itk and kti correctly. + sproxy[key] = std::get(el.second); + }, + data[el.first]); + } + } + + void rem_row(const K &key) { + auto index = kti[key]; + for (auto &el : data) { + std::visit([&key, &index, this](auto &ser) { ser.erase(index); }, + el.second); + } + + kti.erase(key); + itk.erase(index); + } + // adds a new column (series) to the mvmap and returns true. If already + // exists, return false + template + std::optional> add_series(const std::string &sel, + const std::string &desc = "") { + if (has_series(sel)) { + return std::nullopt; + } + data[sel] = series{}; + series_desc[sel] = desc; + return series_proxy(sel, desc, std::get>(data[sel]), *this); + } + + // copies an existing column (series) to a new (unmanifested) column and + // returns true. If the new column already exists, or if the existing column + // doesn't, return false. + bool copy_series(const std::string &from, const std::string &to, + const std::optional &desc = std::nullopt) { + if (has_series(to) || !has_series(from)) { + std::cerr << "copy_series failed from " << from << " to " << to + << std::endl; + return false; + } + // std::cerr << "copying series from " << from << " to " << to << std::endl; + data[to] = data[from]; + series_desc[to] = desc.has_value() ? desc.value() : series_desc[from]; + return true; + } + + template + std::optional> get_series(const std::string &sel) { + if (!has_series(sel)) { + // series doesn't exist or is of the wrong type. + return std::nullopt; + } + // return series_proxy(sel, this->series_desc.at(sel), + // std::get>(data[sel]), *this); + auto foo = series_desc[sel]; + return series_proxy(sel, foo, std::get>(data[sel]), *this); + } + + bool series_is_string(const std::string &sel) const { + return has_series(sel); + } + + bool series_is_double(const std::string &sel) const { + return has_series(sel); + } + + bool series_is_int64_t(const std::string &sel) const { + return has_series(sel); + } + + bool series_is_bool(const std::string &sel) const { + return has_series(sel); + } + + // std::optional> get_variant_series( + // const std::string &sel) { + // if (!has_series(sel)) { + // return std::nullopt; + // } + + // using vtype = decltype(data[sel]); + // return series_proxy(sel, this->series_desc.at(sel), + // data.at(sel), + // this); + // } + + void drop_series(const std::string &sel) { + if (!has_series(sel)) { + return; + } + data.erase(sel); + series_desc.erase(sel); + } + + std::optional get_as_variant(const std::string &sel, + const locator &loc) { + auto col = data[sel]; + std::optional val; + std::visit( + [&val, sel, loc, this](auto &ser) { + using T = std::decay_tsecond)>; + auto sproxy = get_series>(sel) + .value(); // this is a series_proxy + val = sproxy.at(loc); + }, + col); + return val; + // return data[sel][loc.loc]; + } + + std::optional get_as_variant(const std::string &sel, + const K &key) { + auto col = data[sel]; + std::optional val; + std::visit( + [&val, sel, key, this](auto &ser) { + using T = std::decay_tsecond)>; + auto sproxy = get_series>(sel) + .value(); // this is a series_proxy + val = sproxy.at(key); + }, + col); + return val; + } + + // F is a function that takes a key and a locator. + // Users will need to close over series_proxies that they want to use. + template + void for_all(F f) { + for (auto &idx : kti) { + f(idx.first, locator(idx.second)); + } + } + + template + void remove_if(F f) { + std::vector indices_to_delete; + for (auto &idx : kti) { + if (f(idx.first, locator(idx.second))) { + indices_to_delete.emplace_back(idx.second); + } + } + + for (auto &idx : indices_to_delete) { + kti.erase(itk[idx]); + itk.erase(idx); + for (auto &id_ser : data) { + std::visit([&idx](auto &ser) { ser.erase(idx); }, id_ser.second); + } + } + } + + void print() { + std::cout << "mvmap with " << data.size() << " series: " << std::endl; + for (auto &el : data) { + std::cout << "series " << el.first << ":" << std::endl; + std::visit( + [&el, this](auto &ser) { + using T = std::decay_tsecond)>; + auto sproxy = get_series>(el.first) + .value(); // this is a series_proxy + sproxy.print(); + }, + el.second); + + // std::cout << " second: " << el.second << std::endl; + // auto foo = get_variant_series(el.first).value(); + // foo.visit([](auto &ser) { print_series(ser); }); + // print_series(el.second); + } + } + + std::string str_cols(const std::vector &cols) { + std::stringstream sstr; + for_all([this, &cols, &sstr](auto key, auto loc) { + sstr << key << "@" << loc.loc; + for (auto &col : cols) { + auto v = get_as_variant(col, loc); + if (v.has_value()) { + std::visit( + [&col, &sstr](auto &&arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + sstr << " (str) " << col << ": " << arg; + } else if constexpr (std::is_same_v) { + sstr << " (dbl) " << col << ": " << arg; + } else if constexpr (std::is_same_v) { + sstr << " (int) " << col << ": " << arg; + } else if constexpr (std::is_same_v) { + sstr << " (bool) " << col << ": " << arg; + } + }, + v.value()); + } + } + sstr << std::endl; + }); + return sstr.str(); + } + + std::map get_series_vals_at( + const K &key, const std::vector &cols) { + std::vector...>> proxies; + + for (auto &col : cols) { + if (has_series(col)) { + std::visit( + [this, &col, &proxies](auto &coldata) { + using T = std::decay_t::mapped_type; + auto sproxy = + get_series(col).value(); // this is a series_proxy + proxies.push_back(sproxy); + }, + data[col]); + } + } + + std::map row; + for (auto &sproxy : proxies) { + std::visit( + [&row, &key](auto &&arg) { + auto v = arg.at(key); + if (v.has_value()) { + row[arg.id()] = arg[key]; + } + }, + sproxy); + } + return row; + } +}; +}; // namespace mvmap diff --git a/cpp/examples/ExampleGraph/ne.cpp b/cpp/examples/ExampleGraph/ne.cpp new file mode 100644 index 0000000..e3f2549 --- /dev/null +++ b/cpp/examples/ExampleGraph/ne.cpp @@ -0,0 +1,29 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "examplegraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "ne"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Returns the number of edges in the graph"}; + + clip.returns("Number of edges."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_graph = clip.get_state(state_name); + + clip.to_return(the_graph.ne()); + return 0; +} diff --git a/cpp/examples/ExampleGraph/nv.cpp b/cpp/examples/ExampleGraph/nv.cpp new file mode 100644 index 0000000..ea1a2e1 --- /dev/null +++ b/cpp/examples/ExampleGraph/nv.cpp @@ -0,0 +1,29 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "examplegraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "nv"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Returns the number of nodes in the graph"}; + + clip.returns("Number of nodes."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_graph = clip.get_state(state_name); + + clip.to_return(the_graph.nv()); + return 0; +} diff --git a/cpp/examples/ExampleGraph/remove.cpp b/cpp/examples/ExampleGraph/remove.cpp new file mode 100644 index 0000000..361911c --- /dev/null +++ b/cpp/examples/ExampleGraph/remove.cpp @@ -0,0 +1,36 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "examplegraph.hpp" +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "remove_edge"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + + clippy::clippy clip{method_name, "Removes a string from a ExampleSet"}; + + clip.add_required("item", "Item to remove"); + clip.add_required_state>(state_name, "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto item = clip.get("item"); + auto the_set = clip.get_state>(state_name); + the_set.erase(item); + clip.set_state(state_name, the_set); + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleGraph/series_str.cpp b/cpp/examples/ExampleGraph/series_str.cpp new file mode 100644 index 0000000..5425690 --- /dev/null +++ b/cpp/examples/ExampleGraph/series_str.cpp @@ -0,0 +1,65 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "examplegraph.hpp" + +static const std::string method_name = "series_str"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "returns the values of a series based on selector"}; + clip.add_required("selector", "Existing selector name"); + clip.add_required_state(state_name, + "Internal container"); + + clip.returns("String of data."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + auto sel_str = clip.get("selector"); + selector sel{sel_str}; + + bool is_edge_sel = examplegraph::examplegraph::is_edge_selector(sel); + bool is_node_sel = examplegraph::examplegraph::is_node_selector(sel); + + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + auto tail_opt = sel.tail(); + if (!tail_opt) { + std::cerr << "Selector must have a tail" << std::endl; + return 1; + } + auto tail_sel = tail_opt.value(); + + auto the_graph = clip.get_state(state_name); + + if (is_edge_sel) { + clip.to_return(the_graph.str_edge_col(tail_sel)); + } else if (is_node_sel) { + clip.to_return(the_graph.str_node_col(tail_sel)); + } else { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + // clip.set_state(state_name, the_graph); + return 0; +} diff --git a/cpp/examples/ExampleGraph/testconst.cpp b/cpp/examples/ExampleGraph/testconst.cpp new file mode 100644 index 0000000..badd1f1 --- /dev/null +++ b/cpp/examples/ExampleGraph/testconst.cpp @@ -0,0 +1,8 @@ +class Foo { + int x; + +public: + int get_x() const { return x; } + + int get_x() { return x + 1; } +}; diff --git a/cpp/examples/ExampleGraph/testlocator.cpp b/cpp/examples/ExampleGraph/testlocator.cpp new file mode 100644 index 0000000..a91b02c --- /dev/null +++ b/cpp/examples/ExampleGraph/testlocator.cpp @@ -0,0 +1,7 @@ +class locator { + int loc; +} + +int main() { + auto l = locator{5}; +} diff --git a/cpp/examples/ExampleGraph/testmvmap.cpp b/cpp/examples/ExampleGraph/testmvmap.cpp new file mode 100644 index 0000000..c040fd1 --- /dev/null +++ b/cpp/examples/ExampleGraph/testmvmap.cpp @@ -0,0 +1,71 @@ +#include "mvmap.hpp" +#include +// #include +#include +#include +#include + +using mymap_t = mvmap::mvmap; +int main() { + + mymap_t m{}; + + m.add_series("weight"); + std::cout << "added series weight\n"; + m.add_series("name"); + std::cout << "added series name\n"; + + auto hmap = m.get_or_create_series("age"); + std::cout << "created hmap\n"; + + hmap["seth"] = 5; + std::cout << "added seth\n"; + hmap["roger"] = 8; + std::cout << "added roger\n"; + + assert(hmap["seth"] == 5); + assert(hmap["roger"] == 8); + + auto v = boost::json::value_from(m); + std::string j = boost::json::serialize(v); + std::cout << "j = " << j << '\n'; + auto jv = boost::json::parse(j); + std::cout << boost::json::serialize(jv) << "\n"; + auto n = boost::json::value_to(jv); + + std::cout << "created n\n"; + auto hmap2 = n.get_or_create_series("age"); + std::cout << "created hmap2\n"; + assert(hmap2["seth"] == 5); + assert(hmap2["roger"] == 8); + + size_t age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 13); + + hmap2.remove_if([](const auto &k, auto, auto &v) { return v > 6; }); + + assert(hmap2.at("roger") == std::nullopt); + assert(hmap2.at("seth") == 5); + + age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 5); + hmap2["roger"] = 8; + + age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 13); + n.remove_if([&hmap2](const auto &k, auto) { return hmap2[k] == 5; }); + + age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 8); +} diff --git a/cpp/examples/ExampleGraph/testselector.cpp b/cpp/examples/ExampleGraph/testselector.cpp new file mode 100644 index 0000000..9b87720 --- /dev/null +++ b/cpp/examples/ExampleGraph/testselector.cpp @@ -0,0 +1,20 @@ +#include "../../include/clippy/selector.hpp" +#include + +int main() { + selector s = selector("foo.bar.baz"); + + selector zzz = "foo.zoo.boo"; + + selector s2{"x.y.z"}; + std::cout << "s = " << s << "\n"; + assert(s.headeq("foo")); + assert(!s.headeq("bar")); + + auto val = boost::json::value_from(s); + auto str = boost::json::serialize(val); + + auto t = boost::json::value_to(val); + assert(t.headeq("foo")); + std::cout << str << "\n"; +} diff --git a/cpp/examples/ExampleGraph/where.cpp b/cpp/examples/ExampleGraph/where.cpp new file mode 100644 index 0000000..90c9237 --- /dev/null +++ b/cpp/examples/ExampleGraph/where.cpp @@ -0,0 +1,115 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "examplegraph.hpp" +#include "jsonlogic/logic.hpp" + +template +auto parse_where_expression(M &mvmap_, boost::json::object &expression, + boost::json::object &submission_data) { + // std::cerr << " parse_where_expression: expression: " << expression + // << std::endl; + boost::json::object exp2(expression); + + std::shared_ptr jlrule = + std::make_shared( + jsonlogic::create_logic(exp2["rule"])); + // std::cerr << "past create_logic\n"; + auto vars = jlrule->variable_names(); + // boost::json::object submission_data{}; + // jsonlogic::any_expr expression_rule_; + + // we use expression_rule_ (and expression_rule; see below) in order to avoid + // having to recompute this every time we call the lambda. + // std::tie(expression_rule_, vars, std::ignore) = + // jsonlogic::create_logic(exp2["rule"]); + + // this works around a deficiency in C++ compilers where + // unique pointers moved into a lambda cannot be moved into + // an std::function. + // jsonlogic::expr* rawexpr = expression_rule_.release(); + // std::shared_ptr expression_rule{rawexpr}; + // std::shared_ptr jlshared{&jlrule}; + + // std::cerr << "parse_where: # of vars: " << vars.size() << std::endl; + // for (const auto& var : vars) { + // std::cerr << " apply_jl var dump: var: " << var << std::endl; + // } + + auto apply_jl = [&expression, jlrule, &mvmap_, + &submission_data](mvmap::locator loc) mutable { + auto vars = jlrule->variable_names(); + // std::cerr << " apply_jl: # of vars: " << vars.size() << std::endl; + for (const auto &var : vars) { + // std::cerr << " apply_jl: var: " << var << std::endl; + auto var_sel = selector(std::string(var)); + // std::cerr << " apply_jl: var_sel = " << var_sel << std::endl; + // if (!var_sel.headeq("node")) { + // std::cerr << "selector is not a node selector; skipping." << + // std::endl; continue; + // } + auto var_tail = var_sel.tail().value(); + std::string var_str = std::string(var_sel); + // std::cerr << " apply_jl: var: " << var_sel << std::endl; + if (mvmap_.has_series(var_tail)) { + // std::cerr << " apply_jl: has series: " << var_sel << std::endl; + auto val = mvmap_.get_as_variant(var_tail, loc); + if (val.has_value()) { + // std::cerr << " apply_jl: val has value" << std::endl; + std::visit( + [&submission_data, &loc, &var_str](auto &&v) { + submission_data[var_str] = boost::json::value(v); + // std::cerr << " apply_jl: submission_data[" << var_str + // << "] = " << v << " at loc " << loc << "." + // << std::endl; + }, + *val); + } else { + std::cerr << " apply_jl: no value for " << var_sel << std::endl; + submission_data[var_str] = boost::json::value(); + } + } else { + std::cerr << " apply_jl: no series for " << var_sel << std::endl; + } + } + // std::cerr << " apply_jl: submission_data: " << submission_data + // << std::endl; + auto res = jlrule->apply(jsonlogic::json_accessor(submission_data)); + // jsonlogic::apply( + // *expression_rule, jsonlogic::data_accessor(submission_data)); + // std::cerr << " apply_jl: res: " << res << std::endl; + return jsonlogic::unpack_value(res); + }; + + return apply_jl; +} + +std::vector +where_nodes(const examplegraph::examplegraph &g, + boost::json::object &expression) { + std::vector filtered_results; + // boost::json::object exp2(expression); + + // std::cerr << " where: expression: " << expression << std::endl; + + auto nodemap = g.nodemap(); + boost::json::object submission_data; + auto apply_jl = parse_where_expression(nodemap, expression, submission_data); + nodemap.for_all([&filtered_results, &apply_jl, &nodemap, + &expression](const auto &key, const auto &loc) { + // std::cerr << " where for_all key: " << key << std::endl; + if (apply_jl(loc)) { + // std::cerr << " where: applied!" << std::endl; + filtered_results.push_back(key); + } + }); + + return filtered_results; +} diff --git a/cpp/examples/ExampleSelector/CMakeLists.txt b/cpp/examples/ExampleSelector/CMakeLists.txt new file mode 100644 index 0000000..65dec66 --- /dev/null +++ b/cpp/examples/ExampleSelector/CMakeLists.txt @@ -0,0 +1,8 @@ +add_test(ExampleSelector __init__) +add_test(ExampleSelector add) +# add_test(ExampleSelector drop) +add_custom_command( + TARGET ExampleSelector_add POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/meta.json + ${CMAKE_CURRENT_BINARY_DIR}/meta.json) \ No newline at end of file diff --git a/cpp/examples/ExampleSelector/__init__.cpp b/cpp/examples/ExampleSelector/__init__.cpp new file mode 100644 index 0000000..7724193 --- /dev/null +++ b/cpp/examples/ExampleSelector/__init__.cpp @@ -0,0 +1,29 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__init__"; +static const std::string state_name = "selector_state"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Initializes an ExampleSelector"}; + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + std::map map_selectors; + clip.set_state(state_name, map_selectors); + + return 0; +} diff --git a/cpp/examples/ExampleSelector/add.cpp b/cpp/examples/ExampleSelector/add.cpp new file mode 100644 index 0000000..ac44485 --- /dev/null +++ b/cpp/examples/ExampleSelector/add.cpp @@ -0,0 +1,58 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Adds a subselector"}; + clip.add_required("selector", "Parent Selector"); + clip.add_required("subname", "Description of new selector"); + clip.add_optional("desc", "Description", "EMPTY DESCRIPTION"); + clip.add_required_state>("selector_state", + "Internal container"); + + if (clip.parse(argc, argv)) { + return 0; + } + + + std::map sstate; + if(clip.has_state("selector_state")) { + sstate = clip.get_state>("selector_state"); + } + + auto jo = clip.get("selector"); + std::string subname = clip.get("subname"); + std::string desc = clip.get("desc"); + + std::string parentname; + try { + if(jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + parentname = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + sstate[parentname+"."+subname] = desc; + + + clip.set_state("selector_state", sstate); + clip.update_selectors(sstate); + clip.return_self(); + + return 0; +} diff --git a/cpp/examples/ExampleSelector/drop.cpp b/cpp/examples/ExampleSelector/drop.cpp new file mode 100644 index 0000000..f451413 --- /dev/null +++ b/cpp/examples/ExampleSelector/drop.cpp @@ -0,0 +1,52 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "drop"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Drops a subselector"}; + clip.add_required("selector", "Parent Selector"); + clip.add_required_state>("selector_state", + "Internal container"); + + if (clip.parse(argc, argv)) { + return 0; + } + + std::map sstate; + if(clip.has_state("selector_state")) { + sstate = clip.get_state>("selector_state"); + } + + auto jo = clip.get("selector"); + + std::string parentname; + try { + if(jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + parentname = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + sstate.erase(parentname); + + clip.set_state("selector_state", sstate); + clip.update_selectors(sstate); + clip.return_self(); + + return 0; +} diff --git a/cpp/examples/ExampleSelector/meta.json b/cpp/examples/ExampleSelector/meta.json new file mode 100644 index 0000000..d66d783 --- /dev/null +++ b/cpp/examples/ExampleSelector/meta.json @@ -0,0 +1,7 @@ +{ + "__doc__" : "For testing selectors", + "initial_selectors" : { + "nodes" : "Top level for nodes", + "edges" : "Top level for edges" + } +} \ No newline at end of file diff --git a/cpp/examples/ExampleSelector/selector.hpp b/cpp/examples/ExampleSelector/selector.hpp new file mode 100644 index 0000000..c8c7255 --- /dev/null +++ b/cpp/examples/ExampleSelector/selector.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include + +namespace examplegraph { +class selector { + std::string name; + +public: + selector(boost::json::object &jo) { + try { + if (jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY\n"; + exit(-1); + } + name = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!\n"; + exit(-1); + } + } + [[nodiscard]] std::string to_string() const { return name; } +}; + +} // namespace examplegraph diff --git a/cpp/examples/ExampleSet/CMakeLists.txt b/cpp/examples/ExampleSet/CMakeLists.txt new file mode 100644 index 0000000..b7b690e --- /dev/null +++ b/cpp/examples/ExampleSet/CMakeLists.txt @@ -0,0 +1,11 @@ +add_test(ExampleSet __init__) +add_test(ExampleSet __str__) +add_test(ExampleSet insert) +add_test(ExampleSet remove) +add_test(ExampleSet remove_if) +add_test(ExampleSet size) +add_custom_command( + TARGET ExampleSet_size POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/meta.json + ${CMAKE_CURRENT_BINARY_DIR}/meta.json) \ No newline at end of file diff --git a/cpp/examples/ExampleSet/__init__.cpp b/cpp/examples/ExampleSet/__init__.cpp new file mode 100644 index 0000000..0eea4b7 --- /dev/null +++ b/cpp/examples/ExampleSet/__init__.cpp @@ -0,0 +1,30 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include + +namespace boostjsn = boost::json; + + +static const std::string method_name = "__init__"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Initializes a ExampleSet of strings"}; + + + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + std::vector the_bag; + clip.set_state(state_name, the_bag); + + return 0; +} diff --git a/cpp/examples/ExampleSet/__str__.cpp b/cpp/examples/ExampleSet/__str__.cpp new file mode 100644 index 0000000..605fe33 --- /dev/null +++ b/cpp/examples/ExampleSet/__str__.cpp @@ -0,0 +1,38 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__str__"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Str method for ExampleSet"}; + + clip.add_required_state>(state_name, + "Internal container"); + + clip.returns("String of data."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_set = clip.get_state>(state_name); + clip.set_state(state_name, the_set); + + std::stringstream sstr; + for (auto item : the_set) { + sstr << item << " "; + } + clip.to_return(sstr.str()); + + return 0; +} diff --git a/cpp/examples/ExampleSet/insert.cpp b/cpp/examples/ExampleSet/insert.cpp new file mode 100644 index 0000000..9391f8a --- /dev/null +++ b/cpp/examples/ExampleSet/insert.cpp @@ -0,0 +1,34 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "insert"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Inserts a string into a ExampleSet"}; + clip.add_required("item", "Item to insert"); + clip.add_required_state>(state_name, + "Internal container"); + clip.returns_self(); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto item = clip.get("item"); + auto the_set = clip.get_state>(state_name); + the_set.insert(item); + clip.set_state(state_name, the_set); + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleSet/meta.json b/cpp/examples/ExampleSet/meta.json new file mode 100644 index 0000000..29135de --- /dev/null +++ b/cpp/examples/ExampleSet/meta.json @@ -0,0 +1,6 @@ +{ + "__doc__" : "A set data structure", + "initial_selectors" : { + "value" : "A value in the container" + } +} \ No newline at end of file diff --git a/cpp/examples/ExampleSet/remove.cpp b/cpp/examples/ExampleSet/remove.cpp new file mode 100644 index 0000000..d9901a2 --- /dev/null +++ b/cpp/examples/ExampleSet/remove.cpp @@ -0,0 +1,36 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "remove"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + + clippy::clippy clip{method_name, "Removes a string from an ExampleSet"}; + + clip.add_required("item", "Item to remove"); + clip.add_required_state>(state_name, + "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto item = clip.get("item"); + auto the_set = clip.get_state>(state_name); + the_set.erase(item); + clip.set_state(state_name, the_set); + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleSet/remove_if.cpp b/cpp/examples/ExampleSet/remove_if.cpp new file mode 100644 index 0000000..2c27c0e --- /dev/null +++ b/cpp/examples/ExampleSet/remove_if.cpp @@ -0,0 +1,48 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "remove_if"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Removes a string from a ExampleSet"}; + clip.add_required("expression", "Remove If Expression"); + clip.add_required_state>(state_name, "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto expression = clip.get("expression"); + auto the_set = clip.get_state>(state_name); + + // + // + // Expression here + jsonlogic::logic_rule jlrule = jsonlogic::create_logic(expression["rule"]); + + auto apply_jl = [&jlrule](int value) { + return truthy(jlrule.apply(jsonlogic::json_accessor({{"value", value}}))); + }; + + for (auto first = the_set.begin(), last = the_set.end(); first != last;) { + if (apply_jl(*first)) + first = the_set.erase(first); + else + ++first; + } + + clip.set_state(state_name, the_set); + clip.return_self(); + return 0; +} diff --git a/cpp/examples/ExampleSet/size.cpp b/cpp/examples/ExampleSet/size.cpp new file mode 100644 index 0000000..4fdf8a7 --- /dev/null +++ b/cpp/examples/ExampleSet/size.cpp @@ -0,0 +1,31 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "size"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Returns the size of the set"}; + + clip.returns("Size of set."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_set = clip.get_state>(state_name); + + clip.to_return(the_set.size()); + return 0; +} diff --git a/cpp/examples/README.md b/cpp/examples/README.md new file mode 100644 index 0000000..ac3209e --- /dev/null +++ b/cpp/examples/README.md @@ -0,0 +1,22 @@ +# C++ Examples using CLIPPy +This directory contains a series of examples of using CLIPPy for command +line configuration. + + +**NOTE:** These examples are trivial and are used for illustration and testing +purposes. In no way are we advocating sorting strings externally to +Python, for example. + + +## Examples + +- [howdy.cpp](howdy.cpp): Example of String input and output +- [sum.cpp](sum.cpp): Example of Number input and output +- [sort_edges.cpp](sort_edges.cpp): Example of VectorIntInt input and output, also contains optional Boolean. +- [sort_strings.cpp](sort_strings.cpp): Example of VectorStrings input and output, also contains optional Boolean. +- [grumpy.cpp](grumpy.cpp): Example of exception throwing in C++ backend. Grumpy always throws a std::runtime_error() +- [dataframe](dataframe): Example with a dataframe class using freestanding clippy functions (clippy) +- [oo-dataframe](oo-dataframe): Example with a dataframe class using clippy objects (ooclippy) + +## Building +Edit the `Makefile` as necessary and run `make`. diff --git a/cpp/include/clippy/clippy-object.hpp b/cpp/include/clippy/clippy-object.hpp new file mode 100644 index 0000000..d000371 --- /dev/null +++ b/cpp/include/clippy/clippy-object.hpp @@ -0,0 +1,78 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +namespace clippy { + class object; + class array; + + struct array { + using json_type = ::boost::json::array; + + array() = default; + ~array() = default; + array(const array&) = default; + array(array&&) = default; + array& operator=(const array&) = default; + array& operator=(array&&) = default; + + template + void append_json(JsonType obj) + { + data.emplace_back(std::move(obj).json()); + } + + template + void append_val(T obj) + { + data.emplace_back(std::move(obj)); + } + + json_type& json() & { return data; } + const json_type& json() const & { return data; } + json_type&& json() && { return std::move(data); } + + private: + json_type data; + }; + + struct object { + using json_type = ::boost::json::object; + + object() = default; + ~object() = default; + object(const object&) = default; + object(object&&) = default; + object& operator=(const object&) = default; + object& operator=(object&&) = default; + + object(const json_type& dat) + : data(dat) + {} + + template + void set_json(const std::string& key, JsonType val) + { + data[key] = std::move(val).json(); + } + + template + void set_val(const std::string& key, T val) + { + data[key] = ::boost::json::value_from(val); + } + + json_type& json() & { return data; } + const json_type& json() const & { return data; } + json_type&& json() && { return std::move(data); } + + private: + json_type data; + }; +} + diff --git a/cpp/include/clippy/clippy.hpp b/cpp/include/clippy/clippy.hpp new file mode 100644 index 0000000..60322d3 --- /dev/null +++ b/cpp/include/clippy/clippy.hpp @@ -0,0 +1,563 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "clippy-object.hpp" + +// #if __has_include() +// #include +// #endif + +#if __has_include("clippy-log.hpp") +#include "clippy-log.hpp" +#else +static constexpr bool LOG_JSON = false; +#endif + +#if WITH_YGM +#include +#endif /* WITH_YGM */ + +#include + +namespace clippy { + +namespace { +template +struct is_container { + enum { + value = false, + }; +}; + +template +struct is_container> { + enum { + value = true, + }; +}; + +boost::json::value asContainer(boost::json::value val, bool requiresContainer) { + if (!requiresContainer) return val; + if (val.is_array()) return val; + + boost::json::array res; + + res.emplace_back(std::move(val)); + return res; +} + +std::string clippyLogFile{"clippy.log"}; + +#if WITH_YGM +std::string userInputString; + +struct BcastInput { + void operator()(std::string inp) const { + userInputString = std::move(inp); + if (LOG_JSON) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "--in-> " << userInputString << std::endl; + } + } +}; +#endif +} // namespace + +class clippy { + public: + clippy(const std::string &name, const std::string &desc) { + get_value(m_json_config, "method_name") = name; + get_value(m_json_config, "desc") = desc; + get_value(m_json_config, "version") = std::string(CLIPPY_VERSION_NAME); + } + + /// Makes a method a member of a class \ref className and documentation \ref + /// docString. + // \todo Shall we also model the module name? + // The Python serialization module has preliminary support for modules, + // but this is currently not used. + void member_of(const std::string &className, const std::string &docString) { + get_value(m_json_config, class_name_key) = className; + get_value(m_json_config, class_desc_key) = docString; + } + + ~clippy() { + const bool requiresResponse = + !(m_json_return.is_null() && m_json_state.empty() && + m_json_overwrite_args.empty() && m_json_selectors.is_null()); + + if (requiresResponse) { + int rank = 0; +#ifdef MPI_VERSION + if (::MPI_Comm_rank(MPI_COMM_WORLD, &rank) != MPI_SUCCESS) { + MPI_Abort(MPI_COMM_WORLD, EXIT_FAILURE); + } +#endif + if (rank == 0) { + write_response(std::cout); + + if (LOG_JSON) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "<-out- "; + write_response(logfile); + logfile << std::endl; + } + } + } + } + + template + void log(std::ofstream &logfile, const M &msg) { + if (LOG_JSON) logfile << msg << std::flush; + } + + template + void log(const M &msg) { + if (!LOG_JSON) return; + + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + log(logfile, msg); + } + + template + void add_required(const std::string &name, const std::string &desc) { + add_required_validator(name); + size_t position = m_next_position++; + get_value(m_json_config, "args", name, "desc") = desc; + get_value(m_json_config, "args", name, "position") = position; + } + + template + void add_required_state(const std::string &name, const std::string &desc) { + add_required_state_validator(name); + + get_value(m_json_config, state_key, name, "desc") = desc; + } + + template + void add_optional(const std::string &name, const std::string &desc, + const T &default_val) { + add_optional_validator(name); + get_value(m_json_config, "args", name, "desc") = desc; + get_value(m_json_config, "args", name, "position") = -1; + get_value(m_json_config, "args", name, "default_val") = + boost::json::value_from(default_val); + } + + void update_selectors( + const std::map &map_selectors) { + m_json_selectors = boost::json::value_from(map_selectors); + } + + template + void returns(const std::string &desc) { + get_value(m_json_config, returns_key, "desc") = desc; + } + + void returns_self() { + get_value(m_json_config, "returns_self") = true; + m_returns_self = true; + } + + void return_self() { m_returns_self = true; } + + template + void to_return(const T &value) { + // if (detail::get_type_name() != + // m_json_config[returns_key]["type"].get()) { + // throw std::runtime_error("clippy::to_return(value): Invalid type."); + // } + m_json_return = boost::json::value_from(value); + } + + void to_return(::clippy::object value) { + m_json_return = std::move(value).json(); + } + + void to_return(::clippy::array value) { + m_json_return = std::move(value).json(); + } + + bool parse(int argc, char **argv) { + const char *JSON_FLAG = "--clippy-help"; + const char *DRYRUN_FLAG = "--clippy-validate"; + if (argc == 2 && std::string(argv[1]) == JSON_FLAG) { + if (LOG_JSON) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "<-hlp- " << m_json_config << std::endl; + } + std::cout << m_json_config; + return true; + } + std::string buf; + std::getline(std::cin, buf); + m_json_input = boost::json::parse(buf); + + if (LOG_JSON) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "--in-> " << m_json_input << std::endl; + } + validate_json_input(); + + if (argc == 2 && std::string(argv[1]) == DRYRUN_FLAG) { + return true; + } + + // Good to go for reals + return false; + } + +#if WITH_YGM + bool parse(int argc, char **argv, ygm::comm &world) { + const char *JSON_FLAG = "--clippy-help"; + const char *DRYRUN_FLAG = "--clippy-validate"; + + clippyLogFile = "clippy-" + std::to_string(world.rank()) + ".log"; + + if (argc == 2 && std::string(argv[1]) == JSON_FLAG) { + if (LOG_JSON && (world.rank() == 0)) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "<-hlp- " << m_json_config << std::endl; + } + + if (world.rank0()) { + std::cout << m_json_config; + } + return true; + } + + if (world.rank() == 0) { + std::getline(std::cin, userInputString); + world.async_bcast(BcastInput{}, userInputString); + } + + world.barrier(); + + m_json_input = boost::json::parse(userInputString); + + if (LOG_JSON && (world.rank() == 0)) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "--in-> " << m_json_input << std::endl; + } + validate_json_input(); + + if (argc == 2 && std::string(argv[1]) == DRYRUN_FLAG) { + return true; + } + + // Good to go for reals + return false; + } +#endif /* WITH_YGM */ + + template + T get(const std::string &name) { + static constexpr bool requires_container = is_container::value; + + if (has_argument(name)) { // if the argument exists + auto foo = get_value(m_json_input, name); + return boost::json::value_to( + asContainer(get_value(m_json_input, name), requires_container)); + } else { // it's an optional + // std::cerr << "optional argument found: " + name << std::endl; + return boost::json::value_to( + asContainer(get_value(m_json_config, "args", name, "default_val"), + requires_container)); + } + } + + bool has_state(const std::string &name) const { + return has_value(m_json_input, state_key, name); + } + + template + T get_state(const std::string &name) const { + return boost::json::value_to(get_value(m_json_input, state_key, name)); + } + + template + void set_state(const std::string &name, T val) { + // if no state exists (= empty), then copy it from m_json_input if it exists + // there; + // otherwise just start with an empty state. + if (m_json_state.empty()) + if (boost::json::value *stateValue = + m_json_input.get_object().if_contains(state_key)) + m_json_state = stateValue->as_object(); + + m_json_state[name] = boost::json::value_from(val); + } + + template + void overwrite_arg(const std::string &name, const T &val) { + m_json_overwrite_args[name] = boost::json::value_from(val); + } + + bool has_argument(const std::string &name) const { + return m_json_input.get_object().contains(name); + } + + bool is_class_member_function() const try { + return m_json_config.get_object().if_contains(class_name_key) != nullptr; + } catch (const std::invalid_argument &) { + return false; + } + + private: + void write_response(std::ostream &os) const { + // construct the response object + boost::json::object json_response; + + // incl. the response if it has been set + if (m_returns_self) { + json_response["returns_self"] = true; + } else if (!m_json_return.is_null()) + json_response[returns_key] = m_json_return; + + // only communicate the state if it has been explicitly set. + // no state -> no state update + if (!m_json_state.empty()) json_response[state_key] = m_json_state; + + if (!m_json_selectors.is_null()) + json_response[selectors_key] = m_json_selectors; + + // only communicate the pass by reference arguments if explicitly set + if (!m_json_overwrite_args.empty()) + json_response["references"] = m_json_overwrite_args; + + // write the response + os << json_response << std::endl; + } + + void validate_json_input() { + for (auto &kv : m_input_validators) { + kv.second(m_json_input); + } + // TODO: Warn/Check for unknown args + } + + template + void add_optional_validator(const std::string &name) { + if (m_input_validators.count(name) > 0) { + std::stringstream ss; + ss << "CLIPPy ERROR: Cannot have duplicate argument names: " << name + << "\n"; + throw std::runtime_error(ss.str()); + } + m_input_validators[name] = [name](const boost::json::value &j) { + if (!j.get_object().contains(name)) { + return; + } // Optional, only eval if present + try { + static constexpr bool requires_container = is_container::value; + + boost::json::value_to( + asContainer(get_value(j, name), requires_container)); + } catch (const std::exception &e) { + std::stringstream ss; + ss << "CLIPPy ERROR: Optional argument " << name << ": \"" << e.what() + << "\"\n"; + throw std::runtime_error(ss.str()); + } + }; + } + + template + void add_required_validator(const std::string &name) { + if (m_input_validators.count(name) > 0) { + throw std::runtime_error("Clippy:: Cannot have duplicate argument names"); + } + m_input_validators[name] = [name](const boost::json::value &j) { + if (!j.get_object().contains(name)) { + std::stringstream ss; + ss << "CLIPPy ERROR: Required argument " << name << " missing.\n"; + throw std::runtime_error(ss.str()); + } + try { + static constexpr bool requires_container = is_container::value; + + boost::json::value_to( + asContainer(get_value(j, name), requires_container)); + } catch (const std::exception &e) { + std::stringstream ss; + ss << "CLIPPy ERROR: Required argument " << name << ": \"" << e.what() + << "\"\n"; + throw std::runtime_error(ss.str()); + } + }; + } + + template + void add_required_state_validator(const std::string &name) { + // state validator keys are prefixed with "state::" + std::string key{state_key}; + + key += "::"; + key += name; + + if (m_input_validators.count(key) > 0) { + throw std::runtime_error("Clippy:: Cannot have duplicate state names"); + } + + auto state_validator = [name](const boost::json::value &j) { + // \todo check that the path j["state"][name] exists + try { + // try access path and value conversion + boost::json::value_to( + j.as_object().at(clippy::state_key).as_object().at(name)); + //~ boost::json::value_to(get_value(j, clippy::state_key, name)); + } catch (const std::exception &e) { + std::stringstream ss; + ss << "CLIPPy ERROR: state attribute " << name << ": \"" << e.what() + << "\"\n"; + throw std::runtime_error(ss.str()); + } + }; + + m_input_validators[key] = state_validator; + } + + static constexpr bool has_value(const boost::json::value &) { return true; } + + template + static bool has_value(const boost::json::value &value, const std::string &key, + const argts &...inner_keys) { + if (const boost::json::object *obj = value.if_object()) + if (const auto pos = obj->find(key); pos != obj->end()) + return has_value(pos->value(), inner_keys...); + + return false; + } + + static boost::json::value &get_value(boost::json::value &value, + const std::string &key) { + if (!value.is_object()) { + value.emplace_object(); + } + return value.get_object()[key]; + } + + template + static boost::json::value &get_value(boost::json::value &value, + const std::string &key, + const argts &...inner_keys) { + if (!value.is_object()) { + value.emplace_object(); + } + return get_value(value.get_object()[key], inner_keys...); + } + + static const boost::json::value &get_value(const boost::json::value &value, + const std::string &key) { + return value.get_object().at(key); + } + + template + static const boost::json::value &get_value(const boost::json::value &value, + const std::string &key, + const argts &...inner_keys) { + return get_value(value.get_object().at(key), inner_keys...); + } + + boost::json::value m_json_config; + boost::json::value m_json_input; + boost::json::value m_json_return; + boost::json::value m_json_selectors; + boost::json::object m_json_state; + boost::json::object m_json_overwrite_args; + bool m_returns_self = false; + + boost::json::object *m_json_input_state = nullptr; + size_t m_next_position = 0; + + std::map> + m_input_validators; + + public: + static constexpr const char *const state_key = "_state"; + static constexpr const char *const selectors_key = "_selectors"; + static constexpr const char *const returns_key = "returns"; + static constexpr const char *const class_name_key = "class_name"; + static constexpr const char *const class_desc_key = "class_desc"; +}; + +} // namespace clippy + +namespace boost::json { +void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, + const std::vector> &value) { + auto &outer_array = jv.emplace_array(); + outer_array.resize(value.size()); + + for (std::size_t i = 0; i < value.size(); ++i) { + auto &inner_array = outer_array[i].emplace_array(); + inner_array.resize(2); + inner_array[0] = value[i].first; + inner_array[1] = value[i].second; + } +} + +std::vector> tag_invoke( + boost::json::value_to_tag>>, + const boost::json::value &jv) { + std::vector> value; + + auto &outer_array = jv.get_array(); + for (const auto &inner_value : outer_array) { + const auto &inner_array = inner_value.get_array(); + value.emplace_back( + std::make_pair(inner_array[0].as_int64(), inner_array[1].as_int64())); + } + + return value; +} + +void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, + const std::vector> &value) { + auto &outer_array = jv.emplace_array(); + outer_array.resize(value.size()); + + for (std::size_t i = 0; i < value.size(); ++i) { + auto &inner_array = outer_array[i].emplace_array(); + inner_array.resize(2); + inner_array[0] = value[i].first; + inner_array[1] = value[i].second; + } +} + +std::vector> tag_invoke( + boost::json::value_to_tag>>, + const boost::json::value &jv) { + std::vector> value; + + auto &outer_array = jv.get_array(); + for (const auto &inner_value : outer_array) { + const auto &inner_array = inner_value.get_array(); + value.emplace_back( + std::make_pair(std::string(inner_array[0].as_string().c_str()), + std::string(inner_array[1].as_string().c_str()))); + } + + return value; +} +} // namespace boost::json diff --git a/cpp/include/clippy/selector.hpp b/cpp/include/clippy/selector.hpp new file mode 100644 index 0000000..af198ab --- /dev/null +++ b/cpp/include/clippy/selector.hpp @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include + +#include "boost/json.hpp" + +class selector { + std::string sel_str; + + std::vector dots; + + static std::vector make_dots(const std::string &sel_str) { + std::vector dots; + for (size_t i = 0; i < sel_str.size(); ++i) { + if (sel_str[i] == '.') { + dots.push_back(i); + } + } + dots.push_back(sel_str.size()); + return dots; + } + + public: + friend std::ostream &operator<<(std::ostream &os, const selector &sel); + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, const selector &sel); + explicit selector(const std::string &sel) + : sel_str(sel), dots(make_dots(sel_str)) {} + explicit selector(const char *sel) : selector(std::string(sel)) {} + selector(boost::json::object o) { + auto v = o.at("rule").as_object()["var"]; + sel_str = v.as_string().c_str(); + dots = make_dots(sel_str); + } + bool operator<(const selector &other) const { + return sel_str < other.sel_str; + } + operator std::string() { return sel_str; } + operator std::string() const { return sel_str; } + bool headeq(const std::string &comp) const { + return std::string_view(sel_str).substr(0, dots[0]) == comp; + } + std::optional tail() const { + if (dots.size() <= 1) { // remember that end of string is a dot + return std::nullopt; + } + return selector(sel_str.substr(dots[0] + 1)); + } +}; + +std::ostream &operator<<(std::ostream &os, const selector &sel) { + os << sel.sel_str; + return os; +} + +selector tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v) { + return v.as_object(); +} + +void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, + const selector &sel) { + std::cerr << "This should not be called." << std::endl; + // std::map o {}; + // o["expression_type"] = "jsonlogic"; + // o["rule"] = {{"var", sel.sel_str}}; + // v = {"expression_type": "jsonlogic", "rule": {"var": + // "node.degree"}}}sel.sel_str; +} \ No newline at end of file diff --git a/cpp/include/clippy/version.hpp b/cpp/include/clippy/version.hpp new file mode 100644 index 0000000..8421cf2 --- /dev/null +++ b/cpp/include/clippy/version.hpp @@ -0,0 +1,6 @@ +#pragma once + +#define CLIPPY_VERSION_MAJOR 0 +#define CLIPPY_VERSION_MINOR 5 +#define CLIPPY_VERSION_PATCH 0 +#define CLIPPY_VERSION_NAME "0.5.0" diff --git a/pyproject.toml b/py/pyproject.toml similarity index 84% rename from pyproject.toml rename to py/pyproject.toml index aa36ad7..efe7fda 100644 --- a/pyproject.toml +++ b/py/pyproject.toml @@ -31,3 +31,15 @@ optional-dependencies = {dev = {file = ["requirements-dev.txt"] }} [project.urls] Homepage = "https://github.com/LLNL/clippy" Issues = "https://github.com/LLNL/clippy/issues" + +[tool.mypy] +exclude = [ + "^build/", + "attic/", + "^venv/", + "^tests/", +] + +[tool.ruff] +lint.select = ["E", "F", "I", "UP", "B", "C4", "SIM"] +line-length = 120 \ No newline at end of file diff --git a/py/requirements-dev.txt b/py/requirements-dev.txt new file mode 100644 index 0000000..55f9c19 --- /dev/null +++ b/py/requirements-dev.txt @@ -0,0 +1,5 @@ +coverage +ruff +mypy +pytest +json-logic-qubit diff --git a/requirements.txt b/py/requirements.txt similarity index 100% rename from requirements.txt rename to py/requirements.txt diff --git a/src/clippy/__init__.py b/py/src/clippy/__init__.py similarity index 85% rename from src/clippy/__init__.py rename to py/src/clippy/__init__.py index a66808b..f8319a3 100644 --- a/src/clippy/__init__.py +++ b/py/src/clippy/__init__.py @@ -1,15 +1,15 @@ -""" This is the clippy initialization file. """ +"""This is the clippy initialization file.""" # The general flow is as follows: # Create the configurations (see comments in .config for details) # Create the logger from __future__ import annotations -import logging import importlib -from .config import _clippy_cfg -from .clippy_types import AnyDict, CLIPPY_CONFIG +import logging +from .clippy_types import CLIPPY_CONFIG, AnyDict +from .config import _clippy_cfg # Create the main configuraton object and expose it globally. cfg = CLIPPY_CONFIG(_clippy_cfg) @@ -27,15 +27,15 @@ def load_classes(): - '''For each listed backend, import the module of the same name. The + """For each listed backend, import the module of the same name. The backend should expose two functions: a classes() function that returns a dictionary of classes keyed by name, and a get_cfg() function that returns a CLIPPY_CONFIG object with backend-specific configuration. This object is then made an attribute of the global configuration (i.e., `cfg.fs.get('fs_specific_config')`). - ''' + """ for backend in cfg.get("backends"): - b = importlib.import_module(f'.backends.{backend}', package=__name__) + b = importlib.import_module(f".backends.{backend}", package=__name__) setattr(cfg, backend, b.get_cfg()) for name, c in b.classes().items(): # backend_config = importlib.import_module(f".backends.{name}.config.{name}_config") diff --git a/src/clippy/backends/__init__.py b/py/src/clippy/backends/__init__.py similarity index 100% rename from src/clippy/backends/__init__.py rename to py/src/clippy/backends/__init__.py diff --git a/src/clippy/backends/fs/__init__.py b/py/src/clippy/backends/fs/__init__.py similarity index 81% rename from src/clippy/backends/fs/__init__.py rename to py/src/clippy/backends/fs/__init__.py index 0aa959e..e49b2b7 100644 --- a/src/clippy/backends/fs/__init__.py +++ b/py/src/clippy/backends/fs/__init__.py @@ -2,32 +2,29 @@ from __future__ import annotations +import json +import logging import os +import pathlib import stat -import json import sys -import pathlib -import logging from subprocess import CalledProcessError from typing import Any - -from .execution import _validate, _run, _help -from ..version import _check_version -from ..serialization import ClippySerializable - -from ... import constants +from ... import constants as clippy_constants +from ...clippy_types import CLIPPY_CONFIG from ...error import ( ClippyConfigurationError, + ClippyInvalidSelectorError, ClippyTypeError, ClippyValidationError, - ClippyInvalidSelectorError, ) from ...selectors import Selector from ...utils import flat_dict_to_nested - -from ...clippy_types import CLIPPY_CONFIG +from ..serialization import ClippySerializable +from ..version import _check_version from .config import _fs_config_entries +from .execution import _help, _run, _validate # create a fs-specific configuration. cfg = CLIPPY_CONFIG(_fs_config_entries) @@ -46,10 +43,10 @@ def _is_user_executable(path: pathlib.Path) -> bool: gid = os.getgid() # Owner permissions - if st.st_uid == uid and mode & stat.S_IXUSR: + if st.st_uid == uid and mode & stat.S_IXUSR: # noqa: SIM114 return True # Group permissions - elif st.st_gid == gid and mode & stat.S_IXGRP: + elif st.st_gid == gid and mode & stat.S_IXGRP: # noqa: SIM114 return True # Other permissions elif mode & stat.S_IXOTH: @@ -93,13 +90,13 @@ def _create_class(name: str, path: str, topcfg: CLIPPY_CONFIG): a meta.json file in each class directory. The meta.json file typically holds the class's docstring and any initial top-level selectors as a dictionary of selector: docstring.""" - metafile = pathlib.Path(path, name, constants.CLASS_META_FILE) + metafile = pathlib.Path(path, name, clippy_constants.CLASS_META_FILE) meta = {} if metafile.exists(): - with open(metafile, "r", encoding="utf-8") as json_file: + with open(metafile, encoding="utf-8") as json_file: meta = json.load(json_file) # pull the selectors out since we don't want them in the class definition right now - selectors = meta.pop(constants.INITIAL_SELECTOR_KEY, {}) + selectors = meta.pop(clippy_constants.INITIAL_SELECTOR_KEY, {}) meta["_name"] = name meta["_path"] = path meta["_cfg"] = topcfg @@ -144,14 +141,14 @@ def _process_executable(executable: str, cls): # check to make sure we have the method name. This is so the executable can have # a different name than the actual method. - if constants.METHODNAME_KEY not in j: + if clippy_constants.METHODNAME_KEY not in j: raise ClippyConfigurationError("No method_name in " + executable) # check version if not _check_version(j): raise ClippyConfigurationError("Invalid version information in " + executable) - docstring = j.get(constants.DOCSTRING_KEY, "") - args: dict[str, dict] = j.get(constants.ARGS_KEY, {}) # this is now a dict. + docstring = j.get(clippy_constants.DOCSTRING_KEY, "") + args: dict[str, dict] = j.get(clippy_constants.ARGS_KEY, {}) # this is now a dict. # Create a list of descriptions ordered by position (excluding position=-1) ordered_descs = [] @@ -175,21 +172,17 @@ def _process_executable(executable: str, cls): docstring += f" {desc}\n" # if we don't explicitly pass the method name, use the name of the exe. - method = j.get(constants.METHODNAME_KEY, os.path.basename(executable)) + method = j.get(clippy_constants.METHODNAME_KEY, os.path.basename(executable)) if hasattr(cls, method) and not method.startswith("__"): - cls.logger.warning( - f"Overwriting existing method {method} for class {cls} with executable {executable}" - ) + cls.logger.warning(f"Overwriting existing method {method} for class {cls} with executable {executable}") _define_method(cls, method, executable, docstring, args) return cls -def _define_method( - cls, name: str, executable: str, docstr: str, arguments: dict[str, dict] | None -): # pylint: disable=too-complex +def _define_method(cls, name: str, executable: str, docstr: str, arguments: dict[str, dict] | None): # pylint: disable=too-complex """Defines a method on a given class.""" if arguments is None: - arguments = dict() + arguments = {} def m(self, *args, **kwargs): """ @@ -208,7 +201,7 @@ def m(self, *args, **kwargs): # .. add state # argdict[STATE_KEY] = self._state - argdict[constants.STATE_KEY] = getattr(self, constants.STATE_KEY) + argdict[clippy_constants.STATE_KEY] = getattr(self, clippy_constants.STATE_KEY) # ~ for key in statedesc: # ~ statej[key] = getattr(self, key) @@ -216,9 +209,8 @@ def m(self, *args, **kwargs): numpositionals = len(args) for argdesc in arguments: value = arguments[argdesc] - if "position" in value: - if 0 <= value["position"] < numpositionals: - argdict[argdesc] = args[value["position"]] + if 0 <= value.get("position", -1) < numpositionals: + argdict[argdesc] = args[value["position"]] # .. add keyword arguments argdict.update(kwargs) @@ -229,13 +221,14 @@ def m(self, *args, **kwargs): raise ClippyValidationError(stderr) # call executable and create json output + outj = _run(executable, argdict, self.logger) # if we have results that have keys that are in our # kwargs, let's update the kwarg references. Works # for lists and dicts only. for kw, kwval in kwargs.items(): - if kw in outj.get(constants.REFERENCE_KEY, {}): + if kw in outj.get(clippy_constants.REFERENCE_KEY, {}): kwval.clear() if isinstance(kwval, dict): kwval.update(outj[kw]) @@ -245,26 +238,24 @@ def m(self, *args, **kwargs): raise ClippyTypeError() # dump any output - if constants.OUTPUT_KEY in outj: - print(outj[constants.OUTPUT_KEY]) + if clippy_constants.OUTPUT_KEY in outj: + print(outj[clippy_constants.OUTPUT_KEY]) # update state according to json output - if constants.STATE_KEY in outj: - setattr(self, constants.STATE_KEY, outj[constants.STATE_KEY]) + if clippy_constants.STATE_KEY in outj: + setattr(self, clippy_constants.STATE_KEY, outj[clippy_constants.STATE_KEY]) # update selectors if necessary. - if constants.SELECTOR_KEY in outj: - d = flat_dict_to_nested(outj[constants.SELECTOR_KEY]) + if clippy_constants.SELECTOR_KEY in outj: + d = flat_dict_to_nested(outj[clippy_constants.SELECTOR_KEY]) for topsel, subsels in d.items(): if not hasattr(self, topsel): - raise ClippyInvalidSelectorError( - f"selector {topsel} not found in class; aborting" - ) + raise ClippyInvalidSelectorError(f"selector {topsel} not found in class; aborting") getattr(self, topsel)._import_from_dict(subsels) # return result - if outj.get(constants.SELF_KEY, False): + if outj.get(clippy_constants.SELF_KEY, False): return self - return outj.get(constants.RETURN_KEY) + return outj.get(clippy_constants.RETURN_KEY) # end of nested def m diff --git a/src/clippy/backends/fs/config.py b/py/src/clippy/backends/fs/config.py similarity index 100% rename from src/clippy/backends/fs/config.py rename to py/src/clippy/backends/fs/config.py diff --git a/src/clippy/backends/fs/constants.py b/py/src/clippy/backends/fs/constants.py similarity index 56% rename from src/clippy/backends/fs/constants.py rename to py/src/clippy/backends/fs/constants.py index fb85c5b..d3e3944 100644 --- a/src/clippy/backends/fs/constants.py +++ b/py/src/clippy/backends/fs/constants.py @@ -4,9 +4,3 @@ DRY_RUN_FLAG = "--clippy-validate" # the flag to pass to get detailed help for constructing the class HELP_FLAG = "--clippy-help" - -# Arguments for execution progress -PROGRESS_START_KEY = "progress_start" -PROGRESS_END_KEY = "progress_end" -PROGRESS_INC_KEY = "progress_inc" -PROGRESS_SET_KEY = "progress_set" diff --git a/src/clippy/backends/fs/execution.py b/py/src/clippy/backends/fs/execution.py similarity index 81% rename from src/clippy/backends/fs/execution.py rename to py/src/clippy/backends/fs/execution.py index 60523b6..0a1b248 100644 --- a/src/clippy/backends/fs/execution.py +++ b/py/src/clippy/backends/fs/execution.py @@ -1,20 +1,27 @@ """ - Functions to execute backend programs. +Functions to execute backend programs. """ from __future__ import annotations + +import contextlib import json import logging -import select import os - +import select import subprocess -from ...clippy_types import AnyDict + from ... import cfg +from ...clippy_types import AnyDict +from ..serialization import decode_clippy_json, encode_clippy_json from .constants import DRY_RUN_FLAG, HELP_FLAG -from ..serialization import encode_clippy_json, decode_clippy_json +class NonZeroReturnCodeError(Exception): + def __init__(self, execcmd, return_code, extramsg, message="returned non-zero exit code"): + self.return_code = return_code + self.execcmd = execcmd + super().__init__(f"{execcmd} {message}: {return_code}\n{extramsg}") def _stream_exec( cmd: list[str], @@ -36,7 +43,7 @@ def _stream_exec( already be set. """ - logger.debug(f'Submission = {submission_dict}') + logger.debug(f"Submission = {submission_dict}") # PP support passing objects # ~ cmd_stdin = json.dumps(submission_dict) cmd_stdin = json.dumps(submission_dict, default=encode_clippy_json) @@ -47,7 +54,7 @@ def _stream_exec( stderr_lines = [] with subprocess.Popen( - cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8' + cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf8" ) as proc: assert proc.stdin is not None assert proc.stdout is not None @@ -110,10 +117,8 @@ def _stream_exec( # Process any remaining buffered data if stdout_buffer.strip(): - try: + with contextlib.suppress(json.JSONDecodeError): d = json.loads(stdout_buffer, object_hook=decode_clippy_json) - except json.JSONDecodeError: - pass if stderr_buffer.strip(): stderr_lines.append(stderr_buffer) @@ -128,26 +133,24 @@ def _stream_exec( if not d: return None, stderr, proc.returncode if stderr: - logger.debug('Received stderr: %s', stderr) + logger.debug("Received stderr: %s", stderr) if proc.returncode != 0: logger.debug("Process returned %d", proc.returncode) - logger.debug('run(): final stdout = %s', d) + logger.debug("run(): final stdout = %s", d) return (d, stderr, proc.returncode) -def _validate( - cmd: str | list[str], dct: AnyDict, logger: logging.Logger -) -> tuple[bool, str]: - ''' +def _validate(cmd: str | list[str], dct: AnyDict, logger: logging.Logger) -> tuple[bool, str]: + """ Converts the dictionary dct into a json file and calls executable cmd with the DRY_RUN_FLAG. Returns True/False (validation successful) and any stderr. - ''' + """ if isinstance(cmd, str): cmd = [cmd] - execcmd = cfg.get('validate_cmd_prefix').split() + cmd + [DRY_RUN_FLAG] + execcmd = cfg.get("validate_cmd_prefix").split() + cmd + [DRY_RUN_FLAG] logger.debug("Validating %s", cmd) _, stderr, retcode = _stream_exec(execcmd, dct, logger, validate=True) @@ -155,33 +158,33 @@ def _validate( def _run(cmd: str | list[str], dct: AnyDict, logger: logging.Logger) -> AnyDict: - ''' + """ converts the dictionary dct into a json file and calls executable cmd. Prepends cmd_prefix configuration, if any. - ''' + """ if isinstance(cmd, str): cmd = [cmd] - execcmd = cfg.get('cmd_prefix').split() + cmd - logger.debug('Running %s', execcmd) + execcmd = cfg.get("cmd_prefix").split() + cmd + logger.debug("Running %s", execcmd) # should we do something with stderr? - output, _, retcode = _stream_exec(execcmd, dct, logger, validate=False) + output, stderr, retcode = _stream_exec(execcmd, dct, logger, validate=False) if retcode != 0: - logger.warning("Process returned non-zero return code: %d", retcode) + raise NonZeroReturnCodeError(execcmd, retcode, stderr) return output or {} def _help(cmd: str | list[str], dct: AnyDict, logger: logging.Logger) -> AnyDict: - ''' + """ Retrieves the help output from the clippy command. Prepends validate_cmd_prefix if set and appends HELP_FLAG. Unlike `_validate()`, does not append DRY_RUN_FLAG, and returns the output. - ''' + """ if isinstance(cmd, str): cmd = [cmd] - execcmd = cfg.get('validate_cmd_prefix').split() + cmd + [HELP_FLAG] - logger.debug('Running %s', execcmd) + execcmd = cfg.get("validate_cmd_prefix").split() + cmd + [HELP_FLAG] + logger.debug("Running %s", execcmd) # should we do something with stderr? output, _, _ = _stream_exec(execcmd, dct, logger, validate=True) diff --git a/src/clippy/backends/serialization.py b/py/src/clippy/backends/serialization.py similarity index 96% rename from src/clippy/backends/serialization.py rename to py/src/clippy/backends/serialization.py index 2edd4e6..7a8f626 100644 --- a/src/clippy/backends/serialization.py +++ b/py/src/clippy/backends/serialization.py @@ -3,12 +3,14 @@ """ from __future__ import annotations -import jsonlogic as jl + from typing import Any -from ..error import ClippySerializationError + +import jsonlogic as jl + from .. import _dynamic_types from ..clippy_types import AnyDict - +from ..error import ClippySerializationError # TODO: SAB 20240204 complete typing here. @@ -70,9 +72,7 @@ def from_serial(cls, o: AnyDict): raise ClippySerializationError("__clippy_type__.__class__ is unspecified") if type_name not in _dynamic_types: - raise ClippySerializationError( - f'"{type_name}" is not a known type, please clippy import it.' - ) + raise ClippySerializationError(f'"{type_name}" is not a known type, please clippy import it.') # get the type to deserialize into from the _dynamic_types dict # this does not account for the module the type may exist in diff --git a/src/clippy/backends/version.py b/py/src/clippy/backends/version.py similarity index 99% rename from src/clippy/backends/version.py rename to py/src/clippy/backends/version.py index 64f5779..7979af1 100644 --- a/src/clippy/backends/version.py +++ b/py/src/clippy/backends/version.py @@ -3,9 +3,11 @@ """ from __future__ import annotations + from semver import Version -from ..clippy_types import AnyDict + from .. import cfg +from ..clippy_types import AnyDict def _check_version(output_dict: AnyDict | None) -> bool: diff --git a/src/clippy/clippy_types.py b/py/src/clippy/clippy_types.py similarity index 92% rename from src/clippy/clippy_types.py rename to py/src/clippy/clippy_types.py index 8247a35..a58027f 100644 --- a/src/clippy/clippy_types.py +++ b/py/src/clippy/clippy_types.py @@ -7,7 +7,8 @@ """ import os -from typing import Any, Optional +from typing import Any + from .error import ClippyConfigurationError # AnyDict is a convenience type so we can find places @@ -15,7 +16,7 @@ AnyDict = dict[str, Any] # CONFIG_ENTRY is a convenience type for use in CLIPPY_CONFIG. -CONFIG_ENTRY = tuple[Optional[str], Any] +CONFIG_ENTRY = tuple[str | None, Any] # CLIPPY_CONFIG holds configuration items for both diff --git a/src/clippy/config.py b/py/src/clippy/config.py similarity index 93% rename from src/clippy/config.py rename to py/src/clippy/config.py index 0cb6567..f20c598 100644 --- a/src/clippy/config.py +++ b/py/src/clippy/config.py @@ -1,9 +1,10 @@ # pylint: disable=consider-using-namedtuple-or-dataclass -''' This holds a dictionary containing global configuration variables for clippy.''' +"""This holds a dictionary containing global configuration variables for clippy.""" # The format is config_key: (environment variable or None, default value) import logging + from .clippy_types import CONFIG_ENTRY _clippy_cfg: dict[str, CONFIG_ENTRY] = { diff --git a/src/clippy/constants.py b/py/src/clippy/constants.py similarity index 100% rename from src/clippy/constants.py rename to py/src/clippy/constants.py diff --git a/src/clippy/error.py b/py/src/clippy/error.py similarity index 86% rename from src/clippy/error.py rename to py/src/clippy/error.py index e947f18..557ab20 100644 --- a/src/clippy/error.py +++ b/py/src/clippy/error.py @@ -3,52 +3,52 @@ # # SPDX-License-Identifier: MIT -""" This file contains custom Clippy errors. """ +"""This file contains custom Clippy errors.""" class ClippyError(Exception): - ''' + """ This is a top-level custom exception for Clippy. - ''' + """ class ClippyConfigurationError(ClippyError): - ''' + """ This error represents a configuration error on user input. - ''' + """ class ClippyBackendError(ClippyError): - ''' + """ This error should be thrown when the backend returns an abend. - ''' + """ class ClippyValidationError(ClippyError): - ''' + """ This error represents a validation error in the inputs to a clippy job. - ''' + """ class ClippySerializationError(ClippyError): - ''' + """ This error should be thrown when clippy object serialization fails - ''' + """ class ClippyClassInconsistencyError(ClippyError): - ''' + """ This error represents a class inconsistency error (name or docstring mismatch). - ''' + """ class ClippyTypeError(ClippyError): - ''' + """ This error represents an error with the type of data being passed to the back end. - ''' + """ class ClippyInvalidSelectorError(ClippyError): - ''' + """ This error represents an error with a selector that is not defined for a given clippy class. - ''' + """ diff --git a/src/clippy/selectors.py b/py/src/clippy/selectors.py similarity index 82% rename from src/clippy/selectors.py rename to py/src/clippy/selectors.py index 9e8fd4a..5854606 100644 --- a/src/clippy/selectors.py +++ b/py/src/clippy/selectors.py @@ -1,6 +1,7 @@ """Custom selectors for clippy.""" from __future__ import annotations + import jsonlogic as jl from . import constants @@ -11,14 +12,10 @@ class Selector(jl.Variable): """A Selector represents a single variable.""" def __init__(self, parent: Selector | None, name: str, docstr: str): - super().__init__( - name, docstr - ) # op and o2 are None to represent this as a variable. + super().__init__(name, docstr) # op and o2 are None to represent this as a variable. self.parent = parent self.name = name - self.fullname: str = ( - self.name if self.parent is None else f"{self.parent.fullname}.{self.name}" - ) + self.fullname: str = self.name if self.parent is None else f"{self.parent.fullname}.{self.name}" self.subselectors: set[Selector] = set() def __hash__(self): @@ -37,10 +34,8 @@ def hierarchy(self, acc: list[tuple[str, str]] | None = None): def describe(self): hier = self.hierarchy() - maxlen = max((len(sub_desc[0]) for sub_desc in hier)) - return "\n".join( - f"{sub_desc[0]:<{maxlen+2}} {sub_desc[1]}" for sub_desc in hier - ) + maxlen = max(len(sub_desc[0]) for sub_desc in hier) + return "\n".join(f"{sub_desc[0]:<{maxlen + 2}} {sub_desc[1]}" for sub_desc in hier) def __str__(self): return repr(self.prepare()) diff --git a/src/clippy/utils.py b/py/src/clippy/utils.py similarity index 63% rename from src/clippy/utils.py rename to py/src/clippy/utils.py index 23816a3..793ca06 100644 --- a/src/clippy/utils.py +++ b/py/src/clippy/utils.py @@ -1,10 +1,10 @@ """ - Utility functions +Utility functions """ from .clippy_types import AnyDict -from .error import ClippyInvalidSelectorError from .constants import SELECTOR_KEY +from .error import ClippyInvalidSelectorError def flat_dict_to_nested(input_dict: AnyDict) -> AnyDict: @@ -16,23 +16,19 @@ def flat_dict_to_nested(input_dict: AnyDict) -> AnyDict: output_dict: AnyDict = {} for k, v in input_dict.items(): # k is dotted - if '.' not in k: + if "." not in k: raise ClippyInvalidSelectorError("cannot set top-level selectors") - *path, last = k.split('.') - if last.startswith('_'): - raise ClippyInvalidSelectorError( - "selectors must not start with an underscore." - ) + *path, last = k.split(".") + if last.startswith("_"): + raise ClippyInvalidSelectorError("selectors must not start with an underscore.") curr_nest = output_dict for p in path: - if p.startswith('_'): - raise ClippyInvalidSelectorError( - "selectors must not start with an underscore." - ) + if p.startswith("_"): + raise ClippyInvalidSelectorError("selectors must not start with an underscore.") curr_nest.setdefault(p, {}) curr_nest[p].setdefault(SELECTOR_KEY, {}) curr_nest = curr_nest[p][SELECTOR_KEY] - curr_nest.setdefault(last, {'__doc__': v}) + curr_nest.setdefault(last, {"__doc__": v}) return output_dict diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 4cc3f4a..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,7 +0,0 @@ -coverage>=5.5 -flake8 -mypy -pylint>=2.15,<3 -pyproj>=3.6,<4 -pytest>=7,<8 -json-logic-qubit diff --git a/test/conftest.py b/test/conftest.py deleted file mode 100644 index 7e22273..0000000 --- a/test/conftest.py +++ /dev/null @@ -1,2 +0,0 @@ -def pytest_addoption(parser): - parser.addoption("--backend", action="store") diff --git a/test/run_tests.sh b/test/run_tests.sh new file mode 100755 index 0000000..a0d46a8 --- /dev/null +++ b/test/run_tests.sh @@ -0,0 +1,6 @@ +#!/bin/sh +PROJ_ROOT_DIR=$(pwd)/.. +CPP_BUILD_DIR=$PROJ_ROOT_DIR/cpp/build +export PYTHONPATH=$PROJ_ROOT_DIR/py/src:$PYTHONPATH +export CLIPPY_BACKEND_PATH=$CPP_BUILD_DIR/examples +pytest \ No newline at end of file diff --git a/test/test_clippy.py b/test/test_clippy.py index 5cbfcf8..cbd2660 100644 --- a/test/test_clippy.py +++ b/test/test_clippy.py @@ -6,7 +6,7 @@ import jsonlogic as jl import clippy from clippy.error import ClippyValidationError, ClippyInvalidSelectorError - +from clippy.backends.fs.execution import NonZeroReturnCodeError import logging clippy.logger.setLevel(logging.WARN) @@ -14,191 +14,195 @@ @pytest.fixture() -def testbag(): - return clippy.TestBag() +def examplebag(): + return clippy.ExampleBag() @pytest.fixture() -def testset(): - return clippy.TestSet() +def exampleset(): + return clippy.ExampleSet() @pytest.fixture() -def testfun(): - return clippy.TestFunctions() +def examplefunction(): + return clippy.ExampleFunctions() @pytest.fixture() -def testsel(): - return clippy.TestSelector() +def exampleselector(): + return clippy.ExampleSelector() @pytest.fixture() -def testgraph(): - return clippy.TestGraph() +def examplegraph(): + return clippy.ExampleGraph() def test_imports(): - assert "TestBag" in clippy.__dict__ - + assert "ExampleBag" in clippy.__dict__ -def test_bag(testbag): - testbag.insert(41) - assert testbag.size() == 1 - testbag.insert(42) - assert testbag.size() == 2 - testbag.insert(41) - assert testbag.size() == 3 - testbag.remove(41) - assert testbag.size() == 2 - testbag.remove(99) - assert testbag.size() == 2 +def test_bag(examplebag): + examplebag.insert(41) + assert examplebag.size() == 1 + examplebag.insert(42) + assert examplebag.size() == 2 + examplebag.insert(41) + assert examplebag.size() == 3 + examplebag.remove(41) + assert examplebag.size() == 2 + examplebag.remove(99) + assert examplebag.size() == 2 -def test_clippy_call_with_string(testfun): - assert testfun.call_with_string("Seth") == "Howdy, Seth" +def test_clippy_call_with_string(examplefunction): + assert examplefunction.call_with_string("Seth") == "Howdy, Seth" with pytest.raises(ClippyValidationError): - testfun.call_with_string() + examplefunction.call_with_string() -def test_expression_gt_gte(testbag): - testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) - assert testbag.size() == 6 - testbag.remove_if(testbag.value > 51) - assert testbag.size() == 5 - testbag.remove_if(testbag.value >= 50) - assert testbag.size() == 3 - testbag.remove_if(testbag.value >= 99) - assert testbag.size() == 3 +def test_expression_gt_gte(examplebag): + examplebag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + assert examplebag.size() == 6 + examplebag.remove_if(examplebag.value > 51) + assert examplebag.size() == 5 + examplebag.remove_if(examplebag.value >= 50) + assert examplebag.size() == 3 + examplebag.remove_if(examplebag.value >= 99) + assert examplebag.size() == 3 -def test_expression_lt_lte(testbag): - testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) - testbag.remove_if(testbag.value < 42) - assert testbag.size() == 4 - testbag.remove_if(testbag.value <= 51) - assert testbag.size() == 1 +def test_expression_lt_lte(examplebag): + examplebag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + examplebag.remove_if(examplebag.value < 42) + assert examplebag.size() == 4 + examplebag.remove_if(examplebag.value <= 51) + assert examplebag.size() == 1 -def test_expression_eq_neq(testbag): - testbag.insert(10).insert(11).insert(12) - assert testbag.size() == 3 - testbag.remove_if(testbag.value != 11) - assert testbag.size() == 1 - testbag.remove_if(testbag.value == 11) - assert testbag.size() == 0 +def test_expression_eq_neq(examplebag): + examplebag.insert(10).insert(11).insert(12) + assert examplebag.size() == 3 + examplebag.remove_if(examplebag.value != 11) + assert examplebag.size() == 1 + examplebag.remove_if(examplebag.value == 11) + assert examplebag.size() == 0 -def test_expresssion_add(testbag): - testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) - testbag.remove_if(testbag.value + 30 > 70) - assert testbag.size() == 1 +def test_expresssion_add(examplebag): + examplebag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + examplebag.remove_if(examplebag.value + 30 > 70) + assert examplebag.size() == 1 -def test_expression_sub(testbag): - testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) - testbag.remove_if(testbag.value - 30 > 0) - assert testbag.size() == 1 +def test_expression_sub(examplebag): + examplebag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + examplebag.remove_if(examplebag.value - 30 > 0) + assert examplebag.size() == 1 -def test_expression_mul_div(testbag): - testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) - testbag.remove_if(testbag.value * 2 / 4 > 10) - assert testbag.size() == 1 +def test_expression_mul_div(examplebag): + examplebag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + examplebag.remove_if(examplebag.value * 2 / 4 > 10) + assert examplebag.size() == 1 -def test_expression_or(testbag): - testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) - testbag.remove_if((testbag.value < 41) | (testbag.value > 49)) - assert testbag.size() == 2 # 41, 42 +def test_expression_or(examplebag): + examplebag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + examplebag.remove_if((examplebag.value < 41) | (examplebag.value > 49)) + assert examplebag.size() == 2 # 41, 42 -def test_expression_and(testbag): - testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) - testbag.remove_if((testbag.value > 40) & (testbag.value < 50)) - assert testbag.size() == 4 # 10, 50, 51, 52 +def test_expression_and(examplebag): + examplebag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + examplebag.remove_if((examplebag.value > 40) & (examplebag.value < 50)) + assert examplebag.size() == 4 # 10, 50, 51, 52 # TODO: not yet implemented -# def test_expression_floordiv(testbag): -# testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) -# testbag.remove_if(testbag.value * 2 // 4.2 > 10) -# assert testbag.size() == 1 +# def test_expression_floordiv(examplebag): +# examplebag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) +# examplebag.remove_if(examplebag.value * 2 // 4.2 > 10) +# assert examplebag.size() == 1 -def test_expression_mod(testbag): - testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) - testbag.remove_if(testbag.value % 2 == 0) - assert testbag.size() == 2 +def test_expression_mod(examplebag): + examplebag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + examplebag.remove_if(examplebag.value % 2 == 0) + assert examplebag.size() == 2 # TODO: not yet implemented -# def test_expression_pow(testbag): -# testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) -# testbag.remove_if(testbag.value**2 > 1000) -# assert testbag.size() == 2 +# def test_expression_pow(examplebag): +# examplebag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) +# examplebag.remove_if(examplebag.value**2 > 1000) +# assert examplebag.size() == 2 + + +def test_clippy_returns_int(examplefunction): + assert examplefunction.returns_int() == 42 -def test_clippy_returns_int(testfun): - assert testfun.returns_int() == 42 +def test_clippy_throws(examplefunction): + with pytest.raises(NonZeroReturnCodeError, match="I'm Grumpy!"): + examplefunction.throws_error() -def test_clippy_returns_string(testfun): - assert testfun.returns_string() == "asdf" +def test_clippy_returns_string(examplefunction): + assert examplefunction.returns_string() == "asdf" -def test_clippy_returns_bool(testfun): - assert testfun.returns_bool() +def test_clippy_returns_bool(examplefunction): + assert examplefunction.returns_bool() -def test_clippy_returns_dict(testfun): - d = testfun.returns_dict() +def test_clippy_returns_dict(examplefunction): + d = examplefunction.returns_dict() assert len(d) == 3 assert d.get("a") == 1 assert d.get("b") == 2 assert d.get("c") == 3 -def test_clippy_returns_vec_int(testfun): - assert testfun.returns_vec_int() == [0, 1, 2, 3, 4, 5] +def test_clippy_returns_vec_int(examplefunction): + assert examplefunction.returns_vec_int() == [0, 1, 2, 3, 4, 5] -def test_clippy_returns_optional_string(testfun): - assert testfun.call_with_optional_string() == "Howdy, World" - assert testfun.call_with_optional_string(name="Seth") == "Howdy, Seth" +def test_clippy_returns_optional_string(examplefunction): + assert examplefunction.call_with_optional_string() == "Howdy, World" + assert examplefunction.call_with_optional_string(name="Seth") == "Howdy, Seth" -def test_selectors(testsel): - assert hasattr(testsel, "nodes") +def test_selectors(exampleselector): + assert hasattr(exampleselector, "nodes") - testsel.add(testsel.nodes, "b", desc="docstring for nodes.b").add( - testsel.nodes.b, "c", desc="docstring for nodes.b.c" + exampleselector.add(exampleselector.nodes, "b", desc="docstring for nodes.b").add( + exampleselector.nodes.b, "c", desc="docstring for nodes.b.c" ) - assert hasattr(testsel.nodes, "b") - assert hasattr(testsel.nodes.b, "c") - assert testsel.nodes.b.__doc__ == "docstring for nodes.b" - assert testsel.nodes.b.c.__doc__ == "docstring for nodes.b.c" + assert hasattr(exampleselector.nodes, "b") + assert hasattr(exampleselector.nodes.b, "c") + assert exampleselector.nodes.b.__doc__ == "docstring for nodes.b" + assert exampleselector.nodes.b.c.__doc__ == "docstring for nodes.b.c" - assert isinstance(testsel.nodes.b, jl.Variable) - assert isinstance(testsel.nodes.b.c, jl.Variable) + assert isinstance(exampleselector.nodes.b, jl.Variable) + assert isinstance(exampleselector.nodes.b.c, jl.Variable) with pytest.raises(ClippyInvalidSelectorError): - testsel.add(testsel.nodes, "_bad", desc="this is a bad selector name") + exampleselector.add(exampleselector.nodes, "_bad", desc="this is a bad selector name") # with pytest.raises(ClippyInvalidSelectorError): - # testsel.add(testsel, 'bad', desc="this is a top-level selector") + # exampleselector.add(exampleselector, 'bad', desc="this is a top-level selector") -def test_graph(testgraph): - testgraph.add_edge("a", "b").add_edge("b", "c").add_edge("a", "c").add_edge( - "c", "d" - ).add_edge("d", "e").add_edge("e", "f").add_edge("f", "g").add_edge("e", "g") +def test_graph(examplegraph): + examplegraph.add_edge("a", "b").add_edge("b", "c").add_edge("a", "c").add_edge("c", "d").add_edge("d", "e").add_edge( + "e", "f" + ).add_edge("f", "g").add_edge("e", "g") - assert testgraph.nv() == 7 - assert testgraph.ne() == 8 + assert examplegraph.nv() == 7 + assert examplegraph.ne() == 8 - testgraph.add_series(testgraph.node, "degree", desc="node degrees") - testgraph.degree(testgraph.node.degree) - c_e_only = testgraph.dump2(testgraph.node.degree, where=testgraph.node.degree > 2) + examplegraph.add_series(examplegraph.node, "degree", desc="node degrees") + examplegraph.degree(examplegraph.node.degree) + c_e_only = examplegraph.dump2(examplegraph.node.degree, where=examplegraph.node.degree > 2) assert "c" in c_e_only and "e" in c_e_only and len(c_e_only) == 2