Advanced C Programming

Summer 2024 ECE 26400 :: Purdue University

Due 7/20

JSON parse_list(…)

Learning goals

You will learn or practice how to:

  1. Parse strings
  2. Apply test-driven development (TDD).
  3. Use union and enum types.
  4. Write programs that use dynamic memory (malloc(…))
  5. Debug code that uses dynamic memory (malloc(…))
  6. Practice writing programs with linked lists.
  7. Write programs using more complex data structures (beyond the cannonical linked list and BST).
  8. Learn to refactor existing programs with new requirements.
  9. Read and write text files using FILE* and the related functions.
  10. Work with binary search trees as part of a larger program.
  11. Combine all major learning objectives into a single program that demonstrates almost everything you learned in this class.

Overview

This is part 3 of a 4-part sequence in which you will create a decoder for the JSON data format.

JSON is a file format for exchanging hierarchical data between programs, often written in different programming languages and/or on different computers. In web programming, this allows a server application written in any language (e.g., Python or even C) to pass complex structures of information to the user's browser, which then renders it.

A JSON string may represent a number, a string, a boolean, null, an object, or a list. In this context, when we say “list”, we mean a linked list. Here are some examples:

type json data
number 10 10
string "ten" "ten"
list [2, 6, 4] linked list with values 2, 6, 4
boolean true true
null null
object {"key1": "value", "key2": true} Imagine a lovely picture of a binary search tree here. I plan to draw one during lecture.

Parts of this assignment

Originally, this was one assignment. Over time, we have split it into pieces to help you manage your time and efforts. (The assignment has also evolved, as all assignments do.)

HW08
  • Implement parse_int(…).
  • Instructor's solution: 15 SLOC
HW11
  • Add parse_string(…).
  • Implement parse_element(…), parse_json(…), and print_element(…)—but for now, they only need to work for integer and string expressions.
  • Instructor's solution: 79 SLOC
HW13
  • Add parse_list(…), but it only needs to work for flat, correct list expressions. Examples:
    • Might be tested: []
      [1]
      [1,"two",3]
    • Will not be tested for HW13:
      [1, ["two", "three"], 4]
      [768, 336
      [ , 768336]
      [, 2,]
      [1 2]
  • Extend parse_element(…) and print_element(…) to work with lists, as well as integers and strings.
  • Instructor's solution: 113 SLOC
HW15
  • Everything works (including invalid lists).
  • Refactoring existing code for new requirements (text files, null, and booleans).
  • Instructor's solution: 142 SLOC
EC03
  • Previous parts work.
  • Refactoring existing code for objects.
  • Instructor's solution: 142+ SLOC

Linked lists

As in C, whitespace is ignored (except within a string). Thus, the following are all equivalent.

type json data
list [2, 6, 4] linked list with values 2, 6, 4
list [2, 6, "four"] linked list with values 2, 6, "four"
list [2, [3, "+", 3], "four"] linked list with values 2, (3, "+", 3), "four"
list [{"key": "value"}, {"key": "another value"}] Also plan to draw this during lecture
object {"array": [1, 2, 3]} And this one too, its a lot of drawing to do just to post this!
object {"nested object": {"key": 5}} Yeah, pictures

Getting Started on HW13

  1. Use 264get HW13 to fetch the starter code.
    you@eceprog ~/ $ cd 264
    you@eceprog ~/264/ $ 264get hw13
  2. Copy your miniunit.h, log_macros.h, and Makefile from any previous assignment.
    you@eceprog ~/264/ $ mv HW██/miniunit.h HW██/log_macros.h HW██/Makefile -v -t hw13/
    you@eceprog ~/264/ $ cd hw13
    you@eceprog ~/264/hw13 $
  3. Implement parse_int(…).
    1. Create a trivial test (e.g., 0).
    2. Implement just enough of parse_int(…) so that it passes your trivial test. At this point, it should fail most other tests.
    3. Submit.
    4. Add another simple trivial test (e.g., 9).
    5. Implement just enough to pass your two tests so far.
    6. Submit.
    7. Add another test (e.g., 15).
    8. Implement just enough to pass your three tests so far.
    9. Submit.
    10. Add another test (e.g., -15).
    11. … and so on, until parse_int(…) is finished.
  4. Test your parse_int(…) completely, using miniunit.h. Get parse_int(…) completely finished and tested to perfection—including error handling—before you do anything else. Seriously… do not do anything else until this is done.
  5. Submit.
  6. Implement parse_element(…) just enough to support integers.
    1. Add a test for a plain integer. Be sure to call it from main(…).
      static int _test_parse_element_int_plain() {
        mu_start();
        //────────────────────
        Element element; // will be initialized by parse_element(…)
        char const* input = "1";
        char const* pos = input;
        bool is_success = parse_element(&element, &pos);
        mu_check(is_success);
        mu_check(element.as_int == 1);
        mu_check(element.type == ELEMENT_INT);
        mu_check(pos == input + 1);  // pos should now refer to the byte just after the '1' at input.
        mu_check(*pos == '\0');      // That byte should be the null terminator.
        //────────────────────
        mu_end();
      }
      
    2. Add just enough code to parse_element(…) to make this test pass. Expect to add about 2−4 lines of code inside parse_element(…).
    3. Test.
    4. Submit.
    5. Extend the test function above to test for negative numbers.
      // negative number
      input = "-2";
      pos = input;
      is_success = parse_element(&element, &pos);
      mu_check(is_success);
      mu_check(element.as_int == -2);
      mu_check(element.type == ELEMENT_INT);
      mu_check(pos == input + 2);  // pos should now refer to the byte just after the '2'.
      mu_check(*pos == '\0');      // That byte should be the null terminator.
      
    6. Test.
    7. Submit.
    8. Add a test for a integer with leading whitespace. Be sure to call it from main(…).
      static int _test_parse_element_with_leading_whitespace() {
        mu_start();
        //────────────────────
        Element element; // will be initialized by parse_element(…)
        char const* input = "  1";
        char const* pos = input;
        bool is_success = parse_element(&element, &pos);
        mu_check(is_success);
        mu_check(element.as_int == 1);
        mu_check(element.type == ELEMENT_INT);
        mu_check(pos == input + 3);  // pos should now refer to the byte just after the '1' at input.
        mu_check(*pos == '\0');      // That byte should be the null terminator.
        //────────────────────
        mu_end();
      }
      
    9. Add just enough code to parse_element(…) to make this test pass. Expect to add about 1−3 lines of code inside parse_element(…).
    10. Test.
    11. Submit.
    12. Add a test function for some more cases involving integers. Here is a start. This is not adequate. You need to extend this with your own test cases.
      static int _test_parse_element_int_oddballs() {
        mu_start();
        //────────────────────
        Element element; // will be initialized by parse_element(…)
        char const* input = " 4 A";
        char const* pos = input;
        bool is_success = parse_element(&element, &pos);
        mu_check(is_success);
        mu_check(element.as_int == 4);
        mu_check(element.type == ELEMENT_INT);
        mu_check(pos == input + 2);  // pos should now refer to the byte just after the '1' at input.
        mu_check(*pos == ' ');    // That byte contains a space.
      
        // TODO: Add more tests here based on your ideas for tests.
      
        //────────────────────
        mu_end();
      }
      
    13. You may or may not not need to add any code to make these tests pass.
    14. Test.
    15. Submit.
    16. Add a test function for strings that contain integers but are not valid JSON because they are preceded by junk characters (not whitespace).
      static int _test_parse_element_invalid() {
        mu_start();
        //────────────────────
        Element element; // will be initialized by parse_element(…)
        char const* input = "--4";
        char const* pos = input;
        bool is_success = parse_element(&element, &pos);
        mu_check(! is_success);
        mu_check(pos == input + 1);  // pos should now refer to second '-' since that is the
                                     // character where we learn that this is not a valid JSON
                                     // expression for an int.
        mu_check(*pos == '-'); // That byte contains a '-'.
      
        // TODO: Add more tests here based on your ideas for tests.
      
        //────────────────────
        mu_end();
      }
      
    17. You may or may not not need to add any code to make these tests pass.
    18. Test.
    19. Submit.
  7. Implement parse_string(…).
    1. Create a test function for a valid JSON expression representing an empty string.
      static int _test_parse_string_valid_empty() {
        mu_start();
        //──────────────────────────────────────────────────────
        char* result;
        char const* input = "\"\"";
        char const* pos = input;
        bool is_success = parse_string(&result, &pos);
        mu_check(is_success);   // because the input is valid
        mu_check_strings_equal("", result);
        mu_check(pos == input + 2);
        mu_check(*pos == '\0');
        free(result);
        //──────────────────────────────────────────────────────
        mu_end();
      }
      
    2. Implement just enough of parse_string(…) to make this test pass.
    3. Test.
    4. Submit.
    5. Create a test function for a valid JSON expression representing a string of size one.
      static int _test_parse_string_valid_one_char() {
        mu_start();
        //──────────────────────────────────────────────────────
        char* result;
        char const* input = "\"A\"";
        char const* pos = input;
        bool is_success = parse_string(&result, &pos);
        mu_check(is_success);   // because the input is valid
        mu_check_strings_equal("A", result);
        mu_check(pos == input + 3);
        mu_check(*pos == '\0');
        free(result);
        //──────────────────────────────────────────────────────
        mu_end();
      }
      
    6. Implement just enough of parse_string(…) to make this test pass.
    7. Test.
    8. Submit.
    9. Create a test function for a valid JSON expression representing a string containing multiple characters.
      static int _test_parse_string_valid_multiple_chars() {
        mu_start();
        //──────────────────────────────────────────────────────
        char* result;
        char const* input = "\"ABC\"";
        char const* pos = input;
        bool is_success = parse_string(&result, &pos);
        mu_check(is_success);   // because the input is valid
        mu_check_strings_equal("ABC", result);
        mu_check(pos == input + 5);
        mu_check(*pos == '\0');
        free(result);
        //──────────────────────────────────────────────────────
        mu_end();
      }
      
    10. Implement just enough of parse_string(…) to make this test pass.
    11. Test.
    12. Submit.
    13. Add test function(s) to check strings that contain double quotation marks but are not valid JSON. Here is a start. You will need to add more of your own.
      static int _test_parse_string_invalid() {
        mu_start();
        //──────────────────────────────────────────────────────
        char* result;
        char const* input = "\"A";
        char const* pos = input;
        bool is_success = parse_string(&result, &pos);
        mu_check(! is_success);   // because the input is valid
        mu_check(pos == input + 2);
        mu_check(*pos == '\0');
        // We do not call free(…) if the string was invalid.
      
        input = "\"A\nB\"";
        pos = input;
        is_success = parse_string(&result, &pos);
        mu_check(! is_success);   // because the input is valid
        mu_check(pos == input + 2);
        mu_check(*pos == '\n');
      
        // TODO: Add more tests of your own.
      
        //──────────────────────────────────────────────────────
        mu_end();
      }
      
  8. Extend parse_element(…) to support strings. You will need to implement free_element(…) in the same step.
    1. Add a test for a plain string containing multiple characters. Be sure to call it from main(…).
      static int _test_parse_element_string() {
        mu_start();
        //────────────────────
        Element element; // will be initialized by parse_element(…)
        char const* input = "\"ABC\"";
        char const* pos = input;
        bool is_success = parse_element(&element, &pos);
        mu_check(is_success);
        mu_check_strings_equal("ABC", element.as_string);
        mu_check(element.type == ELEMENT_STRING);
        mu_check(pos == input + 5);  // pos should now refer to the byte just after the second double quote.
        mu_check(*pos == '\0');      // That byte should be the null terminator.
        free_element(element);
        //────────────────────
        mu_end();
      }
      
    2. Add just enough code to parse_element(…) to make this test pass. Expect to add about 4 lines of code inside parse_element(…) and 1−3 lines inside free_element(…).
    3. Test.
    4. Submit.
    5. Add additional tests as needed to ensure that parse_element(…) works for JSON expressions representing strings and integers.
    6. Test.
    7. Submit.
  9. Implement print_element(…) so that it supports integers and strings.
    1. Add a test for Element objects that contain an integer. This will not use Miniunit. Since this is simple, you an use visual inspection (just look).
      static void _test_print_element() {
        Element element; // will be initialized by parse_element(…)
        char const* input = "123";
        bool is_success = parse_element(&element, &input);
        printf("Testing parse_element(&element, \"123\")\n");
        printf(" - Expected: 123\n");
        printf(" - Actual:   ");
        print_element(element);
        fputc('\n', stdout);
        free_element(element);
      }
      
    2. Add just enough code to print_element(…) to make this test pass. Expect to add about 1―3 lines of code inside print_element(…).
    3. Test.
    4. Submit.
    5. Add a test for Element objects that contain an string. Again, this will not use Miniunit.
      static int _test_print_element() {
        mu_start();
        //──────────────────────────────────────────────────────
        // This uses diff testing to check print_element(…).
        Element element; // will be initialized by parse_element(…)
        char const* input = "\"ABC\"";
        bool is_success = parse_element(&element, &input);
        printf("Testing parse_element(&element, \"\\\"ABC\\\"\")\n");
        printf(" - Expected: \"ABC\"\n");
        printf(" - Actual:   ");
        print_element(element);
        fputc('\n', stdout);
        free_element(element);
        //──────────────────────────────────────────────────────
        mu_end();
      }
      
    6. Add just enough code to print_element(…) to make this test pass. Expect to add about 3―5 lines of code inside print_element(…).
    7. Test.
    8. Submit.
  10. Implement parse_list(…).
    1. Create a trivial test (e.g., []).
    2. Implement just enough of parse_list(…) so that it passes your trivial test (functionality only). It should fail all other tests with linked lists.
    3. Extend free_element(…) so that your parse_list(…) tests work without any memory leaks (e.g., passes Valgrind).
    4. Add another simple test (e.g., [0]).
    5. Implement just enough to pass your two list tests so far.
    6. Add another simple test (e.g., ["a"]).
    7. Implement just enough to pass your three list tests so far.
    8. Add a test with multiple integers as list items (e.g., [1, 2]).
    9. Implement just enough to pass your tests so far.
    10. Add a test with multiple strings as list items (e.g., ["A", "B"]).
    11. You shouldn't need to add any code for this, but make sure it works 100% before you proceed.
    12. Add a test with an empty list as a list item (e.g., [[]]).
    13. Implement just enough to pass your tests so far.
    14. Add a test with a non-empty list as a list item (e.g., [[1]]).
    15. Implement just enough to pass your tests so far.
    16. Add a few more tests with gradually increasing complexity (e.g., [[1, 2]], [1, [2, 3], 4], [[1, 2], [3, 4], []]).
    17. Test as needed. Get things perfect—including memory (Valgrind) and test coverage (gcov)—and submit at every stage.
  11. Test error cases. Make sure parse_int(…), parse_list(…), and parse_element(…) return false for incorrect input.
  12. Create print_element_to_file(…).
    1. Rename print_element(…) to print_element_to_file(…) and add the extra FILE* parameter. Make no other changes yet.
    2. Recreate print_element(…) by having it call print_element_to_file(…). You should only need a single line of code.
    3. Ensure all tests for print_element(…) still pass.
    4. Submit.
    5. Add tests for print_element_to_file(…). You will need to use fopen(…) and fclose(…).
    6. Ensure your new tests are failing.
    7. Make use of the FILE* parameter from print_element_to_file(…)
    8. Ensure both your new test and your previous tests for print_element(…) work.
    9. Submit.
  13. Implement write_json(…).
    1. Implement tests for the function. They can similar to your tests for print_element_to_file(…).
    2. Implement write_json(…). It should call print_element_to_file(…).
    3. Submit.
  14. Implement read_json(…).
    1. We recommend creating a helper that reads all charaters in a file and returns a malloced string of the contents. It might be worth testing your helper on its own before moving on.
    2. Add tests for read_json(…).
    3. Implement read_json(…). Hint: if you use parse_json(…) alongside the suggested helper, the implementation should be trival.
    4. Ensure your tests are passing.
    5. Submit.
  15. Implement parse_null(…).
    1. Add support for null to the enum and union for Element.
    2. Ensure no previous tests broke.
    3. Add some tests for valid null.
    4. Implement enough code to pass the tests.
    5. Submit.
    6. Add some tests for invalid null.
    7. Implement enough code to pass the tests, this may require no additional code.
    8. Submit.
    9. Add tests for other JSON functions (e.g. parse_json(…)) using null.
    10. Implement code in other functions for null.
    11. Ensure all tests are passing, both new and old.
    12. Submit.
  16. Implement parse_boolean(…).
    1. Add support for booleans to the enum and union for Element.
    2. Ensure no previous tests broke.
    3. Add some tests for valid booleans.
    4. We recommend adding a helper to allow reusing some logic from parse_null(…).
    5. Implement enough code to pass the tests.
    6. Submit.
    7. Add some tests for invalid booleans.
    8. Implement enough code to pass the tests, this may require no additional code.
    9. Submit.
    10. Add tests for other JSON functions using booleans.
    11. Implement code in other functions for booleans.
    12. Ensure all tests are passing, both new and old.
    13. Submit.
  17. Create or update functions adjacent to parse_object(…)
    1. Update json.h to support objects, and add all object related function headers.
    2. Create a test for insert_element(…) and get_element(…). Feel free to do this in a single stage or split based on the size of the tree.
    3. Implement enough code to pass the tests.
    4. Submit.
    5. Next, write tests for print_element(…) or write_json(…) to support objects. This will help you debug.
    6. Implement enough code to pass the tests. You should only need to modify print_element_to_file(…).
    7. Submit.
    8. Update your tests to use free_element(…) for JSON objects
    9. Update free_element(…) to ensure no memory leaks in previous tests. You will need a recursive helper function to free the tree, recommended signature: void _free_tree(BSTNode** a_root).
    10. Submit.
  18. Create parse_object(…)
    1. Start by updating parse_element(…) to support JSON objects. This will allow you to write tests using parse_json(…) or read_json(…) if you wish.
    2. Next, create tests to parse an empty object.
    3. Implement and submit.
    4. Next, create tests to parse an element with a single key and value.
    5. Implement and submit.
    6. Next, update the tests to parse two key and value pairs.
    7. Implement and submit.
    8. Update the tests to parse any number of key and value pairs.
    9. Implement and submit.
    10. Finally, add tests to handle any invalid cases you did not handle before.
    11. Implement and submit.
  19. Test that there are no memory leaks, even for incorrect input.

How much work is this?

You must be disciplined in your development appraoch. If you try to build this all at once, success is extremely unlikely.

HW08: Instructor's solution for parse_int(…) only is 18 sloc (14 sloc in the body of parse_int(…)).

HW11: Instructor's solution for parse_int(…), parse_string(…), and parse_element(…) only is 75 sloc.

HW13: Instructor's solution for parse_int(…), parse_string(…), parse_list(…), and parse_element(…) is 127 sloc.

HW15: Instructor's solution for parse_null(…) and parse_boolean(…) is 15 sloc (5 sloc in the body of a helper called by both).

HW15: Instructor's solution for write_json(…) and read_json(…) is 27 sloc (in addition to the sloc from the body of print_element_to_file(…)).

SLOC means “source lines of code” and does not lines that are blank or contain only comments. The figures above do not count test_json.c. Also, those figures are based on a tight—but defensible—solution that meets code quality standards and uses no methods or language features we have not yet covered.

Requirements

  1. Your submission must contain each of the following files, as specified:
    file contents
    json.c functions
    parse int(int✶ a value, char const✶✶ a pos)
    return type: bool
    Set *a_value to whatever integer value is found at *a_pos.
    1. *a_pos is initially the address of the first character of the integer literal in the input string.
    2. *a_value is the (already allocated) location where the parsed int should be stored.
    3. Return true if a properly formed integer literal was found at *a_pos*a_pos should refer to the next character in the input string, i.e., after the last digit of the integer literal.
      1. Ex: parse_int(…) should return true for 9, -99, and 123AAA.
    4. Return false if an integer literal was not found.  *a_pos should refer to the unexpected character that indicated a problem.
      1. Ex: parse_int(…) should return false for A, AAA123, -A, -A9, and -.
    5. Calling parse_int(…) should not result in any calls to malloc(…).
    6. You do not need to parse hexadecimal, octal, scientific notation, floating point values, or anything other than integers in decimal notation (positive or negative). You may assume any integer is within the range of an int on our platform (i.e., ≥INT_MIN and ≤INT_MAX).
    7. Whenever parse_int(…) returns false, *a_value should not be modified.
    8. parse_int(…) should ignore any trailing characters that come after the last digit in the number.

    Examples

    parse_int(…) should return true for any of these:
    9-99768336YAK768336 EMU

    parse_int(…) should return false for any of these:
    AAAA123-A-A9- 

    parse string(char✶✶ a string, char const✶✶ a pos)
    return type: bool
    Set *a_string to a copy of the string literal at *a_pos.
    1. Caller is responsible for freeing the memory.
    2. A string literal must be surrounded by double quotation marks, and may not contain a newline. In addition, we make two simplifications:
      • Strings may not contain double quotation marks.
      • Backslash is not special. Do not parse escape codes (i.e., "\▒") in the input.
    3. Calling parse_string(…) should result in exactly one call to malloc(…).
    4. Return true if a properly formed string literal was found.  *a_pos should be set to the next character in the input string, i.e., after the ending double quotation mark.
    5. Return false if a string literal was not found.  *a_pos should refer to the unexpected character that indicated a problem (e.g., newline or null terminator in the input).
    6. Whenever parse_string(…) returns false, do not modify *a_string, and no heap memory should be allocated.
    7. parse_string(…) should ignore any trailing characters that come after the closing double quotation mark.

    Examples

    parse_string(…) should return true for any of these:
    "antfox""emubee\""tompup\z""ratkoi"YAK"doecod" YAK 

    parse_string(…) should return false for any of these:
    "henjay""ram
    dog"
    768336"eelfly"768336 "catbug" 

    parse list(Node✶✶ a head, char const✶✶ a pos)
    return type: bool
    Set *a_head to the head of a linked list of Element objects.
    1. Caller is responsible for freeing the memory if parse_list(…) returns true.
    2. Linked list consists of a '[', followed by 0 or more JSON-encoded elements (integers, strings, or lists) separated by commas, and finally a ']'. See the examples above. (There will be no HBB on this definition, unless there is something truly wrong and/or grossly unclear.) There may be any number/amount of whitespace characters (' ', '\n', or '\t'), before/after any of the elements.
    3. Return true if a properly formed list was found.  *a_pos should be set to the next character in the input string, after the list that was just parsed.
      1. Ex: parse_list(…) should return true for [], [1,2], [1,"A"], [1,[2]], and [1]A.
    4. Return false if a list was not found (i.e., syntax error in the input string).  *a_pos should refer to the unexpected character that indicated a problem.
      1. Ex: parse_list(…) should return false for A[], [1,,2], [[, 1,2, and ,2].
      2. Whenever parse_list returns False, you must ensure that all heap memory allocated as a result of that call to parse_list(…) is freed. In other words, testing parse_list(…) with an invalid JSON list representation (e.g., ["one",[1,2],3,,]) should not result in a memory leak.
    5. parse_list(…) should ignore any trailing characters that come after the closing square bracket.
    parse boolean(bool✶ a value, char const✶✶ a pos)
    return type: bool
    Set *a_value to whatever boolean value is found at *a_pos.
    1. *a_pos is initially the address of the first character of the boolean literal in the input string.
    2. *a_value is the (already allocated) location where the parsed boolean should be stored.
    3. Return true if a properly formed boolean literal was found at *a_pos*a_pos should refer to the next character in the input string, i.e., after the last character of the boolean literal. Note that trailing characters are acceptable, just like previous functions.
      1. Ex: parse_boolean(…) should return true for true, false, and falsehood
      Note that the last example is valid because it starts with false.
    4. Return false if a boolean literal was not found.  *a_pos should refer to the unexpected character that indicated a problem.
      1. Ex: parse_boolean(…) should return false for A, 123, "true", and truly
      Note that the third example is a string literal. Also note that in the last example, *a_pos will be 'l' as that is the first invalid character.
    5. Calling parse_boolean(…) should not result in any calls to malloc(…).
    6. Do not confuse the boolean return value from the result. The return value determines whether a boolean was found, while *a_value is the boolean that was found.
    7. Whenever parse_boolean(…) returns false, *a_value should not be modified.
    parse null(char const✶✶ a pos)
    return type: bool
    Determine whether null is found at *a_pos.
    1. *a_pos is initially the address of the first character of the null literal in the input string.
    2. Return true if a properly formed null literal was found at *a_pos*a_pos should refer to the next character in the input string, i.e., after the last character of the null literal. Note that trailing characters are acceptable, just like previous functions.
      1. Ex: parse_null(…) should return true for null, null literal, and nullify
      Note that the last two examples are valid because they starts with null.
    3. Return false if the null literal was not found.  *a_pos should refer to the unexpected character that indicated a problem.
      1. Ex: parse_null(…) should return false for A, 123, "null", and nil
      Note that the third example is a string literal. Also note that in the last example, *a_pos will be 'i' as that is the first invalid character.
    4. Calling parse_null(…) should not result in any calls to malloc(…).
    5. This function has no result, as the only possible result is null.
    parse object(BSTNode✶✶ a root, char const✶✶ a pos)
    return type: bool
    Set *a_root to the root of a tree of Element objects.
    1. Caller is responsible for freeing the memory if parse_object(…) returns true.
    2. Objects consist of a '{', followed by 0 or more key-value pairs separated by commas, and finally a '}'. Each key-value pair consists of a string key, a character , then a JSON-encoded element (integer, string, list, object, boolean, or null). See the examples above. (There will be no HBB on this definition, unless there is something truly wrong and/or grossly unclear.) There may be any number/amount of whitespace characters (' ', '\n', or '\t'), before/after any of the element and before/after the key.
    3. Return true if a properly formed object was found.  *a_pos should be set to the next character in the input string, after the object that was just parsed.
      1. Ex: parse_object(…) should return true for {}, {"number": 123}, {"string": "Hello", "list": [ 1, 2, 3 ]}, { "nested_object" : { "integer": 5 } }, and { "string_contains": "trailing_characters" }ABC.
    4. Return false if an object was not found (i.e., syntax error in the input string).  *a_pos should refer to the unexpected character that indicated a problem.
      1. Ex: parse_object(…) should return false for A{}, {"key": 1,,"value": 2}, { {, "key": 1 { "key": 1 "key2": 2}. and { "key" 1 }.
    5. The parsed elements are stored in a binary search tree sorted by the key values.
    6. If duplicate keys are found, replace the value in the previous node with the new element parsed. For example, {"duplicate": 123, "duplicate": 456} should produce the same result as {"duplicate": 456},
      Hint: this should be handled by insert_element(…).
    7. Whenever parse_object(…) returns false, do not modify *a_root, and free any heap memory that was allocated prior to discovery of the error.
    8. Hint: you already have logic written to parse a string, reuse that logic for the key instead of writing it again.
    parse element(Element✶ a element, char const✶✶ a pos)
    return type: bool
    1. First, eat any whitespace at *a_pos.
      • “Eat whitespace” just means to skip over any whitespace characters (i.e., increment *a_pos until isspace(**a_pos)==false).
    2. Next, decide what kind of element this is.
      1. If it's a digit (isdigit(**a_pos)) or hyphen ('-'), set the element's type to ELEMENT_INT and call parse int(&(a element -> as int), a pos).
      2. If it's a string (**a_pos=='"'), then set the element's type to ELEMENT_STRING and call parse string(&(a element -> as string), a pos).
      3. If it's a list (**a_pos == '['), then set the element's type to ELEMENT_LIST and call: parse list(&(a element -> as list), a pos).
      4. If it's a boolean (**a_pos == 't' or **a_pos == 'f'), then set the element's type to ELEMENT_BOOLEAN and call: parse boolean(&(a element -> as boolean), a pos).
      5. If it's null (**a_pos == 'n'), then set the element's type to ELEMENT_NULL, set the element's as_null to NULL, and call: parse null(a pos).
      6. If it's an object (**a_pos == '{'), then set the element's type to ELEMENT_OBJECT and call: parse object(&(a element -> as object), a pos).
    3. Return whatever was returned by parse_int(…), parse_string(…), parse_boolean(…), parse_null(…), parse_object(…), or parse_list(…).
      • If none of those functions was called—i.e., if the next character was neither digit, '-', '"', nor '['—then return false.
    4. Do not modify *a_pos directly in parse_element(…), except for eating whitespace.
      • *a_pos can—and should—be modified in parse_int(…), parse_string(…), parse_boolean(…), parse_null(…), parse_object(…), and parse_list(…)
    5. Caller is responsible for freeing memory by calling free_element(…) whenever parse_element(…) returns true.
    6. Whenever parse_element(…) returns false, do not modify *a_element. Any heap memory allocated prior to discovery of the error should be freed.
    parse json(char const✶ json)
    return type: ParseResult
    Given a JSON string, parse it and return a ParseResult object.
    1. If parsing succeeds—i.e., the string is a valid JSON representation of any JSON object, such as an integer, string, or list—then the .is_success field of the ParseResult object should be true and the .element field (of the anonymous union) should contain the Element object, as returned by parse_element(…).
      • Calling parse_json("768336"); should return a ParseResult object equivalent to this compound literal:
        (ParseResult) { .is_success = true, .element = (Element) { .type = ELEMENT_INT, .as_int = 768336 } }
    2. If parsing fails, then the .is_success field of the ParseResult object should be false, the .file_error field of the ParseResult object should be 0, and the .error_idx field (of the anonymous union) should contain the index into json of the first character that indicated a parse failure.
      • Example: Calling parse_json("768336X"); should return a ParseResult object equivalent to this compound literal:
        (ParseResult) { .is_success = false, .file_error = 0, .error_idx = 6 }
      • Example: Calling parse_json("X768336"); should return a ParseResult object equivalent to this compound literal:
        (ParseResult) { .is_success = false, .file_error = 0, .error_idx = 0 }
      • Don't forget to set .file_error and test for it! The struct should always be fully initialized even though this function does not use that field.
    3. Ignore leading or trailing whitespace.
      • Example: Parsing 768336, 768336 , or 768336 should return an equivalent result with .is_success == true and .element.as_int == 768336.
    4. Hint: parse_json(…) should not need to do anything special for each type (i.e., int, string, list); if it works for one, it should work with the others with no modification. parse_json(…) is just a wrapper for parse_element(…) that gives you a more usable, understandable result.
    5. parse_json(…) should NOT ignore any non-whitespace trailing characters that come after the JSON object.
      • Example: Parsing X768336, 768336X, 768336 X, or X 768336 X should result in a ParseResult object with .is_success == false and .error_idx equal to the index of the first X character (i.e., 0, 6, 7, or 1, respectively).
      • This is different from parse_int(…), parse_string(…), parse_list(…), and parse_element(…).
    read json(char const✶ filename)
    return type: ParseResult
    Reads a JSON element from the file defined by filename.
    1. Opens the file named filename and reads in the contents as a JSON element.
    2. If parsing succeeds—i.e., the file contains a valid JSON representation of any JSON object, such as an integer or string, string, or list—then the .is_success field of the ParseResult object should be true and the .element field (of the anonymous union) should contain the Element object, as returned by parse_element(…).
      • Calling read_json("integer.json"); where integer.json contains 768336 should return a ParseResult object equivalent to this compound literal:
        (ParseResult) { .is_success = true, .element = (Element) { .type = ELEMENT_INT, .as_int = 768336 } }
    3. If the file was unable to be read, then the .is_success field of the ParseResult object should be false, the .error_idx field should be 0 and the .file_error field should be set to the file error (from errno)
    4. If the file could be read but JSON parsing fails, then the .is_success field of the ParseResult object should be false, the .file_error field should be 0 and the .error_idx field (of the anonymous union) should contain the index into json of the first character that indicated a parse failure.
      • Example: Calling read_json("invalid_trailing.json"); where invalid_trailing.json contains 768336X should return a ParseResult object equivalent to this compound literal:
        (ParseResult) { .is_success = false, .file_error = 0, .error_idx = 6 }
      • Example: Calling read_json("invalid_leading.json"); where invalid_leading.json contains 768336X should return a ParseResult object equivalent to this compound literal:
        (ParseResult) { .is_success = false, .file_error = 0, .error_idx = 0 }
    5. Ignore leading or trailing whitespace.
      • Example: Parsing 768336, 768336 , or 768336 should return an equivalent result with .is_success == true and .element.as_int == 768336.
    6. read_json(…) should NOT ignore any non-whitespace trailing characters that come after the JSON object.
      • Example: Parsing X768336, 768336X, 768336 X, or X 768336 X should result in a ParseResult object with .is_success == false and .error_idx equal to the index of the first X character (i.e., 0, 6, 7, or 1, respectively).
      • This is different from parse_int(…), parse_string(…), parse_list(…), and parse_element(…). However, it is the same as parse_json(…).
    7. Hint: read_json(…) should not need to do anything special to parse elements or handle trailing characters, it is just a wrapper for parse_json(…) that reads from a file instead of a string. We talked in lecture about how to read all text from a file into a string.
    8. Do not add the .json extension automatically. The caller will pass in the full filename they wish to create. If the caller passes in "example", read a file named example, not example.json.
    print element(Element element)
    return type: void
    Given an Element object, print it in JSON notation to the passed file.
    1. Spacing is up to you, as long as it is valid JSON.
    2. If element is an integer, print it using printf(…).
    3. If element is a string, then print it (with double-quotes) using printf{…).
    4. If element is a list, print a '['. Then print each element in the list using print_element(…) (recursively), separated by commas. Finally, print ']'.
    print element(Element element)
    return type: void
    This function should be exactly one line. It should simply call print_element_to_file(…) with the proper argument for printing to the console.
    write json(char const✶ filename, Element element)
    return type: int
    Writes a JSON element to the file defined by filename.
    1. Opens the file named filename.
    2. Create the file if it does not exist, replace contents if it does exist.
    3. Write the contents of element to the file as valid JSON.
    4. Returns 0 if the file is successfully written. If the file was unable to be written (i.e. the file could not be opened), then return the file error (from errno).
    5. Hint: use print_element_to_file(…).
    6. Do not add the .json extension automatically. The caller will pass in the full filename they wish to create. If the caller passes in "example", create a file named example, not example.json.
    insert element(BSTNode✶✶ a root, char✶ key, Element element)
    return type: void
    Inserts the key/value pair into the binary search tree.
    1. Sort the tree using key, no need to consider element when sorting.
    2. This works the same way as the binary search tree insert on HW14, except we handle equal elements differently.
    3. If the key already exists, replace the value in the old node with the new element.
      do not forget to free the old element.
    4. key will be a heap allocated string. element will be any valid element.
    5. If the key is not found, add a new node to the tree.
    6. Hint: you can write this either using recursion or a loop.
    7. Hint: this function will be helpful in creating parse_object(…).
    get element(const BSTNode✶ root, char const✶ key)
    return type: const Element✶
    Finds the node in the binary search tree which matches the key.
    1. If found, return the address of the element in the node.
    2. If not found, return NULL.
    3. Hint: you can write this either using recursion or a loop.
    4. Hint: this function will be helpful in testing parse_object(…).
    free element(Element element)
    return type: void
    Free the contents of the Element, as needed.
    1. If it contains a string, free the string.
    2. If it contains a linked list, free the list, including all elements.
    3. If it contains a binary search tree, free each node in the tree, including all elements and keys.
      Hint: You will want a recursive helper for this.
    4. Do not attempt to free the Element object itself. free_element(element) only frees dynamic memory that element refers to.
    json.h types
    Element
    Provided definition for a struct type called Element that stores a union between different JSON types.
    1. Modify the type enum to add new types: ELEMENT_NULL ELEMENT_BOOLEAN ELEMENT_OBJECT
    2. Modify the union to include void* .as_null
    3. Modify the union to include bool .as_boolean
    4. Modify the union to include BSTNode* .as_object
    ParseResult
    Add a definition for a struct type called ParseResult as needed to work with parse_json(…) and read_json(…).
    BSTNode
    struct type with 4 fields: key (char*), element (Element), left, and right (BSTNode*).
    Leave other definitions in the header file as is.
    functions Function headers for all required functions.
    test_json.c functions
    main(int argc, char✶ argv[])
    return type: int
    Test your all of the above functions using your miniunit.h..
    • This should consist primarily of calls to mu_run(_test_▒▒▒).
    • 100% code coverage is required.
    • Your main(…) must return EXIT_SUCCESS.
  2. parse_int(…), parse_string(…), parse_list(…), parse_boolean(…), parse_null(…), parse_object(…), and parse_element(…) should ignore any trailing characters in the input string, as long as it starts with a well-formed JSON element.
    • Acceptable: 123AAA, "12"AAA, "12",[,
  3. You only need to support the specific features of JSON that are explicitly required in this assignment description. You do not need to support unicode (e.g., "萬國碼", "يونيكود", "യൂണികോഡ്"), objects/dictionaries (e.g., {"a":1, "b":2}), backslash escapes (e.g., "\n"), embedded quotes (e.g., "He said, \"Roar!\""), floating point numbers (e.g., 3.1415), non-decimal notations (e.g., 0xdeadbeef, 0600), null, false.
  4. Do not modify json.h except as explicitly directed.
  5. Ensure there may be no memory faults (e.g., leaks, invalid read/write, etc.), even when parse_▒▒▒(…) return false.
  6. The following external header files, functions, and symbols are allowed.
    header functions/symbols allowed in…
    stdbool.h bool, true, false json.c, test_json.c
    stdio.h printf, fprintf, fputs, stdout, fflush json.c, test_json.c
    assert.h assert json.c, test_json.c
    ctype.h isdigit, isspace json.c, test_json.c
    stdlib.h EXIT_SUCCESS, abs, malloc, free, size_t json.c, test_json.c
    string.h strncpy, strchr, strlen, strcmp json.c, test_json.c
    limits.h INT_MIN, INT_MAX json.c, test_json.c
    miniunit.h anything test_json.c
    log_macros.h anything json.c, test_json.c
    For miniunit.h and log_macros.h, you can use anything from HW05. You are welcome to change them to your liking, and/or add more in the same spirit. All others are prohibited unless approved by the instructor. Feel free to ask if there is something you would like to use.
  7. Submissions must meet the code quality standards and the course policies on homework and academic integrity.

Submit

To submit HW13 from within your hw13 directory, type 264submit HW13 json.c json.h test_json.c miniunit.h log_macros.h Makefile

Pre-tester

The pre-tester for HW13 has been released and is ready to use.

Q&A

  1. How can I structure my tests?

    Here is a start. This assumes you have written your parse_json(…) function and ParseResult type.

    // Okay to copy/adapt, but ONLY IF YOU UNDERSTAND THIS CODE COMPLETELY.
    // ⚠ Do not copy blindly.
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include "json.h"
    #include "miniunit.h"
    
    int _test_int_valid_0() {
        mu_start();
        //────────────────────
        ParseResult result = parse_json("0");
        mu_check(result.is_success);
        if(result.is_success) {
            mu_check(result.element.type == ELEMENT_INT);
            mu_check(result.element.as_int == 0);
            free_element(result.element);  // should do nothing
        }
        //────────────────────
        mu_end();
    }
    
    
    int _test_int_valid_0() {
        mu_start();
        //────────────────────
        ParseResult result = parse_json("0");
        mu_check(result.is_success);
        if(result.is_success) {
            mu_check(result.element.type == ELEMENT_INT);
            mu_check(result.element.as_int == 0);
            free_element(result.element);  // should do nothing
        }
        //────────────────────
        mu_end();
    }
    
    
    int _test_string_valid_abc() {
        mu_start();
        //────────────────────
        Parse Result result = parse_json("\"abc\"");
        mu_check(result.is_success);
        if(result.is_success) {
            mu_check(result.element.type == ELEMENT_STRING);
            mu_check(strcmp(result.element.as_string, "abc") == 0);
            mu_check(strlen(result.element.as_string) == 3);
            free_element(result.element);
        }
        //────────────────────
        mu_end();
    }
    
    static int _test_list_of_ints_valid_1_2() {
        mu_start();
        //────────────────────
        ParseResult result = parse_json("[1, 2]");
        mu_check(result.is_success);
        if(result.is_success) {
            mu_check(result.element.type == ELEMENT_LIST);
            mu_check(result.element.as_list != NULL);
            mu_check(result.element.as_list -> element.as_int == 1);
            mu_check(result.element.as_list -> element.type == ELEMENT_INT);
            mu_check(result.element.as_list -> next != NULL);
            mu_check(result.element.as_list -> next -> element.type == ELEMENT_INT);
            mu_check(result.element.as_list -> next -> element.as_int == 2);
            free_element(result.element);
        }
        //────────────────────
        mu_end();
    }
    
    int main(int argc, char* argv[]) {
        mu_run(_test_int_valid_0);
        mu_run(_test_string_valid_abc);
        mu_run(_test_list_of_ints_valid_1_2);
        return EXIT_SUCCESS;
    }
    
    If you do not understand this code, do not use it.

    The code above covers all parts of this assignment. If you choose to use it, you will likely need to select only the parts relevant to the part you are doing right now. In any case, that is not an adequate test on its own. You will need more. This is just to get you started.

  2. What if I don't have parse_json(…) done yet?

    Here is a simpler version without parse_json(…)

    // OK TO COPY / ADAPT this snippet---but ONLY if you understand it completely.
    // ⚠ Do not copy blindly.
    // 
    // This test is nowhere near adequate on its own.  It is provided to illustrate how to
    // use helper functions to streamline your test code.
    
    #include <stdio.h>
    #include <stdlib.h>
    #include "json.h"
    #include "miniunit.h"
    
    static int _test_parse_int_valid() {
      mu_start();
      //──────────────────────────────────────────────────────
      int   result;  // will be initialized in parse_int(…)
      char* input = "0";
      char const* pos = input;
      bool is_success = parse_int(&result, &pos);
      mu_check(is_success);   // because the input is valid
      mu_check(pos == input + 1);
      mu_check(result == 0);
      //──────────────────────────────────────────────────────
      mu_end();
    }
    
    static int _test_parse_int_invalid() {
      mu_start();
      //──────────────────────────────────────────────────────
      int   result;  // will be initialized in parse_int(…)
      char* input = "A";
      char const* pos = input;
      bool is_success = parse_int(&result, &pos);
      mu_check(!is_success);  // because the input is valid
      mu_check(pos == input); // failure should be at the first character in the input
      //──────────────────────────────────────────────────────
      mu_end();
    }
    
    int main(int argc, char* argv[]) {
      mu_run(_test_parse_int_valid);
      mu_run(_test_parse_int_invalid);
      return EXIT_SUCCESS;
    }
    
  3. What should be the value of *a_pos after parse_▒▒▒(…) returns?

    _____________________________________________
    # EXAMPLE #1
    
    INPUT:
    
        123
    
    BEFORE we call parse_int(…):
    
        123
        ↑
        *a_pos
    
    RETURN value from parse_int(…):
    
        true
    
    After parse_int(…) returns:
    
        123
           ↑
           *a_pos refers to null terminator just
            after the integer literal.
    
        element.type   == ELEMENT_INT
        element.as_int == 123
    
    _____________________________________________
    # EXAMPLE #2
    
    INPUT:
    
        123ABC
    
    BEFORE we call parse_int(…):
    
        123ABC
        ↑
        *a_pos
    
    RETURN value from parse_int(…):
    
        true
    
    After parse_int(…) returns:
    
        123ABC
           ↑
           *a_pos refers to the non-digit character
            after the integer literal.
    
        element.type   == ELEMENT_INT
        element.as_int == 123
    
    _____________________________________________
    # EXAMPLE #3
    
    INPUT:
    
        -A1
    
    BEFORE we call parse_int(…):
    
        -A1
        ↑
        *a_pos
    
    RETURN value from parse_int(…):
    
        false
    
    After parse_int(…) returns:
    
        -A1
         ↑
         *a_pos refers first character that informed
          us this cannot be an integer literal.
    
        element.type   == (don't care)
        element.as_int == (don't care)
  4. What does the output of print_element(…) look like?

    It's just the inverse operation to parse_element(…).
    parse_element(…) takes JSON as input.
    print_element(…) prints JSON as output.
    If you were to parse the output of print_element(…) with parse_element(…) you should get an equivalent object.
    If you parse a JSON string and then print it again, you should get an equivalent string.
    If you're looking for concrete examples, just look at any example of input to parse_element(…) (except for the trailing characters). There are several at the top of this assignment description page.
  5. The specification says we don't have to handle escape sequences, but then it mentions escape sequences. Do we parse escape sequences or don't we? How do we handle backslash?

    We use C escape codes to make C string literals containing certain characters (e.g., double-quote, newline, etc.) in our C code.
    You don't have to parse JSON escape codes. Unless you are doing the escape sequence bonus, just treat a backslash like any other character.
    Here's an example to illustrate the distinction:
    #include <stdlib.h>
    #include <assert.h>
    #include <string.h>
    #include <stdio.h>
    #include "json.h"
    
    int main(int argc, char* argv[]) {
        Element element;   // 'element' will be initialized inside parse_element(…)
    
        // C escape codes used to create a C string literal containing double quotes
        char* json_input = "\"A\""; // same as: {'\"', 'A', '\"'}
        char const* pos = json_input;
        assert(strlen(json_input) == 3);
        parse_element(&element, &pos);
        printf(">>>|%s|<<<\n", element.as_string);
        // Output:
        // >>>|A|<<<
    
        // JSON escape codes
        json_input = "\"A\\nB\"";  // same as: {'\"', 'A', '\\', 'n', 'B', '\"'}
        assert(strlen(json_input) == 5);
        pos  = json_input;
        parse_element(&element, &pos);
        printf(">>>|%s|<<<\n", element.as_string);
        // Output:
        // >>>|A\nB|<<<
    
        // Output: (with escape code bonus)
        // >>>|A
        // B|<<<
    
        // Note: strlen(…) does not count the null terminator
    
        return EXIT_SUCCESS;
    }
    
  6. Should the double quotes be stored in memory?

    No. The double quotes are part of the JSON syntax.
    This is just like how in C, when you define a string like this:
    char s[] = "abc";
    
    … the double quotes are not stored in memory.
  7. How many file errors should I test? Do I have to test all error numbers in errno.h?

    You want to test at minimum 1 type of error, probably no need for more than 2. You do not need to worry about testing all errors as we are not testing fopen, we are testing our JSON functions.

    Some suggestions of reasonable errors to test are a file not existing (ENOENT), or attempting to read from a directory (EISDIR, though sometimes this may show up as EINVAL). You may also consider a too long filename (ENAMETOOLONG). A permissions error (EACCES) is also not hard to test for, but it may be unreliable (you have different permissions than the grader).

    Note many errors defined in errno.h are hard to trigger (e.g. ENOMEM or EDQUOT) or are not relevant to files (e.g. EHOSTDOWN), no need to worry about them; your goal is just finding a couple reasonable errors to test to make sure you properly return the number. I should really emphasize for everyone's sake, do not try to test errors from memory limits or file size limits such as out of memory errors or disk quota exceeded, you will probably just end up getting disconnected and possibly require us putting in a ticket with IT to get you connected; or worst case you could affect the ability for others to connect to the server or access the testers. File name limits are one of the few reasonable limits to test (that is, ENAMETOOLONG).

  8. My code freezes when trying to test file errors, what happened?

    Some times of invalid files can be opened but give unexpected behavior with feof(…). For example, when attempting to read from a directory (testing EISDIR), feof(…) never set to end of file. We can work around this by taking advantage of ferror(…), or by careful usage of EOF.

    The best approach is not worrying about EOF at all, use fseek(…) and ftell(…) to find the file position. fseek(…) will return non-zero if there was an error making it easy to detect.

    If you wanted to do this by checking for end of file, replace:

    for (char ch = fgetc(file); !feof(file); ch = fgetc(file)) {
       // work with ch here
    }
    
    with
    for (char ch = fgetc(file); !feof(file) && !ferror(file); ch = fgetc(file)) {
       // work with ch here
    }
    // handle errors during reading
    if (ferror(file)) {
      // do something with ferror/errno
    }
    
    We could also solve this problem using EOF, but there are a lot more pitfalls:
    for (int ch = fgetc(file); ch != EOF; ch = fgetc(file)) {
       // work with ch here
    }
    // if not at the end of the file, then something went wrong
    // could equivelently check ferror(file) here
    if (!feof(file)) {
      // do something with ferror/errno
    }
    
    Note the type of the loop variable must be int for this to work reliably. However, this is much more prone to bugs than just using fseek(…) and ftell(…), so you should use those.

  9. How do I ensure I am not printing too many commas when printing objects?

    The easiest method to do this is to just pass in a boolean did_print_first_element by address to your recursive printing logic. If the boolean is true, print the comma before your key-value pair; else set the boolean to true and do not print a comma.
  10. How do I ensure the keys are correct when printing?

    JSON objects do not preserve the order of keys when parsing, so printing in the original order is not required.

    We recommend just doing an in-order traversal for printing, as that will make all the keys in alphabetical order. However, any order is valid as long as all keys are eventually printed.

  11. How can I structure my object tests?

    Here's a start. (We may add to this at some point.)
    // OK TO COPY / ADAPT this snippet---but ONLY if you understand it completely.
    // ⚠ Do not copy blindly.
    // 
    // This test is nowhere near adequate on its own.  It is provided to illustrate how to
    // use helper functions to streamline your test code.
    
    #include <stdio.h>
    #include <stdlib.h>
    #include "json.h"
    #include "miniunit.h"
    
    // all tests from JSON 4, do not delete old tests!!!
    
    static int _test_parse_object_two_values() {
      mu_start();
      //──────────────────────────────────────────────────────
    
      char const* input = "{\"key1\": 123, \"key2\": \"value\"}ABC";
      char const* pos = input;
      BSTNode* root;
      bool is_success = parse_object(&root, &pos);
      mu_check(is_success);   // because the input is valid
      // I was too lazy to count the characters, so made C count it for me
      // the 3 is the trailing characters
      mu_check(pos == input + strlen(input) - 3); 
      mu_check(root != NULL);
      // we could check specific locations in the tree,
      // or we could just use the helper which does not care
      const Element* a_element = get_element(root, "key1");
      mu_check(a_element != NULL);
      mu_check(a_element->type == ELEMENT_INT);
      mu_check(a_element->as_int == 123);
      a_element = get_element(root, "key2");
      mu_check(a_element != NULL);
      mu_check(a_element->type == ELEMENT_STRING);
      mu_check_strings_equal(a_element->as_string, "value");
    
      free_element((Element){.as_object = root, .type = ELEMENT_OBJECT});
      //──────────────────────────────────────────────────────
      mu_end();
    }
    
    int main(int argc, char* argv[]) {
      mu_run(_test_parse_object_two_values);
      mu_run(_test_write_int_zero);
      return EXIT_SUCCESS;
    }
    
  12. That string looks super messy, is there a nicer way we can write it?

    You may find it easier to create JSON objects as files. This will prevent the need to escape all the double quotes. You could create helper functions to make your tests easier to write. Some examples are below, Example JSON object file contents:
          {
            "key1": "This is a string",
            "key2": 12345,
            "nested_object": {
                "with a nested list": [ 1, 2, true, null ],
                "okay, this object is probably a bit complex": true,
                "you probably want to test simplier ones :)": false
            }
          }
        

Updates

10/30/2022
  • “clog.h”“log_macros.h”
  • Updated Q&A test samples.
7/2/2024
  • Corrected a reference to parse_element in JSON part 1 requirement when this semester we are not doing it until part 2.
7/29/2024
  • Added objects for EC03.
7/30/2024
  • Corrected signature for insert_element().