Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
Fix tests, don't start mangled names with `___` (for now, see Nickel
bug), put predicate lib under a mangled name as well.

TODO: fixpoint for the reachability of definitons and properties: when
building the environment, converting definitions might create new usages
of other definitions!
  • Loading branch information
yannham committed Apr 10, 2024
1 parent 56635c5 commit 5636e90
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 106 deletions.
12 changes: 9 additions & 3 deletions src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use crate::{
definitions::{self, RefsUsage},
predicates::{AsPredicate, Predicate},
utils::static_access,
PREDICATES_LIBRARY,
};

fn only_ignored_fields<V>(extensions: &BTreeMap<String, V>) -> bool {
Expand Down Expand Up @@ -75,6 +76,11 @@ impl Contract {
.try_as_contract(&mut refs_usage)
.map(|ctr| (ctr, refs_usage))
}

/// Return the `Dyn` contract, always succeeding.
pub fn dynamic() -> Self {
Term::Type(TypeF::Dyn.into()).into()
}
}

/// [TryAsContract] is essentially like `TryInto<Contract>` but passes additional state around used for
Expand Down Expand Up @@ -329,7 +335,7 @@ impl From<RichTerm> for Contract {
impl From<Contract> for RichTerm {
fn from(Contract(c): Contract) -> Self {
match c.as_slice() {
[] => static_access("predicates", ["always"]).into(),
[] => static_access(PREDICATES_LIBRARY, ["always"]).into(),
// TODO: shouldn't need to clone here
[rt] => rt.clone(),
_ => {
Expand All @@ -356,7 +362,7 @@ impl From<&InstanceType> for Contract {
fn from(value: &InstanceType) -> Contract {
match value {
InstanceType::Null => Contract::from(Predicate::from(mk_app!(
static_access("predicates", ["isType"]),
static_access(PREDICATES_LIBRARY, ["isType"]),
Term::Enum("Null".into())
))),
InstanceType::Boolean => Contract::from(TypeF::Bool),
Expand Down Expand Up @@ -461,7 +467,7 @@ impl From<Predicate> for Contract {
// messages)
fn from(pred: Predicate) -> Self {
mk_app!(
static_access("predicates", ["contract_from_predicate"]),
static_access(PREDICATES_LIBRARY, ["contract_from_predicate"]),
pred
)
.into()
Expand Down
156 changes: 99 additions & 57 deletions src/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,14 @@ use nickel_lang_core::{
record::{Field, FieldMetadata, RecordData},
LetAttrs, RichTerm, Term,
},
typ::TypeF,
};
use schemars::schema::{RootSchema, Schema, SchemaObject};

use crate::{
contracts::{Contract, Documentation},
predicates::Predicate,
utils::{decode_json_ptr_part, static_access},
DEFINITIONS_MANGLED, PROPS_PREDICATES_MANGLED,
DEFINITIONS_MANGLED, ENVIRONMENT_MANGLED, PROPS_PREDICATES_MANGLED,
};

/// Specify if a reference is used in a context which requires a contract or a predicate.
Expand Down Expand Up @@ -153,46 +152,62 @@ pub struct ConvertedDef {
contract: Option<Contract>,
}

/// The conversion of a JSON schema property into a Nickel predicate.
#[derive(Clone)]
pub struct ConvertedProp {
doc: Option<Documentation>,
predicate: Predicate,
}


impl ConvertedDef {
/// Convert the contract part of this definition to a record field with the appropriate
/// definition. This method returns `None` if `self.contract` is `None`.
pub fn contract_as_field(&self) -> Option<Field> {
self.as_field(&self.contract)
/// Take the contract part out of this definition and convert it to a record field with the
/// appropriate definition. This method returns `None` if `self.contract` is `None`.
///
/// After calling this method, `self.contract` will be `None`.
pub fn contract_as_field(&mut self) -> Option<Field> {
Self::as_field(self.contract.take(), self.doc.clone())
}

/// Convert the contract part of this definition to a record field with the appropriate
/// definition. This method returns `None` if `self.contract` is `None`.
pub fn predicate_as_field(&self) -> Option<Field> {
self.as_field(&self.predicate)
/// Take the predicate part out of this definition and convert it to a record field with the
/// appropriate definition. This method returns `None` if `self.contract` is `None`.
///
/// After calling this method, `self.predicate` will be `None`.
pub fn predicate_as_field(&mut self) -> Option<Field> {
Self::as_field(self.predicate.take(), self.doc.clone())
}

/// Helper including the logic common to `contract_as_field` and `predicate_as_field`.
fn as_field<V>(&self, value: &Option<V>) -> Option<Field>
fn as_field<V>(value: Option<V>, doc: Option<Documentation>) -> Option<Field>
where
RichTerm: From<V>,
V: Clone,
{
let value = RichTerm::from(value.as_ref()?.clone());
let value = RichTerm::from(value?);

Some(Field {
value: Some(value),
metadata: FieldMetadata {
doc: self.doc.clone().map(String::from),
doc: doc.map(String::from),
..Default::default()
},
..Default::default()
})
}
}

/// The conversion of a JSON schema property into a Nickel predicate.
#[derive(Clone)]
pub struct ConvertedProp {
doc: Option<Documentation>,
predicate: Predicate,
}

impl From<ConvertedProp> for Field {
fn from(value: ConvertedProp) -> Self {
Field {
value: Some(value.predicate.into()),
metadata: FieldMetadata {
doc: value.doc.map(String::from),
..Default::default()
},
..Default::default()
}
}
}

/// State recording which properties and definitions are actually used and how (as predicates or as
/// contracts).
#[derive(Clone, Default)]
Expand Down Expand Up @@ -252,8 +267,8 @@ pub fn resolve_ref(reference: &str, state: &mut RefsUsage, usage: RefUsage) -> R
);

match usage {
RefUsage::Contract => Term::Type(TypeF::Dyn.into()).into(),
RefUsage::Predicate => static_access("predicates", ["always"]),
RefUsage::Contract => Contract::dynamic().into(),
RefUsage::Predicate => Predicate::always().into(),
}
};

Expand All @@ -271,20 +286,26 @@ pub fn resolve_ref(reference: &str, state: &mut RefsUsage, usage: RefUsage) -> R
// as a separator as a key. See the documentation of `PROPS_PREDICATES_MANGLED`
// for more information.
static_access(
PROPS_PREDICATES_MANGLED,
[field_path.path.join("/").as_str()],
ENVIRONMENT_MANGLED,
[PROPS_PREDICATES_MANGLED, field_path.path.join("/").as_str()],
)
}
}
} else if let Some(name) = json_ptr.try_as_def() {
match usage {
RefUsage::Contract => {
state.defs_contracts.insert(name.clone());
static_access(DEFINITIONS_MANGLED, [name.as_ref(), "contract"])
static_access(
ENVIRONMENT_MANGLED,
[DEFINITIONS_MANGLED, "contracts", name.as_ref()],
)
}
RefUsage::Predicate => {
state.defs_predicates.insert(name.clone());
static_access(DEFINITIONS_MANGLED, [name.as_ref(), "predicate"])
static_access(
ENVIRONMENT_MANGLED,
[DEFINITIONS_MANGLED, "predicates", name.as_ref()],
)
}
}
} else {
Expand Down Expand Up @@ -371,49 +392,70 @@ impl Environment {
/// Wrap a Nickel [`RichTerm`] in a let binding containing the definitions
/// from the environment. This is necessary for the Nickel access terms
/// tracked in the environment to actually work.
pub fn wrap(self, inner: RichTerm) -> RichTerm {
pub fn wrap(mut self, inner: RichTerm) -> RichTerm {
let contracts = self
.definitions
.iter()
.iter_mut()
.filter_map(|(k, v)| Some((Ident::from(k), v.contract_as_field()?)))
.collect();

let predicates = self
.definitions
.iter()
.filter_map(|(k, v)| Some((Ident::from(k), v.predicate_as_field()?)))
.into_iter()
.filter_map(|(k, mut v)| Some((Ident::from(k), v.predicate_as_field()?)))
.collect();

let prop_preds = self
.property_preds
.iter()
.filter_map(|(k, v)| Some((Ident::from(k.join("/")), v.clone())))
.into_iter()
.filter_map(|(k, v)| Some((Ident::from(k.join("/")), Field::from(v))))
.collect();

// All the definitions as a Nickel record
let defs = Term::Record(RecordData::with_field_values(
[
(
Ident::from("contracts"),
Term::Record(RecordData {
fields: contracts,
..Default::default()
})
.into(),
),
(
Ident::from("predicates"),
Term::Record(RecordData {
fields: predicates,
..Default::default()
})
.into(),
),
]
.into_iter()
.collect(),
))
.into();

// All the properties (predicates) as a Nickel record
let props = Term::Record(RecordData {
fields: prop_preds,
..Default::default()
})
.into();

// The enclosing record, with one field for the definitions and one for the properties
let global_env = Term::Record(RecordData::with_field_values(
[
(Ident::from(DEFINITIONS_MANGLED), defs),
(Ident::from(PROPS_PREDICATES_MANGLED), props),
]
.into_iter()
.collect(),
));

Term::Let(
"definitions".into(),
Term::Record(RecordData::with_field_values(
[
(
Ident::from("contracts"),
Term::Record(RecordData {
fields: contracts,
..Default::default()
})
.into(),
),
(
Ident::from("predicates"),
Term::Record(RecordData {
fields: predicates,
..Default::default()
})
.into(),
),
]
.into_iter()
.collect(),
))
.into(),
Ident::from(ENVIRONMENT_MANGLED),
global_env.into(),
inner,
LetAttrs {
rec: true,
Expand Down
34 changes: 25 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub mod definitions;
pub mod predicates;
pub(crate) mod utils;

use contracts::{AsCtrThruPred, Contract, TryAsContract};
use contracts::Contract;
use definitions::Environment;
use nickel_lang_core::{
cache::{Cache, ErrorTolerance},
Expand All @@ -34,6 +34,21 @@ use nickel_lang_core::{
use predicates::Predicate;
use schemars::schema::RootSchema;

/// The top-level variable storing the json-schema-to-nickel predicate library included by default
/// in any generated contract.
pub const PREDICATES_LIBRARY: &str = "_js2n_nickel_preds_lib";

/// The top-level variable storing the environment, that is the definitions and the properties
/// referenced in the JSON schema (through the `$ref`) attribute. This variable stores
/// [DEFINITIONS_MANGLED] and [PROPS_PREDICATES_MANGLED], each in their own field.
///
/// We put both under the same variable so that definitions and properties are accessible from
/// everywhere, including from other definitions and properties (in fact, we would like to have
/// mutual recursive let definitions for [DEFINITIONS_MANGLED] and [PROPS_PREDICATES_MANGLED], but
/// Nickel doesn't have mutually recursive lets, so we put both in a recursive record instead).
// pub const ENVIRONMENT_MANGLED: &str = "___js2n_nickel_global_env";
pub const ENVIRONMENT_MANGLED: &str = "_js2n_nickel_global_env";

/// The name of the special variable introduced by json-schema-to-nickel in the final contract
/// which holds the predicates and the contracts corresponding to the definitions of the schema.
/// The name is long and specific on purpose as it could clash with existing variable in the
Expand All @@ -42,7 +57,8 @@ use schemars::schema::RootSchema;
/// This Nickel variable is expected to have the type
/// `{_ : {predicate: _, contract: _}}` where field names correspond to the top-level
/// definitions in the schema.
pub const DEFINITIONS_MANGLED: &str = "___js2n_nickel_defs";
// pub const DEFINITIONS_MANGLED: &str = "___js2n_nickel_defs";
pub const DEFINITIONS_MANGLED: &str = "_js2n_nickel_defs";

/// Same as [DEFINITIONS_MANGLED] but for the predicates corresponding to properties of the schema.
///
Expand All @@ -53,19 +69,19 @@ pub const DEFINITIONS_MANGLED: &str = "___js2n_nickel_defs";
/// Properties can be nested, so we might need to store both a predicate for `foo` and for
/// `foo.bar.baz`. To make this work, we store the predicates in a flat dictionary, where the keys
/// are complete paths using `/` as a separator (to avoid confusion with Nickel field path).
pub const PROPS_PREDICATES_MANGLED: &str = "___js2n_nickel_prop_preds";
// pub const PROPS_PREDICATES_MANGLED: &str = "___js2n_nickel_prop_preds";
pub const PROPS_PREDICATES_MANGLED: &str = "_js2n_nickel_prop_preds";

/// Convert a [`RootSchema`] into a Nickel contract. If the JSON schema is
/// representable as a lazy record contract, this conversion is preferred.
/// Otherwise, we fall back to generating a predicate.
pub fn root_schema(root: &RootSchema) -> RichTerm {
let env = Environment::new(todo!(), todo!());

let (contract, refs_usage) = Contract::from_root_schema(root).unwrap_or_else(|| {
let (predicate, refs_usage) = Predicate::from_root_schema(root);
pub fn root_schema(root_schema: &RootSchema) -> RichTerm {
let (contract, refs_usage) = Contract::from_root_schema(root_schema).unwrap_or_else(|| {
let (predicate, refs_usage) = Predicate::from_root_schema(root_schema);
(Contract::from(predicate), refs_usage)
});

let env = Environment::new(root_schema, &refs_usage);
wrap_contract(env, contract)
}

Expand All @@ -82,7 +98,7 @@ pub fn wrap_contract(env: Environment, contract: Contract) -> RichTerm {
let lib_rt = parser.parse_strict(file_id, lexer).unwrap();

Term::Let(
"predicates".into(),
PREDICATES_LIBRARY.into(),
lib_rt,
env.wrap(contract.into()),
Default::default(),
Expand Down
Loading

0 comments on commit 5636e90

Please sign in to comment.