Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 59 additions & 10 deletions crates/ogar-from-ruff/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,25 @@ use ruff_spo_triplet::{
ModelGraph, ScopeDecl, ScopeKind, StiInfo, Validation as RuffValidation, ValidationKind,
};

/// Lift every model in a [`ModelGraph`] to an OGAR [`Class`]. Output
/// preserves declaration order so downstream consumers can rely on
/// deterministic ordering for snapshot tests.
/// Lift every model in a [`ModelGraph`] to an OGAR [`Class`] (Rails /
/// `ruff_ruby_spo` producer — [`Language::Ruby`]). Output preserves
/// declaration order so downstream consumers can rely on deterministic
/// ordering for snapshot tests.
#[must_use]
pub fn lift_model_graph(graph: &ModelGraph) -> Vec<Class> {
lift_model_graph_with_language(graph, Language::Ruby)
}

/// Lift a whole [`ModelGraph`] for a Python / Odoo producer
/// (`ruff_python_spo`) — like [`lift_model_graph`] but stamps each class as
/// [`Language::Python`]. The `odoo` namespace already routes to the `erp`
/// source domain via the same `classify_domain` path.
#[must_use]
pub fn lift_model_graph_python(graph: &ModelGraph) -> Vec<Class> {
lift_model_graph_with_language(graph, Language::Python)
}

fn lift_model_graph_with_language(graph: &ModelGraph, language: Language) -> Vec<Class> {
let domain = classify_domain(&graph.namespace);
let concept_domain = domain.as_deref().and_then(ogar_vocab::source_domain_concept);
// The harvest namespace IS the curator id (`"openproject"`,
Expand All @@ -94,7 +108,7 @@ pub fn lift_model_graph(graph: &ModelGraph) -> Vec<Class> {
.models
.iter()
.map(|m| {
let mut class = lift_model(m);
let mut class = lift_model_with_language(m, language);
class.source_domain = domain.clone();
class.source_curator = curator.clone();
// Domain-gate the canonical concept. `lift_model` resolves
Expand Down Expand Up @@ -130,18 +144,31 @@ fn classify_domain(namespace: &str) -> Option<String> {
}
}

/// Lift one [`Model`] to an OGAR [`Class`]. Pure projection — no I/O.
/// Lift one [`Model`] to an OGAR [`Class`] stamped as [`Language::Ruby`]
/// (the Rails / `ruff_ruby_spo` producer). Pure projection — no I/O.
///
/// **Language is always [`Language::Ruby`]** at this layer. Other
/// frontends (Python/Elixir/etc.) using `ruff_spo_triplet::Model` will
/// likely want their own wrapper that sets the discriminant. We don't
/// guess it from `ModelGraph::namespace` because the namespace
/// For the Python / Odoo producer (`ruff_python_spo`) use
/// [`lift_model_python`]. Both delegate to the same projection and differ
/// only in the language discriminant. Language is set explicitly rather
/// than guessed from `ModelGraph::namespace`, because the namespace
/// (`openproject`, `odoo`, …) doesn't bind to the producer language
/// one-to-one.
#[must_use]
pub fn lift_model(model: &Model) -> Class {
lift_model_with_language(model, Language::Ruby)
}

/// Lift one [`Model`] to an OGAR [`Class`] stamped as [`Language::Python`]
/// — the Odoo / Django producer path (`ruff_python_spo`). Identical
/// projection to [`lift_model`]; only the language discriminant differs.
#[must_use]
pub fn lift_model_python(model: &Model) -> Class {
lift_model_with_language(model, Language::Python)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Map Python fields before exposing Odoo path

For ruff_python_spo/Odoo graphs with fields.X declarations, the ruff IR records those as Model::fields (the core SPO shape), while lift_model_with_language only reads the Rails-side model.attributes/model.associations vectors and never populates Class.attributes, Class.associations, or Class.computed_fields from fields. Delegating the new Python API directly to that projection means a normal Odoo model can be stamped Python/erp but lose its schema, so downstream consumers receive empty classes.

Useful? React with 👍 / 👎.

}

fn lift_model_with_language(model: &Model, language: Language) -> Class {
let mut class = Class::new(&model.name);
class.language = Language::Ruby;
class.language = language;
class.parent = model.sti.as_ref().and_then(sti_parent);
class.inheritance = lift_inheritance(model);
class.canonical_concept = Some(canonical_concept(&model.name));
Expand Down Expand Up @@ -667,6 +694,28 @@ mod tests {
assert!(matches!(class.language, Language::Ruby));
}

#[test]
fn lift_model_python_stamps_python_language() {
// The Python/Odoo producer path: same projection, Python discriminant.
let class = lift_model_python(&mk_model());
assert!(matches!(class.language, Language::Python));
assert_eq!(class.name, "WorkPackage");
assert_eq!(class.parent.as_deref(), Some("Issue"));
}

#[test]
fn lift_model_graph_python_stamps_python_and_keeps_erp_domain() {
// An Odoo ModelGraph (namespace "odoo") lifts as Python and routes to
// the `erp` source domain / `odoo` curator via classify_domain.
let mut graph = ModelGraph::new("odoo");
graph.models.push(Model::new("account_move"));
let classes = lift_model_graph_python(&graph);
assert_eq!(classes.len(), 1);
assert!(matches!(classes[0].language, Language::Python));
assert_eq!(classes[0].source_domain.as_deref(), Some("erp"));
assert_eq!(classes[0].source_curator.as_deref(), Some("odoo"));
}

#[test]
fn lift_inheritance_concrete_from_sti_parent() {
// mk_model's StiInfo has inherits_from = Some("Issue").
Expand Down
Loading