Initial commit: JMESPath Testing Tool

- React-based web application for testing JMESPath expressions
- macOS-first containerization with Apple container command
- Bootstrap UI with real-time evaluation
- GitHub Actions CI/CD pipeline
- Docker fallback support
- Comprehensive documentation and development scripts
This commit is contained in:
2026-01-18 13:19:07 +01:00
commit c09e545637
26 changed files with 19427 additions and 0 deletions

69
src/App.css Normal file
View File

@@ -0,0 +1,69 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
padding: 20px;
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Custom card styling */
.card {
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-header {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
}
/* Button hover effects */
.btn {
transition: all 0.2s ease;
}
.btn:hover {
transform: translateY(-1px);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.header-section {
padding: 1rem 0;
}
.display-4 {
font-size: 2rem;
}
.lead {
font-size: 1rem;
}
}

240
src/App.js Normal file
View File

@@ -0,0 +1,240 @@
import React, { useState, useEffect } from 'react';
import jmespath from 'jmespath';
import './App.css';
function App() {
const [jmespathExpression, setJmespathExpression] = useState('people[0].name');
const [jsonData, setJsonData] = useState(`{
"people": [
{
"name": "John Doe",
"age": 30,
"city": "New York"
},
{
"name": "Jane Smith",
"age": 25,
"city": "Los Angeles"
}
],
"total": 2
}`);
const [result, setResult] = useState('');
const [error, setError] = useState('');
const [jsonError, setJsonError] = useState('');
const evaluateExpression = () => {
try {
// Clear previous errors
setError('');
setJsonError('');
// Validate and parse JSON
let parsedData;
try {
parsedData = JSON.parse(jsonData);
} catch (jsonErr) {
setJsonError(`Invalid JSON: ${jsonErr.message}`);
setResult('');
return;
}
// Evaluate JMESPath expression
const queryResult = jmespath.search(parsedData, jmespathExpression);
// Format the result
if (queryResult === null || queryResult === undefined) {
setResult('null');
} else {
setResult(JSON.stringify(queryResult, null, 2));
}
} catch (jmesErr) {
setError(`JMESPath Error: ${jmesErr.message}`);
setResult('');
}
};
// Auto-evaluate when inputs change
useEffect(() => {
if (jmespathExpression && jsonData) {
evaluateExpression();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [jmespathExpression, jsonData]);
const handleJmespathChange = (e) => {
setJmespathExpression(e.target.value);
};
const handleJsonChange = (e) => {
setJsonData(e.target.value);
};
const formatJson = () => {
try {
const parsed = JSON.parse(jsonData);
setJsonData(JSON.stringify(parsed, null, 2));
} catch (err) {
// If JSON is invalid, don't format
}
};
const clearAll = () => {
setJmespathExpression('');
setJsonData('');
setResult('');
setError('');
setJsonError('');
};
const loadSample = () => {
setJmespathExpression('people[*].name');
setJsonData(`{
"people": [
{
"name": "Alice Johnson",
"age": 28,
"city": "Chicago",
"skills": ["JavaScript", "React", "Node.js"]
},
{
"name": "Bob Wilson",
"age": 35,
"city": "Seattle",
"skills": ["Python", "Django", "PostgreSQL"]
},
{
"name": "Carol Davis",
"age": 32,
"city": "Austin",
"skills": ["Java", "Spring", "MySQL"]
}
],
"total": 3,
"department": "Engineering"
}`);
};
return (
<div className="container-fluid">
{/* Header Section */}
<div className="header-section">
<div className="container">
<div className="row">
<div className="col-12 text-center">
<h1 className="display-4 mb-3">JMESPath Testing Tool</h1>
<p className="lead">
Validate and test JMESPath expressions against JSON data in real-time.
Enter your JMESPath query and JSON data below to see the results instantly.
</p>
</div>
</div>
</div>
</div>
<div className="container-fluid content-section">
<div className="row h-100">
{/* JMESPath Expression Input - Middle Section */}
<div className="col-12 mb-3">
<div className="card">
<div className="card-header d-flex justify-content-between align-items-center">
<h5 className="mb-0">
<i className="bi bi-search me-2"></i>
JMESPath Expression
</h5>
<div>
<button
className="btn btn-outline-primary btn-sm me-2"
onClick={loadSample}
title="Load sample data"
>
Load Sample
</button>
<button
className="btn btn-outline-danger btn-sm"
onClick={clearAll}
title="Clear all inputs"
>
Clear All
</button>
</div>
</div>
<div className="card-body">
<input
type="text"
className={`form-control jmespath-input ${error ? 'error' : ''}`}
value={jmespathExpression}
onChange={handleJmespathChange}
placeholder="Enter JMESPath expression (e.g., people[*].name)"
/>
{error && (
<div className="alert alert-danger mt-2 mb-0">
<small>{error}</small>
</div>
)}
</div>
</div>
</div>
{/* Bottom Row - JSON Input and Results */}
<div className="col-md-6 mb-3">
<div className="card h-100">
<div className="card-header d-flex justify-content-between align-items-center">
<h5 className="mb-0">
<i className="bi bi-file-code me-2"></i>
JSON Data
</h5>
<button
className="btn btn-outline-secondary btn-sm"
onClick={formatJson}
title="Format JSON"
>
Format JSON
</button>
</div>
<div className="card-body input-section">
<div className="textarea-container">
<textarea
className={`form-control json-input ${jsonError ? 'error' : ''}`}
value={jsonData}
onChange={handleJsonChange}
placeholder="Enter JSON data here..."
/>
{jsonError && (
<div className="alert alert-danger mt-2 mb-0">
<small>{jsonError}</small>
</div>
)}
</div>
</div>
</div>
</div>
{/* Results Output - Bottom Right */}
<div className="col-md-6 mb-3">
<div className="card h-100">
<div className="card-header">
<h5 className="mb-0">
<i className="bi bi-arrow-right-circle me-2"></i>
Query Result
</h5>
</div>
<div className="card-body output-section">
<div className="textarea-container">
<textarea
className={`form-control result-output ${result && !error && !jsonError ? 'success' : ''}`}
value={result}
readOnly
placeholder="Results will appear here..."
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default App;

22
src/App.test.js Normal file
View File

@@ -0,0 +1,22 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders JMESPath Testing Tool title', () => {
render(<App />);
const titleElement = screen.getByText(/JMESPath Testing Tool/i);
expect(titleElement).toBeInTheDocument();
});
test('renders input areas', () => {
render(<App />);
const jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
expect(jmespathInput).toBeInTheDocument();
expect(jsonInput).toBeInTheDocument();
});
test('renders result area', () => {
render(<App />);
const resultArea = screen.getByPlaceholderText(/Results will appear here/i);
expect(resultArea).toBeInTheDocument();
});

83
src/index.css Normal file
View File

@@ -0,0 +1,83 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f8f9fa;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.container-fluid {
height: 100vh;
display: flex;
flex-direction: column;
}
.content-section {
flex: 1;
min-height: 0;
}
.textarea-container {
height: 100%;
display: flex;
flex-direction: column;
}
.form-control {
resize: vertical;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
}
.input-section .form-control {
flex: 1;
min-height: 200px;
}
.output-section .form-control {
flex: 1;
min-height: 200px;
background-color: #f8f9fa;
}
.error {
background-color: #f8d7da !important;
border-color: #f5c6cb !important;
color: #721c24;
}
.success {
background-color: #d4edda !important;
border-color: #c3e6cb !important;
color: #155724;
}
.header-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem 0;
margin-bottom: 2rem;
}
.jmespath-input {
background-color: #fff3cd;
border-color: #ffeaa7;
font-weight: 500;
}
.json-input {
background-color: #e8f5e8;
border-color: #c3e6cb;
}
.result-output {
background-color: #e7f3ff;
border-color: #b3d7ff;
}

18
src/index.js Normal file
View File

@@ -0,0 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

13
src/reportWebVitals.js Normal file
View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
src/setupTests.js Normal file
View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';