Unveiling the Power of Abstract Syntax Trees (AST) and TypeScript
Abstract Syntax Trees (ASTs) possess immense potential to enhance your programming skills within the expansive realm of software development. This article aims to provide a comprehensive understanding of ASTs, employing the well-known JSON format as a tangible demonstration. Prepare yourself for an enlightening expedition into the realm of abstract structures and their real-world significance!
What is AST?
AST, or Abstract Syntax Trees, are hierarchical representations of the structure and semantics of a program’s source code. They are often used in programming languages and compilers to analyze and manipulate code. AST allow developers to understand the relationships between different code elements, such as functions, variables, and control flow. By parsing source code into an AST, developers can perform various operations like code generation, optimization, and static analysis. AST provide a powerful tool for understanding and manipulating code at a higher level of abstraction.
What is JSON?
JSON, which stands for JavaScript Object Notation, is a lightweight and widely-used data interchange format. It is a text-based format that represents structured data in a human-readable manner. JSON is easy for humans to understand and write, and it is also easy for machines to parse and generate. It is often used for transmitting data between a server and a web application, as it can be easily converted to and from JavaScript objects. JSON consists of key-value pairs enclosed in curly braces, and it supports various data types such as strings, numbers, booleans, arrays, and nested objects.
A Developer’s Playground: AST through JSON
Developers, particularly those who have experience with JavaScript, frequently come across JSON in different situations. JSON acts as a simple and efficient way to exchange data, seamlessly connecting different programming languages. We have decided to explore AST using JSON due to its developer-friendly characteristics and its widespread usage in real-world scenarios.
Let’s Dive In: Verifying JSON Format with TypeScript
In this tutorial, we’ll define validation rules, traverse the abstract structure, and ensure the JSON adheres to our specifications.
The example’s develpment was thought using the next JSON structure:
{
"name": "Juan",
"age": 25,
"city": "New York",
"interest": ["programming", "reading"],
"pets": [
{
"petKind":"Cat",
"name": "Mary",
"age": 3
},
{
"petKind":"Dog",
"name": "Scooby",
"age": 5
}
]
}
As you can see, Its a simple structure, some keys are string, another one is a string’s array and finally there is another one that is an object’s array.
Recognize unique data types
For our example, we are going to start by defining the enumerations that we are going to use. Enumerations help us to define restricted values (in other words, avoid to use invalid values).
Enumerations, also known as enums, are a data type in programming that allows you to define a set of possible values for a variable. These values are defined as constants and are assigned descriptive names. Enumerations are useful for representing a fixed set of options or states that a variable can take.
For our example, we are going to start by defining the enumerations that we are using.
Reading the JSON, we find the first structure we can ‘convert’ into an enum:
enum City {
buenosAires= "Buenos Aires",
london = "London",
mexicoCity = "Mexico City",
newYork = "New York",
}
All possibles values are there, there are no chance to assign another city.
After that, and continuing with reading the JSON, we find another structure to convert:
enum Interest {
Programming = "programming",
Travel = "travel",
Reading = "reading",
}
All possibles values are there, there are no chance to have other interest.
Finally, we found the latest one, pets:
enum Pets {
canary = "Canary",
cat = "Cat",
dog = "Dog",
Turtle = "Turtle"
}
After analyzing and having found all our enums, we are ready to develop our JSON Validator project.
//recursive type that represents a JSON value
interface JSONValue {
[key: string]: JSONValue | JSONValue[] | string;
}
//function type that takes a JSONValue as input and returns a boolean value.
type ValidationRule = (json: JSONValue) => boolean;
//JSONValidatorRule interface defines a rule for validating a JSON object.
interface JSONValidatorRule {
path: string[];
rule: ValidationRule;
}
//JSONValidator class is responsible for validating JSON objects based on a set of rules.
class JSONValidator {
private rules: JSONValidatorRule[];
constructor(rules: JSONValidatorRule[]) {
this.rules = rules;
}
// validate a JSON against all the rules.
validate(json: JSONValue): boolean {
return this.rules.every((rule) => {
const value = this.traverse(json, rule.path);
return rule.rule(value);
});
}
//takes a JSON object and a path as input and traverses the object to find the value at the specified path
private traverse(json: JSONValue, path: string[]): JSONValue {
let current = json;
for (const key of path) {
if (typeof current !== 'object' || current === null) {
throw new Error(`Invalid path: ${path.join('.')}`);
}
current = current[key];
}
return current;
}
}
Now, we are going to define our JSON validator rules:
// Example usage with enum values
const interestsEnumValues = Object.values(Interest);
const citiesEnumValues = Object.values(City);
const rules: JSONValidatorRule[] = [
{ path: ['name'], rule: (value) => typeof value === 'string' },
{ path: ['age'], rule: (value) => typeof value === 'number' },
{ path: ['city'], rule: (value) => typeof value === 'string' && citiesEnumValues.includes(value) },
{ path: ['interest'], rule: (value) => Array.isArray(value) && value.every((interest) => interestsEnumValues.includes(interest)) },
];
const validator = new JSONValidator(rules);
In this point, we are ready to start the testing!
Testing the code
First, we are going to test if our validator works with a valid case:
const validJSON = {
"name": "Juan",
"age": 25,
"city": City.MexicoCity,
"interest": [Interest.Programming, Interest.Travel],
"pets": [
{
"petKind":Pets.cat,
"name": "Mary",
"age": 3
},
{
"petKind":Pets.dog,
"name": "Scooby",
"age": 5
}
]
};
const isValidValid = validator.validate(validJSON);
console.log(`Invalid JSON is valid: ${isValidValid}`);
In this case, Our code shows us the next message:
Cool!, good news, It seems that the code works as expected.
Now, we are going to to introduce a new interest, this called “InvalidInterest”, but this new interst is not a member of our interest enum.
const invalidJSON = {
"name": "Carlos",
"age": 30,
"city": City.London,
"interest": [Interest.Programming, "InvalidInterest"],
"pets": [
{
"petKind":Pets.cat,
"name": "Mary",
"age": 3
},
{
"petKind":Pets.dog,
"name": "Scooby",
"age": 5
}
]
};
const isInvalid = validator.validate(invalidJSON);
console.log(`Invalid JSON is valid: ${isInvalid}`);
Our code works as we expected! (We expected that, due to “InvalidInterest” is not a member of Interest enum, it should be not a ‘valid’ json).
Note: You can check the examploe code -and additional test cases- using the next link.
Conclusion
In this exploration of Abstract Syntax Trees through the lens of TypeScript and JSON, we’ve witnessed the potent synergy between these technologies. As a developer, grasping the concept of AST can open new avenues for code analysis, manipulation, and optimization. The journey we’ve embarked upon today is just the beginning — may your future endeavors in the world of AST be both exciting and enlightening!
Happy coding!