1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
/*!
Writer to produce [PlantUML](https://plantuml.com/) text files for diagramming.

# Mapping

The mapping is reasonably simple, but also effective.

1. The namespace for the model becomes the name of a package enclosing all model elements.
1. Each service is a UML class with the `«service»` stereotype, and
   1. the `version` number is modeled as a field, with a value,
   1. the `operations` are modeled as UML operations, with the input and output copied from the
      operation shape, although errors are added as classes with the `«error»` stereotype,
   1. the `resources` are shown as separate classes (see next), with an aggregate relationship
      from service to resource.
1. Each resource is a UML class with the `«resource»` stereotype, and
   1. the `identifiers` map is modeled as individual fields on the class,
   1. the lifecycle methods are shown as UML operations, but they use the lifecycle name, and not
      the name of the operation shape,
   1. the operations and collection_operations are modeled as UML operations, with the input and
      output copied from the operation shape, although errors are added as classes with the
      `«error»` stereotype,
   1. the `resources` are shown as separate classes (see next), with an aggregate relationship
      from service to resource.
1. Each structure and union shape is modeled as a class, with a stereotype that is added, in order
   of precedence:  `«error»` if it has the error trait applied, `«union»` if it is a union shape
   or none,
   1. members of the shape are modeled as UML fields on the class.
1. Each simple shape is modeled as a `«dataType»` class,
   1. `member` for list and set as well as the `key` and `value` for a map are modeled as
      association relationships to the relevant types,
   1. the base type for the shape (string, list, etc.) is modeled as an inheritance relationship
      from the new type to a type in the Smithy model with the stereotype `«primitive»`
1. Traits on shapes are shown as UML fields, traits on members are not shown.
1. Note that the `error` trait is not shown in this way as it is used as a stereotype for those
   shapes that have it applied.
1. The `documentation` trait is also not shown in this way as it each becomes a note related to
   the shape.

# Example

For the _message of the day_ model, this writer will generate the following text.

```text
@startuml

hide empty members

package smithy.api {
    class blob <<primitive>> { }
    class boolean <<primitive>> { }
    class document <<primitive>> { }
    class string <<primitive>> { }
    class byte <<primitive>> { }
    class short <<primitive>> { }
    class integer <<primitive>> { }
    class long <<primitive>> { }
    class float <<primitive>> { }
    class double <<primitive>> { }
    class bigInteger <<primitive>> { }
    class bigDecimal <<primitive>> { }
    class timestamp <<primitive>> { }
}

package example.motd {

    class BadDateValue <<error>> {
        errorMessage: String
    }

    class GetMessageInput {
        date: string
    }

    class MessageOfTheDay <<service>> {
        version: string = "2020-06-21"
    }
    note "Provides a Message of the day." as MessageOfTheDay_note_0
    MessageOfTheDay .. MessageOfTheDay_note_0
    MessageOfTheDay *-- Message

    class Message <<resource>> {
        date: string
        create(in: GetMessageInput): GetMessageOutput
    }

    class Date <<dataType>> {
        @pattern = "^\d\d\d\d\-\d\d-\d\d$"
        ..
    }

    smithy.api.string <|-- Date

    class GetMessageOutput {
        message: String
    }

}
example.motd ..> smithy.api

@enduml
```

Which would produce an image like the following.

![PlantUML](https://raw.githubusercontent.com/johnstonskj/rust-atelier/master/atelier-lib/doc/motd-model.png)

*/

use atelier_core::io::ModelWriter;
use atelier_core::model::shapes::HasTraits;
use atelier_core::model::shapes::{ShapeKind, TopLevelShape};
use atelier_core::model::values::Value;
use atelier_core::model::HasIdentity;
use atelier_core::model::{Model, NamespaceID, ShapeID};
use atelier_core::prelude::PRELUDE_NAMESPACE;
use atelier_core::syntax::{
    MEMBER_COLLECTION_OPERATIONS, MEMBER_OPERATIONS, MEMBER_VERSION, SHAPE_BIG_DECIMAL,
    SHAPE_BIG_INTEGER, SHAPE_BLOB, SHAPE_BOOLEAN, SHAPE_BYTE, SHAPE_DOCUMENT, SHAPE_DOUBLE,
    SHAPE_FLOAT, SHAPE_INTEGER, SHAPE_LONG, SHAPE_RESOURCE, SHAPE_SERVICE, SHAPE_SHORT,
    SHAPE_STRING, SHAPE_TIMESTAMP, SHAPE_UNION,
};
use std::collections::HashSet;
use std::io::Write;
use std::str::FromStr;

// ------------------------------------------------------------------------------------------------
// Public Types
// ------------------------------------------------------------------------------------------------

///
/// This writer produces textual output supported by the [PlantUML](https://plantuml.com/) tools.
/// This is a relatively simple (and incomplete) mapping used to provide a useful overview diagram.
///
#[derive(Debug)]
pub struct PlantUmlWriter {
    expand_smithy_api: bool,
}

///
/// The extension to use when reading from, or writing to, files of this type.
///
pub const FILE_EXTENSION: &str = "uml";

// ------------------------------------------------------------------------------------------------
// Implementations
// ------------------------------------------------------------------------------------------------

impl Default for PlantUmlWriter {
    fn default() -> Self {
        Self {
            expand_smithy_api: false,
        }
    }
}

impl ModelWriter for PlantUmlWriter {
    fn write(&mut self, w: &mut impl Write, model: &Model) -> atelier_core::error::Result<()> {
        writeln!(w, "@startuml")?;
        writeln!(w)?;
        writeln!(w, "hide empty members")?;
        writeln!(w)?;
        if self.expand_smithy_api {
            self.write_smithy_model(w)?;
        }
        let namespaces: HashSet<&NamespaceID> =
            model.shape_names().map(ShapeID::namespace).collect();
        for namespace in namespaces {
            writeln!(w, "package {} {{", namespace)?;
            writeln!(w)?;
            for element in model.shapes() {
                match element.body() {
                    ShapeKind::Simple(_) => self.write_data_type(w, element, model)?,
                    ShapeKind::Service(_) => self.write_service(w, element, model)?,
                    ShapeKind::Resource(_) => self.write_resource(w, element, model)?,
                    ShapeKind::Structure(_) | ShapeKind::Union(_) => {
                        self.write_class(w, element, model)?
                    }
                    _ => {}
                }
            }
            writeln!(w, "}}")?;
            if self.expand_smithy_api {
                writeln!(w, "{} ..> smithy.api", namespace)?;
            }
        }
        writeln!(w)?;
        writeln!(w, "@enduml")?;
        Ok(())
    }
}

impl PlantUmlWriter {
    ///
    /// Construct a new writer, denoting whether you want to also visualize the Smithy API model
    /// as well. The default behavior is _not_ to expand this model.
    ///
    pub fn new(expand_smithy_api: bool) -> Self {
        Self { expand_smithy_api }
    }

    fn write_smithy_model(&self, w: &mut impl Write) -> atelier_core::error::Result<()> {
        writeln!(w, "package {} {{", PRELUDE_NAMESPACE)?;
        for name in &[
            SHAPE_BLOB,
            SHAPE_BOOLEAN,
            SHAPE_DOCUMENT,
            SHAPE_STRING,
            SHAPE_BYTE,
            SHAPE_SHORT,
            SHAPE_INTEGER,
            SHAPE_LONG,
            SHAPE_FLOAT,
            SHAPE_DOUBLE,
            SHAPE_BIG_INTEGER,
            SHAPE_BIG_DECIMAL,
            SHAPE_TIMESTAMP,
        ] {
            writeln!(w, "    class {} <<primitive>> {{ }}", name)?;
        }
        writeln!(w, "}}")?;
        writeln!(w)?;
        Ok(())
    }

    fn write_service(
        &self,
        w: &mut impl Write,
        service: &TopLevelShape,
        model: &Model,
    ) -> atelier_core::error::Result<()> {
        writeln!(w, "    class {} <<{}>> {{", service.id(), SHAPE_SERVICE)?;
        let notes = self.write_class_traits(w, service, model)?;
        let body = service.body().as_service().unwrap();
        writeln!(
            w,
            "        {}: {} = \"{}\"",
            MEMBER_VERSION,
            SHAPE_STRING,
            body.version()
        )?;
        for operation in body.operations() {
            self.write_operation(w, operation, model, None)?;
        }
        writeln!(w, "    }}")?;
        self.write_class_notes(w, service.id(), notes)?;
        for resource in body.resources() {
            writeln!(w, "    {} *-- {}", service.id(), resource)?;
        }
        writeln!(w)?;
        Ok(())
    }

    fn write_resource(
        &self,
        w: &mut impl Write,
        resource: &TopLevelShape,
        model: &Model,
    ) -> atelier_core::error::Result<()> {
        writeln!(w, "    class {} <<{}>> {{", resource.id(), SHAPE_RESOURCE)?;
        let notes = self.write_class_traits(w, resource, model)?;
        let body = resource.body().as_resource().unwrap();
        for (id, shape_id) in body.identifiers() {
            writeln!(w, "        {}: {}", id, shape_id)?;
        }
        if let Some(id) = body.create() {
            self.write_operation(w, id, model, Some("create"))?;
        }
        if let Some(id) = body.put() {
            self.write_operation(w, id, model, Some("create"))?;
        }
        if let Some(id) = body.read() {
            self.write_operation(w, id, model, Some("create"))?;
        }
        if let Some(id) = body.update() {
            self.write_operation(w, id, model, Some("create"))?;
        }
        if let Some(id) = body.delete() {
            self.write_operation(w, id, model, Some("create"))?;
        }
        if let Some(id) = body.list() {
            self.write_operation(w, id, model, Some("create"))?;
        }
        if body.has_operations() {
            writeln!(w, "        ..{}..", MEMBER_OPERATIONS)?;
            for operation in body.operations() {
                self.write_operation(w, operation, model, None)?;
            }
        }
        if body.has_collection_operations() {
            writeln!(w, "        ..{}..", MEMBER_COLLECTION_OPERATIONS)?;
            for operation in body.collection_operations() {
                self.write_operation(w, operation, model, None)?;
            }
        }
        writeln!(w, "    }}")?;
        self.write_class_notes(w, resource.id(), notes)?;
        for other in body.resources() {
            writeln!(w, "    {} *-- {}", resource.id(), other)?;
        }
        writeln!(w)?;
        Ok(())
    }

    fn write_class(
        &self,
        w: &mut impl Write,
        structure: &TopLevelShape,
        model: &Model,
    ) -> atelier_core::error::Result<()> {
        let (is_union, body) = match structure.body() {
            ShapeKind::Structure(s) => (false, s),
            ShapeKind::Union(s) => (true, s),
            _ => unreachable!(),
        };
        if structure.has_trait(&ShapeID::from_str("smithy.api#trait").unwrap()) {
            writeln!(w, "    annotation {} {{", structure.id())?;
        } else if structure.has_trait(&ShapeID::from_str("smithy.api#error").unwrap()) {
            writeln!(w, "    class {} <<error>> {{", structure.id())?;
        } else if is_union {
            writeln!(w, "    class {} <<{}>> {{", structure.id(), SHAPE_UNION)?;
        } else {
            writeln!(w, "    class {} {{", structure.id())?;
        }
        let notes = self.write_class_traits(w, structure, model)?;
        for member in body.members() {
            writeln!(w, "        {}: {}", member.id(), member.target())?;
        }
        writeln!(w, "    }}")?;
        self.write_class_notes(w, structure.id(), notes)?;
        writeln!(w)?;
        Ok(())
    }

    fn write_data_type(
        &self,
        w: &mut impl Write,
        data_type: &TopLevelShape,
        model: &Model,
    ) -> atelier_core::error::Result<()> {
        writeln!(w, "    class {} <<dataType>> {{", data_type.id())?;
        let notes = self.write_class_traits(w, data_type, model)?;
        writeln!(w, "    }}")?;
        writeln!(w, "    {}", notes.join("\n"))?;
        if self.expand_smithy_api {
            let simple = data_type.body().as_simple().unwrap();
            writeln!(
                w,
                "    {}.{} <|-- {}",
                PRELUDE_NAMESPACE,
                simple,
                data_type.id()
            )?;
        }
        writeln!(w)?;
        Ok(())
    }

    fn write_operation(
        &self,
        w: &mut impl Write,
        oper_id: &ShapeID,
        model: &Model,
        other_name: Option<&str>,
    ) -> atelier_core::error::Result<()> {
        let operation = model.shape(oper_id).unwrap();
        let name = operation.id();
        let operation = operation.body().as_operation().unwrap();
        writeln!(
            w,
            "        {}(in: {}){}",
            match other_name {
                None => name.to_string(),
                Some(name) => name.to_string(),
            },
            if operation.has_input() {
                self.type_string(&operation.input().as_ref().unwrap(), model)
            } else {
                String::new()
            },
            if operation.has_output() {
                format!(
                    ": {}",
                    self.type_string(&operation.output().as_ref().unwrap(), model)
                )
            } else {
                String::new()
            },
        )?;
        Ok(())
    }

    fn write_class_traits(
        &self,
        w: &mut impl Write,
        shape: &TopLevelShape,
        _model: &Model,
    ) -> atelier_core::error::Result<Vec<String>> {
        let mut traits: Vec<String> = Default::default();
        let mut notes: Vec<String> = Default::default();
        for (id, value) in shape.traits() {
            if id == &ShapeID::from_str("smithy.api#error").unwrap() {
                // ignore
            } else if id == &ShapeID::from_str("smithy.api#documentation").unwrap() {
                if let Some(Value::String(s)) = value {
                    notes.push(s.clone())
                }
            } else {
                traits.push(match value {
                    None | Some(Value::None) => format!("    @{}", id),
                    Some(Value::String(v)) => format!("    @{} = \"{}\"", id, v),
                    Some(Value::Number(v)) => format!("    @{} = {}", id, v),
                    Some(Value::Boolean(v)) => format!("    @{} = {}", id, v),
                    Some(Value::Array(_)) => format!("    @{} = [ .. ]", id),
                    Some(Value::Object(_)) => format!("    @{} = {{ .. }}", id),
                });
            }
        }
        if !traits.is_empty() {
            writeln!(w, "    {}", traits.join("\n"))?;
            writeln!(w, "        ..")?;
        }
        Ok(notes)
    }

    fn write_class_notes(
        &self,
        w: &mut impl Write,
        shape_id: &ShapeID,
        notes: Vec<String>,
    ) -> atelier_core::error::Result<()> {
        for (idx, note) in notes.iter().enumerate() {
            if note.contains('\n') {
                writeln!(w, "    note as {}_note_{}", shape_id, idx)?;
                writeln!(w, "{:?}", note)?;
                writeln!(w, "    end note")?;
            } else {
                writeln!(w, "    note {:?} as {}_note_{}", note, shape_id, idx)?;
            }
            writeln!(w, "    {} .. {}_note_{}", shape_id, shape_id, idx)?;
        }
        Ok(())
    }

    fn type_string(&self, type_id: &ShapeID, model: &Model) -> String {
        if let Some(shape) = model.shape(type_id) {
            match shape.body() {
                ShapeKind::Simple(st) => st.to_string(),
                ShapeKind::List(list) => format!("List<{}>", list.member().target().shape_name()),
                ShapeKind::Set(set) => format!("Set<{}>", set.member().target().shape_name()),
                ShapeKind::Map(map) => format!(
                    "Map<{}, {}>",
                    map.key().target().shape_name(),
                    map.value().target().shape_name()
                ),
                _ => type_id.to_string(),
            }
        } else {
            type_id.to_string()
        }
    }
}