Make struct fields visible to selected modules

In a large code base it is often bad to make struct fields public, because it opens up ways to misuse the fields. It is better to use them through a pre-defined API. However there are special cases where you want to access the struct directly, for example when constructing instances of the struct for testing.

assert_eq!(
    fn_to_test(),
    ResultType {
        field_a: 1,
        field_b: "correct".to_string(),
    },
    "test numero uno",
);

You can do pub(crate) to use the fields with public access from within your whole crate, but this is a little too permissive in this case. I would like something like pub(crate::{modA::tests, modB::tests}) or similar, to be able to expose items to specific modules of the crate but have them private to the remaining crate.

Is there a way to do this with the current features of Rust?

(The alternative would of course be to implement an API for constructing the test results, but I find this much less expressive because I can't see the field names and have to remember the constructor signature.)

If those tests are unit tests then you can probably put them in a submodule of your struct module, thus having access to those fields even if they're not pub.

A somewhat hacky way would be to make the fields pub(crate) only in tests:

#[derive(PartialEq, Debug)]
pub struct ResultType {
    #[cfg(not(test))]
    field_a: i32,
    #[cfg(test)]
    pub(crate) field_a: i32,

    #[cfg(not(test))]
    field_b: String,
    #[cfg(test)]
    pub(crate) field_b: String,
}

This could probably be wrapped up into a macro_rules! macro, but it would be tricky to parse the whole struct correctly.

2 Likes

Would the pub(in crate::modA::tests) syntax work for you?

Keep in mind that your xxx::tests modules are probably annotated with #[cfg(test)] so they don't exist when building the crate normally, meaning you'll probably get "no such module" errors when building the main crate.

What about adding constructors or getters that only exist during testing?

struct ResultType { field_a: u32 }

#[cfg(test)]
impl ResultType {
  pub fn new(field_a: u32) -> Self { ... }
  pub fn field_a(&self) -> u32 { self.field_a }
}
3 Likes

The pub(in crate::path) visibility is only allowed when crate::path is an ancestor of the current module. The asker wants to let other modules' tests access the fields while not letting the modules themselves access the fields (otherwise, pub(crate) would work).

1 Like

This is basically what I've been looking for, but I didn't think about the #[cfg(test)] annotation.. Also it seems @LegionMammal978 is right, pub(in path) only works for ancestor modules..

What about adding constructors or getters that only exist during testing?

This is always an option, but I find it is quite a lot of boilerplate and it makes the tests less readable.. to illustrate, this is what a test looks like when the result type's fields are pub(crate):

assert_eq!(
    byz_found,
    Ok(QueryFound {
        found: FoundPath::Range(GraphRangePath {
            start: StartPath::Path {
                entry: xabyz.to_pattern_location(xaby_z_id)
                    .to_child_location(0),
                path: vec![
                    ChildLocation {
                        parent: xaby,
                        pattern_id: xa_by_id,
                        sub_index: 1,
                    },
                ],
                width: 2,
            },
            end: vec![],
            exit: 1,
            end_width: 1,
        }),
        query: QueryRangePath {
            exit: query.len() - 1,
            query,
            entry: 0,
            start: vec![],
            end: vec![],
        },
    }),
    "by_z"
);

and this would be with constructors:

assert_eq!(
    byz_found,
    Ok(QueryFound::new(
        FoundPath::Range(GraphRangePath::new(
            StartPath::new_path(
                xabyz.to_pattern_location(xaby_z_id)
                    .to_child_location(0),
                vec![
                    ChildLocation {
                        parent: xaby,
                        pattern_id: xa_by_id,
                        sub_index: 1,
                    },
                ],
                2,
            ),
            vec![],
            1,
            1,
        )),
        QueryRangePath::new(
            query.len() - 1,
            query,
            0,
            vec![],
            vec![],
        ),
    )),
    "by_z"
);

Without the field names, it becomes very difficult to tell what some values mean. You would have to add comments, but this makes formatting more tedious.

I think I will take a shot at the macro for switching the visibility for different cfg settings, that looked very promising.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.