praxis 2.0.pre.31 → 2.0.pre.32

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b0d313f269a5d2eee55bec87524dc37d0b424ed747969fa35f3f8454e658028
4
- data.tar.gz: 894589492bcd9d0cd1c8af237215892e163c9229f6f81a6aa90a709b43623a25
3
+ metadata.gz: 481a1aedfa10aa914a0b0bba13a1b39ca3de5d9ecfd3840520674919fc82563e
4
+ data.tar.gz: 7796b1d757c700cb9e56a9d16f1471ef5930ee0fe19b3b596d948a9a8b7f8b3c
5
5
  SHA512:
6
- metadata.gz: 1744d64f7e8521185d687cc124c8b553b297f79734c74b0bffb83bab7a5f8bdbacf1e71899ffce8044fddbf1d92902270fae4892425808aaedc6eff7a2490f61
7
- data.tar.gz: c5602b15ddf593f4b387f39c398fd411158e2b936e5763a21cf01edb86c3ea4c87f3a09f95c5fa2bff29317075ebae6bbec52eba5190b9594359e335e14ef23f
6
+ metadata.gz: 91b4c7f698267e0a533ccc1ba73e500ed2c9570550b83dacee4cc1ff80bd615e55b66bfd9707ba25c05b38599a98da2661505aec15e96def2d0ae1b3f94f2b6e
7
+ data.tar.gz: c21b5bed2302eba252b58fa78da841c82227994796344eacc0d2b9c20608d166e3f9e5fd3e5bb73c2fa191b909891c5d901627832de94a4a594ee085be3f26b6
data/.gitignore CHANGED
@@ -3,6 +3,7 @@ coverage
3
3
  Gemfile.lock
4
4
  _site/
5
5
  .sass-cache/
6
+ .vscode
6
7
 
7
8
  .data/
8
9
  .tmp/
@@ -15,4 +16,4 @@ lib/api_browser/app/bower_components/
15
16
  lib/api_browser/.tmp/
16
17
  lib/api_browser/bower.json
17
18
 
18
- development.sqlite3
19
+ development.sqlite3
data/CHANGELOG.md CHANGED
@@ -2,143 +2,175 @@
2
2
 
3
3
  ## next
4
4
 
5
+ ## 2.0.pre.32
6
+ - Spruced up the scaffolding generation, to be more configurable using a `.praxis_scaffold` file at the root, where one can specify things like
7
+ the base module for all generated classes (`base`), the path of the models directory (`models_dir`) and the version to use (`version`). These, except the models directory can also be passed and overriden by command line arguments (and they would be saved into the config file to be usable in future invocations)
8
+ - More efficient validation of Blueprint structures
9
+ - Fix pseudo bug where the field expander capped subfields as soon as it encountered a type without explicit attributes (i.e., Hash). Instead, it allows any of the subfields to percolate through.
10
+ - OpenApi generation improvements:
11
+ - Add global parameters for versioning (ApiVersionHeader and ApiVersionParam) appropriately if the API is versioned by them. Have actions point to these definitions by $ref
12
+ - Expect the 'server' definition in the APIDefinition to contain url/description and 'variables' sections which might define variables in the server url
13
+
5
14
  ## 2.0.pre.31
6
- * Switch the locally generated index.html file to browse the generated OpenAPI docs to use `elements` instead of `reDoc`
7
- * Spruce up the initial Gemfile for the generated example app
8
- * Fix Praxis::Mapper ordering code, to not prefix top level columns with the table name, as this completely confuses ActiveRecord which switches to full-on eager loading all of the associations in the top query. i.e., passing an invalid table/column name in a `references` method will trigger that (seemingly a current bug)
15
+
16
+ - Switch the locally generated index.html file to browse the generated OpenAPI docs to use `elements` instead of `reDoc`
17
+ - Spruce up the initial Gemfile for the generated example app
18
+ - Fix Praxis::Mapper ordering code, to not prefix top level columns with the table name, as this completely confuses ActiveRecord which switches to full-on eager loading all of the associations in the top query. i.e., passing an invalid table/column name in a `references` method will trigger that (seemingly a current bug)
9
19
 
10
20
  ## 2.0.pre.30
11
- * A few cleanup and robustness additions:
12
- * OpenAPI: Disable overriding a description when the schema is a ref (there are known issues with UI browsers)
13
- * Internal: use `_pk` in batch processor invocation instead of `id` (resources will now have a `_pk` method which defaults to `id`)
14
- * Bumped gemspec Ruby dependency to >=2.7 (but note, that this is just a little relaxed for older codebase, we're fully building for 3.x)
15
- * Backwards incompatible changes:
16
- * Enforces property names are symbols (before strings were allowed)
17
- * Resource properties, using the `as:` option, are now enforced to be real association names (will not accept other resource names and unroll their dependencies)
18
- * Deprecated the `:through` option for a property. You can just use `as:` with a long, dot-separated association path directly.
19
- * Enhanced ordering semantics for pagination to allow for sorting of deep associated fields:
20
- * Right now, you can sort by fields such as `books.author.name` as one of the sorting components (with `+` or `-` still available)
21
- * Introduced better attribute grouping concepts, that help in defining subgroups of attributes of the same object, and allow lazy loading of only partial subsets so that one can have expensive computations on some of them, but they will never be invoked unless necessary. See MediaType.`group` and Resoruce.`property_group` explanations below.
22
- * Introduced a 'group' stanza in MediaTypes, to specify a structure of attributes that exist in the main object, but that we want to neatly expose as a subset (instead of having them unrolled at the top):
23
- * You can now use things like `group subinfo do ... end` blocks, defining which attributes to group
24
- * Internal: Underneath, the system will create a BlueprintAttributeGroup (instead of a Struct) as a way to ensure that only the individual attributes that need to be rendered, are accessed (and not load the whole struct at once). While the behavior, to the outside, is gonna be identical to a Struct (i.e., exposes attributes as methods), this distinct object implementation is very important as it allows you to have attributes in the subgroup that are expensive to compute, and can be rest assured that they will not be accessed/computed unless they are required for rendering.
25
- * Introduced the `property_group` stanza in resources, to indicate that a property contains a substructure of attributes, each of which must be able to be loaded only when necessary. This commonly goes hand in hand with a `group` stanza in the resource's MediaType:
26
- * Usage of property group requires the name of the substructure (a symbol), and the associated mediatype that contains the definition of the `group` struct, under the same name of the property.
27
- * Internally, this stanza, will define a normal property, and include as dependencies all of the sub attributes read from the MediaType's property, but appending the name (and `_`) to them to avoid collisions.
28
- * Also, it will define a method with the property name which will return a Forwarding object, which will delegate each of the attribute methods back to the original self objects. This allows the object to avoid being 'loaded' as a whole as it happens with Struct, therefore only materializing/calling the attribute that we actually need to use, selectively.
29
- * For example, if we have the `Book` MediaType which has a group atrribute called `subinfo` with a few attributes (like `name` and `pages`), we can use `property_group :subinfo, Book` on its domain object, so that the system will:
30
- * define a `subinfo` property which will depend on `subinfo_name` and `subinfo_pages`
31
- * define a `subinfo` method that will return a Forwarding object, that will forward `name` and `pages` methods to `subinfo_name` and `subinfo_pages` methods of the self resource.
32
- * with that, we just need to define our `subinfo_name` and `subinfo_page` methods in the resource (and also define property dependencies for them if we need to)
21
+
22
+ - A few cleanup and robustness additions:
23
+ - OpenAPI: Disable overriding a description when the schema is a ref (there are known issues with UI browsers)
24
+ - Internal: use `_pk` in batch processor invocation instead of `id` (resources will now have a `_pk` method which defaults to `id`)
25
+ - Bumped gemspec Ruby dependency to >=2.7 (but note, that this is just a little relaxed for older codebase, we're fully building for 3.x)
26
+ - Backwards incompatible changes:
27
+ - Enforces property names are symbols (before strings were allowed)
28
+ - Resource properties, using the `as:` option, are now enforced to be real association names (will not accept other resource names and unroll their dependencies)
29
+ - Deprecated the `:through` option for a property. You can just use `as:` with a long, dot-separated association path directly.
30
+ - Enhanced ordering semantics for pagination to allow for sorting of deep associated fields:
31
+ - Right now, you can sort by fields such as `books.author.name` as one of the sorting components (with `+` or `-` still available)
32
+ - Introduced better attribute grouping concepts, that help in defining subgroups of attributes of the same object, and allow lazy loading of only partial subsets so that one can have expensive computations on some of them, but they will never be invoked unless necessary. See MediaType.`group` and Resoruce.`property_group` explanations below.
33
+ - Introduced a 'group' stanza in MediaTypes, to specify a structure of attributes that exist in the main object, but that we want to neatly expose as a subset (instead of having them unrolled at the top):
34
+ - You can now use things like `group subinfo do ... end` blocks, defining which attributes to group
35
+ - Internal: Underneath, the system will create a BlueprintAttributeGroup (instead of a Struct) as a way to ensure that only the individual attributes that need to be rendered, are accessed (and not load the whole struct at once). While the behavior, to the outside, is gonna be identical to a Struct (i.e., exposes attributes as methods), this distinct object implementation is very important as it allows you to have attributes in the subgroup that are expensive to compute, and can be rest assured that they will not be accessed/computed unless they are required for rendering.
36
+ - Introduced the `property_group` stanza in resources, to indicate that a property contains a substructure of attributes, each of which must be able to be loaded only when necessary. This commonly goes hand in hand with a `group` stanza in the resource's MediaType:
37
+ - Usage of property group requires the name of the substructure (a symbol), and the associated mediatype that contains the definition of the `group` struct, under the same name of the property.
38
+ - Internally, this stanza, will define a normal property, and include as dependencies all of the sub attributes read from the MediaType's property, but appending the name (and `_`) to them to avoid collisions.
39
+ - Also, it will define a method with the property name which will return a Forwarding object, which will delegate each of the attribute methods back to the original self objects. This allows the object to avoid being 'loaded' as a whole as it happens with Struct, therefore only materializing/calling the attribute that we actually need to use, selectively.
40
+ - For example, if we have the `Book` MediaType which has a group atrribute called `subinfo` with a few attributes (like `name` and `pages`), we can use `property_group :subinfo, Book` on its domain object, so that the system will:
41
+ - define a `subinfo` property which will depend on `subinfo_name` and `subinfo_pages`
42
+ - define a `subinfo` method that will return a Forwarding object, that will forward `name` and `pages` methods to `subinfo_name` and `subinfo_pages` methods of the self resource.
43
+ - with that, we just need to define our `subinfo_name` and `subinfo_page` methods in the resource (and also define property dependencies for them if we need to)
33
44
 
34
45
  ## 2.0.pre.29
35
- * Assorted set of fixes to generate cleaner and more compliant OpenApi documents.
36
- * Mostly in the area of multipart generation, and requirements and nullability for OpenApi 3.0
46
+
47
+ - Assorted set of fixes to generate cleaner and more compliant OpenApi documents.
48
+ - Mostly in the area of multipart generation, and requirements and nullability for OpenApi 3.0
49
+
37
50
  ## 2.0.pre.28
38
- * Enhance the mapper's Resource property to allow for a couple more powerful options using the `as:` keyword:
39
- * `as: :self` will provide a way to map any further incoming fields on top of the already existing object. This is useful when we want to expose some properties for a resource, grouped within a sub structure, but that in reality they exist directly in the resource's underlying model (i.e., to organize the information of the model in a more structured/groupable way).
40
- * `as: 'association1.association2'` allows us to traverse more than 1 association, and continue applying the incoming fields under that. This is commonly used when we want to expose a relationship on a resource, which is really coming from more than a single association level depth.
51
+
52
+ - Enhance the mapper's Resource property to allow for a couple more powerful options using the `as:` keyword:
53
+ - `as: :self` will provide a way to map any further incoming fields on top of the already existing object. This is useful when we want to expose some properties for a resource, grouped within a sub structure, but that in reality they exist directly in the resource's underlying model (i.e., to organize the information of the model in a more structured/groupable way).
54
+ - `as: 'association1.association2'` allows us to traverse more than 1 association, and continue applying the incoming fields under that. This is commonly used when we want to expose a relationship on a resource, which is really coming from more than a single association level depth.
55
+
41
56
  ## 2.0.pre.27
42
- * Introduce a new `as:` option for resource's `property`, to indicate that the underlying association method it is connected to, has a different name.
43
- * This also will create a delegation function for the property name, that instead of calling the underlying association on the record, and wrapping the result with a resource instance, it will simply call the aliased method name (which is likely gonna hit the autogenerated code for that properyty, unless we have overriden it)
44
- * With this change, the selector generator (i.e., the thing that looks at the incoming `fields` parameters and calculates which select and includes are necessary to query all the data we need), will be able to understand this aliasing cases, and properly pass along, and continue expanding any nested fields that are under the property name (before this, and further inner fields would be not included as soon as we hit a property that didn't have that direct association underneath).
57
+
58
+ - Introduce a new `as:` option for resource's `property`, to indicate that the underlying association method it is connected to, has a different name.
59
+ - This also will create a delegation function for the property name, that instead of calling the underlying association on the record, and wrapping the result with a resource instance, it will simply call the aliased method name (which is likely gonna hit the autogenerated code for that properyty, unless we have overriden it)
60
+ - With this change, the selector generator (i.e., the thing that looks at the incoming `fields` parameters and calculates which select and includes are necessary to query all the data we need), will be able to understand this aliasing cases, and properly pass along, and continue expanding any nested fields that are under the property name (before this, and further inner fields would be not included as soon as we hit a property that didn't have that direct association underneath).
45
61
 
46
62
  ## 2.0.pre.26
47
- * Make POST action forwarding more robust against technically malformed GET requests with no body but passing `Content-Type`. This could cause issues when using the `enable_large_params_proxy_action` DSL.
63
+
64
+ - Make POST action forwarding more robust against technically malformed GET requests with no body but passing `Content-Type`. This could cause issues when using the `enable_large_params_proxy_action` DSL.
48
65
 
49
66
  ## 2.0.pre.25
50
- * Improve surfacing of requirement attributes in Structs for OpenApi generated documentation
51
- * Introduction of a new dsl `enable_large_params_proxy_action` for GET verb action definitions. When used, two things will happen:
52
- * A new POST verb equivalent action will be defined:
53
- * It will have a `payload` matching the shape of the original GET's params (with the exception of any param that was originally in the URL)
54
- * By default, the route for this new POST request is gonna have the same URL as the original GET action, but appending `/actions/<action_name>` to it. This can be customized by passing the path with the `at:` parameter of the DSL. I.e., `enable_large_params_proxy_action at: /actions/myspecialname` will change the generated path (can use the `//...` syntax to not include the prefix defined for the endpoint). NOTE: this route needs to be compatible with any params that might be defined for the URL (i.e., `:id` and such).
55
- * This action will be fully visible and fully documented in the API generated docs. However, it will not need to have a corresponding controller implementation since it will special-forward it to the original GET action switching the parameters for the payload.
56
- * Specifically, upon receiving a request to the POST equivalent action, Praxis will detect it is a special action and will:
57
- * use directly the original action (i.e., will do the before/after filters and call the controller's method)
58
- * will load the parameters for the action from the incoming payload
59
- * This functionality is to allow having a POST counterpart to any GET requests that require long query strings, and for which the client cannot use a payload bodies (i.e,. Browser JS clients cannot send payload on GET requests).
60
- * Performance improvement:
61
- * Cache praxis associations' computation for ActiveRecord (so no communication with AR or DB happens after that)
62
- * Performance improvement: Use OJ as the (faster) default JSON renderer.
63
- * Introduce batch computation of resource attributes: This defines an optional DSL (`batch_computed`) to enable easier calculation of expensive attributes that can be calculated much more efficiently in group:
64
- * The new DSL takes an attribute name (Symbol), options and an implementation block that is able to get a list of resource instances (a hash of them, indexed by id) and perform the computation for all of them at once.
65
- * Defining an attribute this way, resources can be used to be much more efficiently to calculate values that can be retrieved much more efficiently in bulk, and/or that depend on other resources of the same type to do so (i.e., things that to calculate that attribute for one single resource can be greatly amortized by doing it for many).
66
- * The provided block to calculate the value of the attribute for a collection of resources of the same type is stored as a method inside an inner module of the resource class called BatchProcessors
67
- * The class level method is callable through `::BatchProcessors.<property_name>(rows_by_id: xxx)`. The rows_by_id: parameter has resource 'ids' as keys, and the resource instances themselves a values
68
- * By default an instance method of the same `<property_name>` name will also be created, with a default implementation that will call the `BatchProcessor.<property_name>` with only its instance id and instance, and will return only its result from it.
69
- * If creating the helper instance method is not desired, one can pass `with_instance_method: false` when defining the batched_computed block. This might be necessary if we want to define the method ourselves, or in cases where the resource itself has an 'id' property that is not called 'id' (in which case the implementation would not be correct as it uses the `id` property of the resource). If that's the case, disable the creation, and add your own instance method that uses the defined BatchProcessor method passing the right parameters.
70
- * It is also possible to query which attributes for a resource class are batch computed. This is done through .batched_attributes (which returns and array of symbol names)
71
- * NOTE: Defining batch_computed attributes needs to be done before finalization
67
+
68
+ - Improve surfacing of requirement attributes in Structs for OpenApi generated documentation
69
+ - Introduction of a new dsl `enable_large_params_proxy_action` for GET verb action definitions. When used, two things will happen:
70
+ - A new POST verb equivalent action will be defined:
71
+ - It will have a `payload` matching the shape of the original GET's params (with the exception of any param that was originally in the URL)
72
+ - By default, the route for this new POST request is gonna have the same URL as the original GET action, but appending `/actions/<action_name>` to it. This can be customized by passing the path with the `at:` parameter of the DSL. I.e., `enable_large_params_proxy_action at: /actions/myspecialname` will change the generated path (can use the `//...` syntax to not include the prefix defined for the endpoint). NOTE: this route needs to be compatible with any params that might be defined for the URL (i.e., `:id` and such).
73
+ - This action will be fully visible and fully documented in the API generated docs. However, it will not need to have a corresponding controller implementation since it will special-forward it to the original GET action switching the parameters for the payload.
74
+ - Specifically, upon receiving a request to the POST equivalent action, Praxis will detect it is a special action and will:
75
+ - use directly the original action (i.e., will do the before/after filters and call the controller's method)
76
+ - will load the parameters for the action from the incoming payload
77
+ - This functionality is to allow having a POST counterpart to any GET requests that require long query strings, and for which the client cannot use a payload bodies (i.e,. Browser JS clients cannot send payload on GET requests).
78
+ - Performance improvement:
79
+ - Cache praxis associations' computation for ActiveRecord (so no communication with AR or DB happens after that)
80
+ - Performance improvement: Use OJ as the (faster) default JSON renderer.
81
+ - Introduce batch computation of resource attributes: This defines an optional DSL (`batch_computed`) to enable easier calculation of expensive attributes that can be calculated much more efficiently in group:
82
+ - The new DSL takes an attribute name (Symbol), options and an implementation block that is able to get a list of resource instances (a hash of them, indexed by id) and perform the computation for all of them at once.
83
+ - Defining an attribute this way, resources can be used to be much more efficiently to calculate values that can be retrieved much more efficiently in bulk, and/or that depend on other resources of the same type to do so (i.e., things that to calculate that attribute for one single resource can be greatly amortized by doing it for many).
84
+ - The provided block to calculate the value of the attribute for a collection of resources of the same type is stored as a method inside an inner module of the resource class called BatchProcessors
85
+ - The class level method is callable through `::BatchProcessors.<property_name>(rows_by_id: xxx)`. The rows_by_id: parameter has resource 'ids' as keys, and the resource instances themselves a values
86
+ - By default an instance method of the same `<property_name>` name will also be created, with a default implementation that will call the `BatchProcessor.<property_name>` with only its instance id and instance, and will return only its result from it.
87
+ - If creating the helper instance method is not desired, one can pass `with_instance_method: false` when defining the batched_computed block. This might be necessary if we want to define the method ourselves, or in cases where the resource itself has an 'id' property that is not called 'id' (in which case the implementation would not be correct as it uses the `id` property of the resource). If that's the case, disable the creation, and add your own instance method that uses the defined BatchProcessor method passing the right parameters.
88
+ - It is also possible to query which attributes for a resource class are batch computed. This is done through .batched_attributes (which returns and array of symbol names)
89
+ - NOTE: Defining batch_computed attributes needs to be done before finalization
72
90
 
73
91
  ## 2.0.pre.24
74
- Assorted set of fixes and cleanup:
75
- * better forwarding signature for query methods
76
- * Fix the way with which to decide how to wrap an association (based on Enumerable isn't right, as Hashes are Enumerable as well). Wrapping decision
77
- is now made based on the association type, and not the shape of the resulting type.
78
- * Built handling of some multivalue and/or fuzzy matching cases in filtering params
79
- * unrestrict mustermann's dependent version
80
- * Support options and even passing a full type (instead of a block) in signature definitions (TypedMethods for resources)
92
+
93
+ Assorted set of fixes and cleanup:
94
+
95
+ - better forwarding signature for query methods
96
+ - Fix the way with which to decide how to wrap an association (based on Enumerable isn't right, as Hashes are Enumerable as well). Wrapping decision
97
+ is now made based on the association type, and not the shape of the resulting type.
98
+ - Built handling of some multivalue and/or fuzzy matching cases in filtering params
99
+ - unrestrict mustermann's dependent version
100
+ - Support options and even passing a full type (instead of a block) in signature definitions (TypedMethods for resources)
81
101
 
82
102
  ## 2.0.pre.22
83
- * Small fix in OpenAPI doc generation, which would detect and report more output types, even if they are only defined within the
84
- children of anonymous types.
103
+
104
+ - Small fix in OpenAPI doc generation, which would detect and report more output types, even if they are only defined within the
105
+ children of anonymous types.
85
106
 
86
107
  ## 2.0.pre.22
87
- * Introduced Resource callbacks (an includeable concern). Callbacks allow you to define methods or blocks to be executed `before`, `after` or `around` any existing method in the resource. Class-level callbacks are defined with `self.xxxxx`. These methods will be executed within the instance of the resource (i.e., in the same context of the original) and must be defined with the same parameter signature. For around methods, only blocks can be used, and to call the original (inner) one, one needs to yield.
88
- * Introduced QueryMethods for resources (an includeable concern). QueryMethods expose handy querying methods (`.get`, `.get!`, `.all`, `.first` and `.last` ) which will reach into the underlying ORM (i.e., right now, only ActiveModelCompat is supported) to perform the desired loading of data (and subsequent wrapping of results in resource instances).
89
- * For ActiveRecord `.get` takes a condition hash that will translate to `.find_by`, and `.all` gets a condition hash that will translate to `.where`.
90
- * `.get!` is a `.get` but that will raise a `Praxis::Mapper::ResourceNotFound` exception if nothing was found.
91
- * There is an `.including(<spec>)` function that can be use to preload the underlying associations. I.e., the `<spec>` argument will translate to `.includes(<spec>)` in ActiveRecord.
92
- * Introduced method signatures and validations for resources.
93
- * One can define a method signature with the `signature(<name>)` stanza, passing a block defining Attributor parameters. For instance method signatures, the `<name>` is just a symbol with the name of the method. For class level methods use a string, and prepend `self.` to it (i.e., `self.create`).
94
- * Signatures can only work for methods that either have a single argument (taken as a whole hash), or that have only keyword arguments (i.e., no mixed args and kwargs). It would be basically impossible to validate that combo against an Attributor Struct.
95
- * The calls to typed methods will be intercepted (using an around callback), and the incoming parameters will be validated against the Attributor Struct defined in the siguature, coerced if necessary and passed onto the original method. If the incoming parameters fail validation, a `IncompatibleTypeForMethodArguments` exception will be thrown.
108
+
109
+ - Introduced Resource callbacks (an includeable concern). Callbacks allow you to define methods or blocks to be executed `before`, `after` or `around` any existing method in the resource. Class-level callbacks are defined with `self.xxxxx`. These methods will be executed within the instance of the resource (i.e., in the same context of the original) and must be defined with the same parameter signature. For around methods, only blocks can be used, and to call the original (inner) one, one needs to yield.
110
+ - Introduced QueryMethods for resources (an includeable concern). QueryMethods expose handy querying methods (`.get`, `.get!`, `.all`, `.first` and `.last` ) which will reach into the underlying ORM (i.e., right now, only ActiveModelCompat is supported) to perform the desired loading of data (and subsequent wrapping of results in resource instances).
111
+ - For ActiveRecord `.get` takes a condition hash that will translate to `.find_by`, and `.all` gets a condition hash that will translate to `.where`.
112
+ - `.get!` is a `.get` but that will raise a `Praxis::Mapper::ResourceNotFound` exception if nothing was found.
113
+ - There is an `.including(<spec>)` function that can be use to preload the underlying associations. I.e., the `<spec>` argument will translate to `.includes(<spec>)` in ActiveRecord.
114
+ - Introduced method signatures and validations for resources.
115
+ - One can define a method signature with the `signature(<name>)` stanza, passing a block defining Attributor parameters. For instance method signatures, the `<name>` is just a symbol with the name of the method. For class level methods use a string, and prepend `self.` to it (i.e., `self.create`).
116
+ - Signatures can only work for methods that either have a single argument (taken as a whole hash), or that have only keyword arguments (i.e., no mixed args and kwargs). It would be basically impossible to validate that combo against an Attributor Struct.
117
+ - The calls to typed methods will be intercepted (using an around callback), and the incoming parameters will be validated against the Attributor Struct defined in the siguature, coerced if necessary and passed onto the original method. If the incoming parameters fail validation, a `IncompatibleTypeForMethodArguments` exception will be thrown.
96
118
 
97
119
  ## 2.0.pre.21
98
- * Fix nullable attribute in OpenApi generation
120
+
121
+ - Fix nullable attribute in OpenApi generation
122
+
99
123
  ## 2.0.pre.20
100
- * Changed the behavior of dev-mode when validate_responses. Now they return a 500 status code (instead of a 400) but with the same validation error format body.
101
- * validate_responses is meant to catch the application returning non-compliant responses for development only. As such, a 500 is much more appropriate and clear, as the validation is done on the behavior of the server, and not on the information sent by the client (i.e., it is a server problem, not reacting the way the API is defined)
102
- * Introduced a method to reload a Resouce (.reload), which will clear the memoized values and call record.reload as well
103
- * Open API Generation enhancements:
104
- * Fixed type discovery (where some types wouldn't be included in the output)
105
- * Changed the generation to output named types into components, and use `$ref` to point to them whenever appropriate
106
- * Report nullable attributes
124
+
125
+ - Changed the behavior of dev-mode when validate_responses. Now they return a 500 status code (instead of a 400) but with the same validation error format body.
126
+ - validate_responses is meant to catch the application returning non-compliant responses for development only. As such, a 500 is much more appropriate and clear, as the validation is done on the behavior of the server, and not on the information sent by the client (i.e., it is a server problem, not reacting the way the API is defined)
127
+ - Introduced a method to reload a Resouce (.reload), which will clear the memoized values and call record.reload as well
128
+ - Open API Generation enhancements:
129
+ - Fixed type discovery (where some types wouldn't be included in the output)
130
+ - Changed the generation to output named types into components, and use `$ref` to point to them whenever appropriate
131
+ - Report nullable attributes
132
+
107
133
  ## 2.0.pre.19
108
- * Introduced a new DSL for the `FilteringParams` type that allows filters for common attributes in your Media Types:
109
- * The new `any` DSL allows you to define which final leaf attribute to always allow, and with which operators and/or fuzzy restrictions.
110
- * For example, you can add `any updated_at, using: ['>','<']` which would allow the type to accept filters like `updated_at>2000-01-01`, or any existing nested fields like `posts.comments.updated_at>2000-01-01`
111
- * Note that the path of attributes passed in, will still need to exist and will be validated. Also, you still need to make sure that you have the right `filters_mapping` defined in your resources.
112
- * Changed `filters_mapping` to allow implicitly any filter path that is a valid representation of existing columns and associations. I.e., you do not have to explicitly define long nested filters that correspond to the same underlying path of associations and columns.
134
+
135
+ - Introduced a new DSL for the `FilteringParams` type that allows filters for common attributes in your Media Types:
136
+ - The new `any` DSL allows you to define which final leaf attribute to always allow, and with which operators and/or fuzzy restrictions.
137
+ - For example, you can add `any updated_at, using: ['>','<']` which would allow the type to accept filters like `updated_at>2000-01-01`, or any existing nested fields like `posts.comments.updated_at>2000-01-01`
138
+ - Note that the path of attributes passed in, will still need to exist and will be validated. Also, you still need to make sure that you have the right `filters_mapping` defined in your resources.
139
+ - Changed `filters_mapping` to allow implicitly any filter path that is a valid representation of existing columns and associations. I.e., you do not have to explicitly define long nested filters that correspond to the same underlying path of associations and columns.
140
+
113
141
  ## 2.0.pre.18
114
- * Upgraded to newest Attributor, which cleans up the required: true semantics to only work on keys, and introduces null: true for nullability of values (independent from presence of keys or not)
115
- * Fixed a selector generator bug that would occur when using deep nested resource dependencies as strings 'foo.bar.baz.bam'. In this cases only partial tracking of relationships would be built, which could cause to not fully eager load DB queries.
142
+
143
+ - Upgraded to newest Attributor, which cleans up the required: true semantics to only work on keys, and introduces null: true for nullability of values (independent from presence of keys or not)
144
+ - Fixed a selector generator bug that would occur when using deep nested resource dependencies as strings 'foo.bar.baz.bam'. In this cases only partial tracking of relationships would be built, which could cause to not fully eager load DB queries.
145
+
116
146
  ## 2.0.pre.17
117
- * Changed the Parameter Filtering to use left outer joins (and extra conditions), to allow for the proper results when OR clauses are involved in certain configurations.
118
- * Built support for allowing filtering directly on associations using `!` and `!!` operators. This allows to filter results where
119
- there are no associated rows (`!!`) or if there are some associated rows (`!`)
120
- * Allow implicit definition of `filters_mapping` for filter names that match top-level associations of the model (i.e., like we do for the columns)
147
+
148
+ - Changed the Parameter Filtering to use left outer joins (and extra conditions), to allow for the proper results when OR clauses are involved in certain configurations.
149
+ - Built support for allowing filtering directly on associations using `!` and `!!` operators. This allows to filter results where
150
+ there are no associated rows (`!!`) or if there are some associated rows (`!`)
151
+ - Allow implicit definition of `filters_mapping` for filter names that match top-level associations of the model (i.e., like we do for the columns)
152
+
121
153
  ## 2.0.pre.16
122
154
 
123
- * Updated `Resource.property` signature to only accept known named arguments (`dependencies` and `though` at this time) to spare anyone else from going insane wondering why their `depednencies` aren't working.
124
- * Fixed issue with Filtering Params, that occurred with using the ! or !! operators on String-typed fields.
155
+ - Updated `Resource.property` signature to only accept known named arguments (`dependencies` and `though` at this time) to spare anyone else from going insane wondering why their `depednencies` aren't working.
156
+ - Fixed issue with Filtering Params, that occurred with using the ! or !! operators on String-typed fields.
125
157
 
126
158
  ## 2.0.pre.14
127
159
 
128
- * More encoding/decoding robustness for filters.
129
- * Specs for how to encode filters are now properly defined by:
130
- * The "value" of the filters query string needs to be URI encoded (like any other query string value). This encoding is subject to the normal rules, and therefore "could" leave some of the URI unreserved characters (i.e., 'markers') unencoded depending on the client (Section 2.2 of https://tools.ietf.org/html/rfc2396).
131
- * The "values" for any of the conditions in the contents of the filters, however, will need to be properly "escaped" as well (prior to URL-encoding the whole syntax string itself like described above). This means that any match value needs to ensure that it has (at least) "(",")","|","&" and "," escaped as they are reserved characters for the filter expression syntax. For example, if I want to search for a name with value "Rocket&(Pants)", I need to first compose the syntax by: "name=<escaped Rocket&(Pants)>, which is "name=Rocket%26%28Pants%29" and then, just URI encode that query string value for the filters parameter in the URL like any other. For example: "filters=name%3DRocket%2526%2528Pants%2529"
132
- * When using a multi-match (csv-separated) list of values, you need to escape each of the values as well, leaving the 'comma' unescape, as that's part of the syntax. Then uri-encode it all for the filters query string parameter value like above.
133
- * Now, one can properly differentiate between fuzzy query prefix/postfix, and the literal data to search for (which can be or include '*'). Report that multi-matches (i.e., csv separated values for a single field, which translate into "IN" clauses) is not allowed if fuzzy matches are received (need to use multiple OR clauses for it).
160
+ - More encoding/decoding robustness for filters.
161
+ - Specs for how to encode filters are now properly defined by:
162
+ - The "value" of the filters query string needs to be URI encoded (like any other query string value). This encoding is subject to the normal rules, and therefore "could" leave some of the URI unreserved characters (i.e., 'markers') unencoded depending on the client (Section 2.2 of https://tools.ietf.org/html/rfc2396).
163
+ - The "values" for any of the conditions in the contents of the filters, however, will need to be properly "escaped" as well (prior to URL-encoding the whole syntax string itself like described above). This means that any match value needs to ensure that it has (at least) "(",")","|","&" and "," escaped as they are reserved characters for the filter expression syntax. For example, if I want to search for a name with value "Rocket&(Pants)", I need to first compose the syntax by: "name=<escaped Rocket&(Pants)>, which is "name=Rocket%26%28Pants%29" and then, just URI encode that query string value for the filters parameter in the URL like any other. For example: "filters=name%3DRocket%2526%2528Pants%2529"
164
+ - When using a multi-match (csv-separated) list of values, you need to escape each of the values as well, leaving the 'comma' unescape, as that's part of the syntax. Then uri-encode it all for the filters query string parameter value like above.
165
+ - Now, one can properly differentiate between fuzzy query prefix/postfix, and the literal data to search for (which can be or include '\*'). Report that multi-matches (i.e., csv separated values for a single field, which translate into "IN" clauses) is not allowed if fuzzy matches are received (need to use multiple OR clauses for it).
134
166
 
135
167
  ## 2.0.pre.13
136
168
 
137
- * Fix filters parser regression, which would incorrectly decode url-encoded values
169
+ - Fix filters parser regression, which would incorrectly decode url-encoded values
138
170
 
139
171
  ## 2.0.pre.12
140
172
 
141
- * Rebuilt API filters to support a much richer syntax. One can now use ANDs and ORs (with ANDs having order precedence), as well as group them with parenthesis. The same individual filter operands are supported. For example: 'email=*@gmail.com&(friends.first_name=Joe*,Patty|friends.last_name=Smith)
173
+ - Rebuilt API filters to support a much richer syntax. One can now use ANDs and ORs (with ANDs having order precedence), as well as group them with parenthesis. The same individual filter operands are supported. For example: 'email=_@gmail.com&(friends.first_name=Joe_,Patty|friends.last_name=Smith)
142
174
 
143
175
  ## 2.0.pre.11
144
176
 
@@ -152,7 +184,7 @@ there are no associated rows (`!!`) or if there are some associated rows (`!`)
152
184
  - Simple, but pervasive breaking change: Rename `ResourceDefinition` to `EndpointDefinition` (but same functionality).
153
185
  - Remove all deprecated features (and raise error describing it's not supported yet)
154
186
  - Remove `Links` and `LinkBuilder`. Those seem unnecessary from a Framework point of view as they aren't clear most
155
- applications would benefit from it. Applications can choose to add that functionality on their own if so desire.
187
+ applications would benefit from it. Applications can choose to add that functionality on their own if so desire.
156
188
  - Rebuilt app generators: for new empty app, and example app.
157
189
  - Updated default layout to match new naming structure and more concepts commonly necessary for normal applications.
158
190
  - Completely removed the native Praxis API documentation browser in lieu of OpenAPI 3.x standards, and reDoc.
@@ -190,6 +222,7 @@ applications would benefit from it. Applications can choose to add that function
190
222
  - Added support for OpenAPI 3.x document generation. Consider this in Beta state, although it is fairly close to feature complete.
191
223
 
192
224
  ## 2.0.pre.4
225
+
193
226
  - Reworked the field selection DB query generation to support full tree of eager loaded dependencies
194
227
  - Built support for both ActiveRecord and Sequel gems
195
228
  - Selected DB fields will include/map the defined resource properties and will always include any necessary fields on both sides of the joins for the given associations.
data/bin/praxis CHANGED
@@ -47,6 +47,8 @@ path_to_loader = format('%<path>s/tasks/loader.thor', path: path_to_praxis)
47
47
  load path_to_loader
48
48
 
49
49
  class PraxisGenerator < Thor
50
+ SCAFFOLD_CONFIG_FILE = "#{Dir.pwd}/.praxis_scaffold"
51
+
50
52
  # Include a few fake thor action descriptions (for the rake tasks above) so they can show up in the same usage messages
51
53
  desc 'routes [json]', 'Prints the route table of the application. Defaults to table format, but can produce json'
52
54
  def routes; end
@@ -91,7 +93,9 @@ class PraxisGenerator < Thor
91
93
  # Cannot use the argument below or it will apply to all commands (the action in the class has it)
92
94
  # argument :collection_name, required: false
93
95
  # The options, however, since they're optional are fine (But need to be duplicated from the class :( )
94
- option :version, required: false, default: '1',
96
+ option :base, required: false,
97
+ desc: 'Module name to enclose all generated files. Empty by default. You can pass things like MyApp, or MyApp::SubModule'
98
+ option :version, required: false,
95
99
  desc: 'Version string for the API endpoint. This also dictates the directory structure (i.e., v1/endpoints/...))'
96
100
  option :design, type: :boolean, default: true,
97
101
  desc: 'Include the Endpoint and MediaType files for the collection'
@@ -120,6 +124,25 @@ class PraxisGenerator < Thor
120
124
  opts = { orm: options[:model] }
121
125
  opts[:orm] = 'activerecord' if opts[:orm] == 'model' # value is model param passed by no value
122
126
  ::PraxisGen::Model.new([collection_name.singularize], opts).invoke(:g)
127
+ self.class.save_scaffolding_config
128
+ end
129
+
130
+ # Read and pass the hash around in the class, so callers can read and modify it if desired
131
+ # Final contents will be saved at the end of scaffolding generation
132
+ def self.scaffold_config
133
+ return @current_config if @current_config
134
+
135
+ @current_config = File.exist?(SCAFFOLD_CONFIG_FILE) ? JSON.parse(File.read(SCAFFOLD_CONFIG_FILE), symbolize_names: true) : {}
136
+ end
137
+
138
+ def self.save_scaffolding_config
139
+ if File.exist?(SCAFFOLD_CONFIG_FILE)
140
+ contents_from_file = JSON.parse(File.read(SCAFFOLD_CONFIG_FILE), symbolize_names: true)
141
+ return if contents_from_file == @current_config
142
+ end
143
+
144
+ puts "Saving new scaffolding config into #{SCAFFOLD_CONFIG_FILE}"
145
+ File.write(SCAFFOLD_CONFIG_FILE, JSON.pretty_generate(scaffold_config))
123
146
  end
124
147
 
125
148
  # Initially, the idea was to build some quick model generator, but I think it's better to keep it
@@ -40,7 +40,7 @@ module Praxis
40
40
  end
41
41
 
42
42
  def inspect
43
- "<#{self.class}##{object_id} root: #{@root}>"
43
+ "#<#{self.class}##{object_id} @root=#{@root}>"
44
44
  end
45
45
 
46
46
  def setup(root: '.')
@@ -333,23 +333,31 @@ module Praxis
333
333
  errors = []
334
334
  keys_provided = []
335
335
 
336
- self.class.attributes.each do |key, attribute|
337
- sub_context = self.class.generate_subcontext(context, key)
338
- value = _get_attr(key)
339
- keys_provided << key if @object.key?(key)
336
+ keys_provided = object.contents.keys
340
337
 
341
- next if value.respond_to?(:validating) && value.validating # really, it's a thing with sub-attributes
338
+ keys_provided.each do |key|
339
+ sub_context = self.class.generate_subcontext(context, key)
340
+ attribute = self.class.attributes[key]
342
341
 
343
- # Isn't this handled by the requirements validation? NO! we might want to combine
344
- errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is required."] if attribute.options[:required] && !@object.key?(key)
345
- if @object[key].nil?
346
- errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is not nullable."] if !Attributor::Attribute.nullable_attribute?(attribute.options) && @object.key?(key) # It is only nullable if there's an explicite null: true (undefined defaults to false)
342
+ if object.contents[key].nil?
343
+ errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is not nullable."] if !Attributor::Attribute.nullable_attribute?(attribute.options) && object.contents.key?(key) # It is only nullable if there's an explicite null: true (undefined defaults to false)
347
344
  # No need to validate the attribute further if the key wasn't passed...(or we would get nullable errors etc..cause the attribute has no
348
345
  # context if its containing key was even passed (and there might not be a containing key for a top level attribute anyways))
349
346
  else
347
+ value = _get_attr(key)
348
+ next if value.respond_to?(:validating) && value.validating # really, it's a thing with sub-attributes
349
+
350
350
  errors.concat attribute.validate(value, sub_context)
351
351
  end
352
352
  end
353
+
354
+ leftover = self.class.attributes.keys - keys_provided
355
+ leftover.each do |key|
356
+ attribute = self.class.attributes[key]
357
+
358
+ errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is required."] if attribute.options[:required]
359
+ end
360
+
353
361
  self.class.attribute.type.requirements.each do |requirement|
354
362
  validation_errors = requirement.validate(keys_provided, context)
355
363
  errors.concat(validation_errors) unless validation_errors.empty?
@@ -35,6 +35,10 @@ module Praxis
35
35
  @response = response
36
36
  end
37
37
 
38
+ def inspect
39
+ "#<#{self.class}##{object_id} @request=#{@request.inspect}>"
40
+ end
41
+
38
42
  def definition
39
43
  self.class.definition
40
44
  end
@@ -31,6 +31,15 @@ module Praxis
31
31
  # security: [{}]
32
32
  # servers: [{}]
33
33
  }
34
+
35
+ # Handle versioning header/params for the action in a special way, by linking to the existing component
36
+ # spec that will be generated globally
37
+ api_info = ApiDefinition.instance.infos[action.endpoint_definition.version]
38
+ if (version_with = api_info.version_with)
39
+ all_parameters.push('$ref' => '#/components/parameters/ApiVersionHeader') if version_with.include?(:header)
40
+ all_parameters.push('$ref' => '#/components/parameters/ApiVersionParam') if version_with.include?(:params)
41
+ end
42
+
34
43
  h[:description] = action.description if action.description
35
44
  h[:tags] = all_tags.uniq unless all_tags.empty?
36
45
  h[:parameters] = all_parameters unless all_parameters.empty?
@@ -32,14 +32,14 @@ module Praxis
32
32
  id = resource.id
33
33
  # fill in the paths hash with a key for each path for each action/route
34
34
  resource.actions.each do |action_name, action|
35
- params_example = action.params ? action.params.example(nil) : nil
35
+ params_example = action.params ? action.params.example(nil) : nil
36
36
  url = ActionDefinition.url_description(route: action.route, params: action.params, params_example: params_example)
37
37
 
38
38
  verb = url[:verb].downcase
39
39
  templetized_path = OpenApiGenerator.templatize_url(url[:path])
40
40
  path_entry = paths[templetized_path]
41
41
  # Let's fill in verb stuff within the working hash
42
- raise "VERB #{_verb} already defined for #{id}!?!?!" if path_entry[verb]
42
+ raise "VERB #{verb} already defined for #{id}!?!?!" if path_entry[verb]
43
43
 
44
44
  action_uid = "action-#{action_name}-#{id}"
45
45
  # Add a tag matching the resource name (hoping all actions of a resource are grouped)
@@ -109,7 +109,15 @@ module Praxis
109
109
 
110
110
  info_object = OpenApi::InfoObject.new(version: version, api_definition_info: @infos[version])
111
111
  # We only support a server in Praxis ... so we'll use the base path
112
- server_object = OpenApi::ServerObject.new(url: @infos[version].base_path)
112
+ server_params = {}
113
+ if(server_info = @infos[version].server)
114
+ server_params[:url] = server_info[:url]
115
+ server_params[:variables] = server_info[:variables] if server_info[:variables]
116
+ else
117
+ server_params[:url] = @infos[version].base_path
118
+ end
119
+ server_params[:description] = server_info[:description] if server_info[:description]
120
+ server_object = OpenApi::ServerObject.new(**server_params)
113
121
 
114
122
  paths_object = OpenApi::PathsObject.new(resources: resources_by_version[version])
115
123
 
@@ -151,6 +159,28 @@ module Praxis
151
159
  schemas: component_schemas
152
160
  }
153
161
 
162
+ # Common params/headers for versioning (actions will link to them when appropriate, by name)
163
+ if (version_with = @infos[version].version_with)
164
+ common_params = {}
165
+ if version_with.include?(:header)
166
+ common_params['ApiVersionHeader'] = {
167
+ in: 'header',
168
+ name: 'X-Api-Version',
169
+ schema: { type: 'string', enum: [version]},
170
+ required: version_with.size == 1
171
+ }
172
+ end
173
+ if version_with.include?(:params)
174
+ common_params['ApiVersionParam'] = {
175
+ in: :query,
176
+ name: 'api_version',
177
+ schema: { type: 'string', enum: [version]},
178
+ required: version_with.size == 1
179
+ }
180
+ end
181
+ full_data[:components][:parameters] = common_params
182
+ end
183
+
154
184
  # REDOC specific grouping of sidebar
155
185
  resource_tags = { name: 'Resources', tags: tags_for_resources.map { |t| t[:name] } }
156
186
  schema_tags = { name: 'Models', tags: tags_for_mts.map { |t| t[:name] } }
@@ -170,26 +200,26 @@ module Praxis
170
200
  converted_full_data = JSON.parse(json_data) # So symbols disappear
171
201
  File.open("#{filename}.yml", 'w') { |f| f.write(YAML.dump(converted_full_data)) }
172
202
 
173
- html = <<-HTML
174
- <!doctype html>
175
- <html lang="en">
176
- <head>
177
- <meta charset="utf-8">
178
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
179
- <title>Elements in HTML</title>
180
-
181
- <script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
182
- <link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
183
- </head>
184
- <body>
185
-
186
- <elements-api
187
- apiDescriptionUrl="http://localhost:9090/#{version_file}/openapi.json"
188
- router="hash"
189
- />
190
-
191
- </body>
192
- </html>
203
+ html = <<~HTML
204
+ <!doctype html>
205
+ <html lang="en">
206
+ <head>
207
+ <meta charset="utf-8">
208
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
209
+ <title>Elements in HTML</title>
210
+ #{' '}
211
+ <script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
212
+ <link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
213
+ </head>
214
+ <body>
215
+
216
+ <elements-api
217
+ apiDescriptionUrl="http://localhost:9090/#{version_file}/openapi.json"
218
+ router="hash"
219
+ />
220
+
221
+ </body>
222
+ </html>
193
223
  HTML
194
224
  html_file = File.join(doc_root_dir, version_file, 'index.html')
195
225
  File.write(html_file, html)
@@ -68,7 +68,7 @@ module Praxis
68
68
  end
69
69
 
70
70
  # just include the full thing if it has no attributes
71
- return true if object.attributes.empty?
71
+ return fields if object.attributes.empty?
72
72
 
73
73
  # True, expands to the default fieldset for blueprints
74
74
  fields = object.default_fieldset if object < Praxis::Blueprint && fields == true
@@ -321,7 +321,7 @@ module Praxis
321
321
  end
322
322
 
323
323
  def inspect
324
- "<#{self.class}# @root=#{@root.inspect}>"
324
+ "#<#{self.class} @resource=#{@resource.name.inspect} @select=#{@select.inspect} @select_star=#{@select_star.inspect} @tracking.keys=#{@tracks.keys} (recursion omitted)>"
325
325
  end
326
326
  end
327
327
 
@@ -184,7 +184,7 @@ module Praxis
184
184
  # Override the inspect instance method of a request, as, by default, the kernel inspect will go nuts
185
185
  # traversing the action and app_instance and therefore all associated instance variables reachable through that
186
186
  def inspect
187
- "'@env' => #{@env.inspect},\n'@headers' => #{@headers.inspect},\n'@params' => #{@params.inspect},\n'@query' => #{@query.inspect}"
187
+ "#<#{self.class}##{object_id} @action=#{@action.inspect} @params=#{@params.inspect}>"
188
188
  end
189
189
  end
190
190
  end
@@ -27,6 +27,9 @@ namespace :praxis do
27
27
  PROMPT_C: "%N(#{nickname}):%03n:%i* "
28
28
  }
29
29
 
30
+ # Disable inefficient, distracting autocomplete
31
+ IRB.conf[:USE_AUTOCOMPLETE] = false
32
+
30
33
  # Set the IRB main object.
31
34
  IRB.irb(nil, Praxis::Application.instance)
32
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Praxis
4
- VERSION = '2.0.pre.31'
4
+ VERSION = '2.0.pre.32'
5
5
  end
@@ -97,6 +97,17 @@ describe Praxis::Application do
97
97
  end
98
98
  end
99
99
 
100
+ describe '#inspect' do
101
+ let(:klass) { Class.new(Praxis::Application) }
102
+ subject { klass.instance }
103
+
104
+ it 'includes name, object ID and root' do
105
+ SomeApplication = klass # de-anonymize class name
106
+ klass.instance.instance_variable_set(:@root, '/tmp')
107
+ expect(subject.inspect).to match(%r{#<SomeApplication#[0-9]+ @root=/tmp>})
108
+ end
109
+ end
110
+
100
111
  describe '#setup' do
101
112
  subject { Class.new(Praxis::Application).instance }
102
113
 
@@ -166,6 +166,12 @@ describe Praxis::Blueprint do
166
166
  it { should be_empty }
167
167
  end
168
168
 
169
+ context 'with a valid nested blueprint' do
170
+ let(:hash) { { name: 'bob', myself: { name: 'PseudoBob'}} }
171
+
172
+ it { should be_empty }
173
+ end
174
+
169
175
  context 'with invalid sub-attribute' do
170
176
  let(:hash) { { name: 'bob', address: { state: 'ME' } } }
171
177
 
@@ -173,6 +179,15 @@ describe Praxis::Blueprint do
173
179
  its(:first) { should =~ /Attribute \$.address.state/ }
174
180
  end
175
181
 
182
+ context 'with an invalid nested blueprint' do
183
+ let(:hash) { { name: 'bob', myself: { name: 'PseudoBob', address: { state: 'ME' }}} }
184
+
185
+ it { should have(1).item }
186
+ its(:first) { should =~ /Attribute \$.myself.address.state/ }
187
+
188
+ end
189
+
190
+
176
191
  context 'for objects of the wrong type' do
177
192
  it 'raises an error' do
178
193
  expect do
@@ -31,4 +31,13 @@ describe Praxis::Controller do
31
31
  expect(subject).to eq(PeopleResource.controller)
32
32
  end
33
33
  end
34
+
35
+ describe '#inspect' do
36
+ it 'includes name, object ID and request' do
37
+ SomeController = subject # de-anonymize class name
38
+ expect(subject.new('eioio').inspect).to match(
39
+ /#<SomeController#[0-9]+ @request="eioio">/
40
+ )
41
+ end
42
+ end
34
43
  end
@@ -146,6 +146,16 @@ describe Praxis::Request do
146
146
  end
147
147
  end
148
148
 
149
+ context '#inspect' do
150
+ it 'includes action and params' do
151
+ request.action = 'eioio'
152
+ request.params = 'zzyzx'
153
+ expect(request.inspect).to match(
154
+ /#<Praxis::Request#[0-9]+ @action="eioio" @params="zzyzx">/
155
+ )
156
+ end
157
+ end
158
+
149
159
  context '#load_headers' do
150
160
  it 'is done preserving the original case' do
151
161
  request.load_headers(context[:headers])
data/tasks/thor/model.rb CHANGED
@@ -13,6 +13,8 @@ module PraxisGen
13
13
  argument :model_name, required: true
14
14
  option :orm, required: false, default: 'activerecord', enum: %w[activerecord sequel]
15
15
  def g
16
+ models_dir = 'app/models'
17
+ models_dir = PraxisGenerator.scaffold_config[:models_dir] if PraxisGenerator.scaffold_config[:models_dir]
16
18
  # self.class.check_name(model_name)
17
19
  template_file = \
18
20
  if options[:orm] == 'activerecord'
@@ -21,7 +23,7 @@ module PraxisGen
21
23
  'models/sequel.rb'
22
24
  end
23
25
  puts "Generating Model for #{model_name}"
24
- template template_file, "app/models/#{model_name}.rb"
26
+ template template_file, "#{models_dir}/#{model_name}.rb"
25
27
  nil
26
28
  end
27
29
  # Helper functions (which are available in the ERB contexts)
@@ -13,7 +13,9 @@ module PraxisGen
13
13
 
14
14
  desc 'g', 'Generates an API design and implementation scaffold for managing a collection of <collection_name>'
15
15
  argument :collection_name, required: true
16
- option :version, required: false, default: '1',
16
+ option :base, required: false,
17
+ desc: 'Module name to enclose all generated files. Empty by default. You can pass things like MyApp, or MyApp::SubModule'
18
+ option :version, required: false,
17
19
  desc: 'Version string for the API endpoint. This also dictates the directory structure (i.e., v1/endpoints/...))'
18
20
  option :design, type: :boolean, default: true,
19
21
  desc: 'Include the Endpoint and MediaType files for the collection'
@@ -26,6 +28,11 @@ module PraxisGen
26
28
  option :actions, type: :string, default: 'crud', enum: %w[cr cru crud u ud d],
27
29
  desc: 'Specifies the actions to generate for the API. cr=create, u=update, d=delete. Index and show actions are always generated'
28
30
  def g
31
+ incorporate_config_options
32
+ # Good defaults
33
+ options[:version] = '1' unless options[:version].presence
34
+ options[:models_dir] = 'app/models' unless options[:models_dir]
35
+
29
36
  self.class.check_name(collection_name)
30
37
  @actions_hash = self.class.compose_actions_hash(options[:actions])
31
38
  env_rb = Pathname.new(destination_root) + Pathname.new('config/environment.rb')
@@ -53,10 +60,31 @@ module PraxisGen
53
60
  template 'implementation/controllers/collection.rb', "app/#{version_dir}/controllers/#{collection_name}.rb"
54
61
  end
55
62
  nil
63
+ save_last_config_options
56
64
  end
57
65
 
58
66
  # Helper functions (which are available in the ERB contexts)
59
67
  no_commands do
68
+ def incorporate_config_options
69
+ @saved_original_options = options
70
+ self.options = options.dup
71
+ return if PraxisGenerator.scaffold_config.empty?
72
+
73
+ begin
74
+ options[:base] = PraxisGenerator.scaffold_config[:base] unless PraxisGenerator.scaffold_config[:base].presence
75
+ options[:version] = PraxisGenerator.scaffold_config[:version] unless PraxisGenerator.scaffold_config[:version].presence
76
+ options[:models_dir] = PraxisGenerator.scaffold_config[:models_dir] if PraxisGenerator.scaffold_config[:models_dir]
77
+ rescue StandardError # rubocop:disable Lint/SuppressedException
78
+ end
79
+ end
80
+
81
+ def save_last_config_options
82
+ return if @saved_original_options.slice('base', 'version').empty?
83
+
84
+ opts_to_save = options.slice('base', 'version').transform_keys(&:to_sym).reject { |_k, v| v.nil? }
85
+ PraxisGenerator.scaffold_config.merge!(opts_to_save)
86
+ end
87
+
60
88
  def plural_class
61
89
  collection_name.camelize
62
90
  end
@@ -65,16 +93,20 @@ module PraxisGen
65
93
  collection_name.singularize.camelize
66
94
  end
67
95
 
96
+ def base_module
97
+ options[:base]
98
+ end
99
+
68
100
  def version
69
101
  options[:version]
70
102
  end
71
103
 
72
104
  def version_module
73
- "V#{version}"
105
+ base_module.presence ? "#{base_module}::V#{version}" : "V#{version}"
74
106
  end
75
107
 
76
108
  def version_dir
77
- version_module.camelize(:lower)
109
+ "v#{version}"
78
110
  end
79
111
 
80
112
  def action_enabled?(action)
@@ -77,6 +77,7 @@ module <%= version_module %>
77
77
  # attribute :name
78
78
  end
79
79
  response :no_content
80
+ response :not_found
80
81
  response :bad_request
81
82
  end
82
83
  <%- end -%>
@@ -5,7 +5,7 @@ module <%= version_module %>
5
5
  class <%= singular_class %> < Praxis::MediaType
6
6
  identifier 'application/json'
7
7
 
8
- domain_model '<%= version_module %>::Resources::<%= singular_class %>'
8
+ domain_model 'Resources::<%= singular_class %>'
9
9
  description 'Structural definition of a <%= singular_class %>'
10
10
 
11
11
  attributes do
@@ -11,7 +11,7 @@ module <%= version_module %>
11
11
  # Retrieve all <%= plural_class %> with the right necessary associations
12
12
  # and render them appropriately with the requested field selection
13
13
  def index
14
- objects = build_query(model_class).all
14
+ objects = build_query(model_class)
15
15
  display(objects)
16
16
  end
17
17
  <%- end -%>
@@ -42,14 +42,12 @@ module <%= version_module %>
42
42
  <%- if action_enabled?(:update) -%>
43
43
  # Updates some of the information of a <%= singular_class %>
44
44
  def update(id:)
45
- # A good pattern is to call the same name method on the corresponding resource,
46
- # passing the incoming id and payload (or massaging it first)
47
- updated_resource = Resources::<%= singular_class %>.update(
48
- id: id,
49
- payload: request.payload,
50
- )
51
- return Praxis::Responses::NotFound.new unless updated_resource
45
+ # A good pattern is to retrieve the resource instance by id, and then
46
+ # call the same name method on it, by passing the incoming payload (or massaging it first)
47
+ resource = Resources::<%= singular_class %>.get(id: id)
48
+ return Praxis::Responses::NotFound.new unless resource
52
49
 
50
+ resource.update(payload: request.payload)
53
51
  Praxis::Responses::NoContent.new
54
52
  end
55
53
  <%- end -%>
@@ -57,13 +55,12 @@ module <%= version_module %>
57
55
  <%- if action_enabled?(:delete) -%>
58
56
  # Deletes an existing <%= singular_class %>
59
57
  def delete(id:)
60
- # A good pattern is to call the same name method on the corresponding resource,
61
- # maybe passing the already loaded model
62
- deleted_resource = Resources::<%= singular_class %>.delete(
63
- id: id
64
- )
65
- return Praxis::Responses::NotFound.new unless deleted_resource
58
+ # A good pattern is to retrieve the resource instance by id, and then
59
+ # call the same name method on it
60
+ resource = Resources::<%= singular_class %>.get(id: id)
61
+ return Praxis::Responses::NotFound.new unless resource
66
62
 
63
+ resource.delete(payload: request.payload)
67
64
  Praxis::Responses::NoContent.new
68
65
  end
69
66
  <%- end -%>
@@ -22,21 +22,17 @@ module <%= version_module %>
22
22
  <%- end -%>
23
23
 
24
24
  <%- if action_enabled?(:update) -%>
25
- def self.update(id:, payload:)
26
- record = model.find_by(id: id)
27
- return nil unless record
25
+ def update(payload:)
28
26
  # Assuming the API field names directly map the the model attributes. Massage if appropriate.
29
27
  record.update(**payload.to_h)
30
- self.new(record)
28
+ self
31
29
  end
32
30
  <%- end -%>
33
31
 
34
32
  <%- if action_enabled?(:delete) -%>
35
33
  def self.delete(id:)
36
- record = model.find_by(id: id)
37
- return nil unless record
38
34
  record.destroy
39
- self.new(record)
35
+ self
40
36
  end
41
37
  <%- end -%>
42
38
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: praxis
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.pre.31
4
+ version: 2.0.pre.32
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep M. Blanquer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-03-07 00:00:00.000000000 Z
12
+ date: 2023-05-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport