pinterest / plank Goto Github PK
View Code? Open in Web Editor NEWA tool for generating immutable model objects
Home Page: https://pinterest.github.io/plank/
License: Apache License 2.0
A tool for generating immutable model objects
Home Page: https://pinterest.github.io/plank/
License: Apache License 2.0
I think we should supporting GraphQL’s schema representation in Plank. Four big reasons:
@deprecated
, etc.Now that #67 landed we should be able to bridge between Objc and Flow. We should add integration tests that test this bridging and if we add more languages in the future extend this tests.
We should either assert or dedupe references to the same $ref in a oneOf declaration. Currently we generate code with duplicate symbols and is not developer friendly.
I had issues with a few variable names that are reserved, I know that plank handle a few of them (id, description), but not all. I had issues with "class" and "continue".
Is there anything that I can do to avoid this problem?
We added support for ObjC reserved keywords in this PR (#79) but we should generalize this for all languages since the purpose is agnostic
We should revise the copyright section in the LICENSE
file as it's currently says the following: Copyright {yyyy} {name of copyright owner}
The Model and ADT classes need to share a lot of logic for things like hash
and isEqual
. Rather than duplicating this code we should be able to share a lot of the logic using protocols and protocol extensions.
Here is the plan:
ObjCRootsRender
to extend off the protocol itself.Currently we assume that overwrite
is the write strategy for merging maps, lists, etc. Based on the application this assumption can be incorrect and the desired interaction is likely a merge of the data types instead.
This likely means augmenting how we declare properties to allow users to specify this and updating the merge with model code.
This might not be available on all property types so we should clearly document when and where this option can be used.
I don't know if its plank fault or homebrew but I can't update to version 1.3
14:19 $ brew install plank
Updating Homebrew...
Error: plank 1.2 is already installed
To upgrade to 1.3, run brew upgrade plank
✘-1 ~
14:24 $ brew upgrade plank
Error: pinterest/tap/plank 1.2 already installed
Currently we define a description and default value for each entry in the string enum specification. The entries default value should support "null" in order to allow the caller to specify the overall default enum value.
Hi!
Just starting to use Plank, looks awesome!
Small question: just wondering, what is the reason that NSSet
is not included as one of the Schema’s Property Types?
In the Buffer API for profiles we have both a service and a service_type. When we add these to our scheme as String Enums we only get BFRProfileServiceType outputted as all enums get the suffix 'Type'.
From our side I think we'd be happy with BFRProfileService as the name and have 'Type' dropped which would resolve the conflict.
plank/Sources/Core/ObjectiveCIR.swift
Line 78 in 8d5edd7
There are a number of minor errors that need to be resolved for compatibility but since 3.1 is GA we should migrate to the latest version and update our .swiftenv configuration
This generates the error: https://github.com/pinterest/plank/blob/master/Makefile#L25
brew upgrade pinterest/tap/plank
==> Upgrading 1 outdated package, with result:
pinterest/tap/plank 1.1
==> Upgrading pinterest/tap/plank
==> Cloning https://github.com/pinterest/plank.git
Updating /Users/Leo-KiddoLabs/Library/Caches/Homebrew/plank--git
==> Checking out tag v1.1
==> unset CC; make archive
Last 15 lines from /Users/Leo-KiddoLabs/Library/Logs/Homebrew/plank/01.unset:
2017-09-25 14:16:47 -0300
unset CC; make archive
xcrun swift build -c release -Xswiftc -static-stdlib
error: manifest parse error(s):
sandbox-exec: sandbox_apply_container: Operation not permitted
make: *** [archive] Error 1
Do not report this issue to Homebrew/brew or Homebrew/core!
Error: You are using macOS 10.13.
We do not provide support for this pre-release version.
You may encounter build failures or other breakages.
Please create pull-requests instead of filing issues.
Error: You are using macOS 10.13.
We do not provide support for this pre-release version.
You may encounter build failures or other breakages.
Please create pull-requests instead of filing issues.
EDIT: A simpler solution would be to investigate fixes for the slowness that caused us to want a cache in the first place (i.e. reduce unnecessary writes)
Right now Pinterest's Bazel setup for plank makes two targets. One for headers and one for sources generated from plank.
We do this because invoking plank with many schemas at once speeds up the generation of the sources by an order of magnitude (due to the way we cache and reuse schemas we parse).
Since we treat all pieces as one target, we have to recompile every single generated model when one of them changes (this is the slow part).
If there were some way to dump cache state to a file and read it from a file, then we could invoke plank in several build targets with several artifacts that are hashed and cached separately.
It's important that this "shared cache file" (maybe it's an mmap-ed hashtable) be thread safe since several plank invocations could run concurrently.
This change would save around 30s per incremental build on a plank change
We should have documentation and examples available on https://pinterest.github.io/plank/
These should be similar to the information we have currently for Objective-C.
Incremental builds are suffering since we need to call plank on all schemas whenever touching just one of them. Recreating any models invalidates all of them during our build.
There may be workarounds, but it would be simpler if we could afford to invoke plank once per schema (w.r.t. wall-clock time).
We get the below error because it is present in Foundation APIs
error: multiple methods named 'dictionaryRepresentation' found [-Werror,-Wstrict-selector-match]
514 [result0 addObject:[obj0 dictionaryRepresentation]];
515 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
516 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.0.sdk/System/Librar y/Frameworks/Foundation.framework/Headers/NSMapTable.h:68:1: note: using
517 - (NSDictionary<KeyType, ObjectType> *)dictionaryRepresentation; // create a dictionary of contents
518 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
519 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.0.sdk/System/Librar y/Frameworks/Foundation.framework/Headers/NSUserDefaults.h:146:1: note: also found
520 - (NSDictionary<NSString *, id> *)dictionaryRepresentation;
521 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
522 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.0.sdk/System/Librar y/Frameworks/Foundation.framework/Headers/NSUbiquitousKeyValueStore.h:48:58: note: also found
523 @property (readonly, copy) NSDictionary<NSString *, id> *dictionaryRepresentation;
.....
I believe we need another entry under Array
showing how you can now generate a Set after @rmls PR merged.
https://pinterest.github.io/plank/docs/json-reference/property-fields.html
Currently we always reference the type
property by convention but we should make this explicit
Let's say I have a set of N models, and they all have optional array
s that all can contain the same types of models. Right now I have to duplicate all of the oneOf
declarations in each schema, and duplicate the match
ing code for each of the N models.
I wish there was a way to group multiple models for ADT, then we can also keep all the logic for serializing/match
ing these models in one place to remove the duped code.
I'd like to use plank
for generating models for use inside an app i.e. no JSON parsing, and it'd be great to be able to specify @property id myThing
and @property UIColor *myColor
.
Is anyone else interesting in this capability?
Right now isEqual:
has && self == otherObject &&
so we absolutely need to remove that.
We are planning to add Flow type support to plank and and here are some initial thoughts around it.
The initial idea was to provide a more extended JS integration with immutable models backed by Immutable.js. A partial implementation was already on a side branch too, but as Immutable.js is pretty hefty in size as well as would introduce a certain convention to use Immutable.js up front, we decided to take a step back for the first pass and start with adding Flow support to plank.
A pull request that implements this issue as currently described is ready: #57
For a first example what the following PDT definition of an extensive representation of a Pin type is provided as well as the created Flow type definition below.
{
"id": "pin.json",
"title": "pin",
"description" : "Schema definition of Pinterest Pin",
"$schema": "http://json-schema.org/schema#",
"type": "object",
"properties": {
"id" : { "type": "string" },
"link" : {
"type": "string",
"format": "uri"
},
"url" : {
"type": "string",
"format": "uri"
},
"creator": {
"type": "object",
"additionalProperties": { "$ref": "user.json" }
},
"board": { "$ref": "board.json" },
"created_at" : {
"type": "string",
"format": "date-time"
},
"note" : { "type": "string" },
"color" : { "type": "string" },
"counts": {
"type": "object",
"additionalProperties": { "type": "integer" }
},
"media": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"attribution": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"description" : { "type": "string" },
"image": { "$ref": "image.json" }
},
"required": []
}
import type { PlankDate, PlankURI } from "./runtime.flow.js";
import type BoardType from "./BoardType.js";
import type ImageType from "./ImageType.js";
import type UserType from "./UserType.js";
export type PinType = $Shape<{|
+note?: string | null,
+media?: { [string]: string } | null,
+counts?: { [string]: number } /* Integer */ | null,
+descriptionText?: string | null,
+creator?: { [string]: UserType } | null,
+attribution?: { [string]: string } | null,
+board?: BoardType | null,
+color?: string | null,
+link?: PlankDate | null,
+identifier?: string | null,
+image?: ImageType | null,
+createdAt?: PlankDate | null,
+url?: PlankDate | null,
|}> & {
id: string
};
Let's pick out some interesting pieces from the example above and provide bit more details.
For every plank type an exported Flow type alias with the name TitleType
will be created.
Currently all properties are defined as covariant (read-only), declared by the plus symbol in front of the property name.
As it's currently not possible to know for sure, that properties are included within the API response we declare the properties as optional. Furthermore, if the property is included in the API response we don't know for sure if a valid value or null was received. Therefore the type definition of a property is always declared as optional.
For types like integer or strings, equivalent primitive types like number and string are used.
For object types that act like a map, like thecounts
property from above, we use a special kind of property, called an "indexer property".
For specific format types, we are providing pre-defined types which are defined in a specific runtime file. In case of the date
and uri
type the representation is just a string for now:
...
export type PlankDate = string;
export type PlankURI = string;
...
If references to other types are defined within the PDT, the referenced type will be imported and the property will be annotated with the reference type.
We also have support for enums and ADTs, which are not present in the example above. Examples for both of them would look like the following:
...
"attribution": {
"oneOf": [
{ "$ref": "image.json" },
{ "$ref": "board.json" }
]
},
...
...
export type PinAttributionType = ImageType | BoardType;
...
export type PinType = $Shape<{|
+attribution?: PinAttributionType | null,
...
|}> ...
...
"status" : {
"type": "string",
"enum": [
{ "default" : "unknown", "description" : "unknown" },
{ "default" : "new", "description" : "new" },
{ "default" : "accepted", "description" : "accepted" },
{ "default" : "denied", "description" : "denied" },
{ "default" : "pending_approval", "description" : "pending_approval" },
{ "default" : "contact_request_not_approved", "description": "contact_request_not_approved" }
],
"default" : "unknown"
},
"availability" : {
"type": "integer",
"enum": [
{ "default" : 1, "description" : "in_stock" },
{ "default" : 2, "description" : "out_of_stock" },
{ "default" : 3, "description" : "preorder" },
{ "default" : 4, "description" : "unavailable" }
]
},
...
export type PinStatusType =
| "unknown"
| "new"
| "accepted"
| "denied"
| "pending_approval"
| "contact_request_not_approved";
export type PinAvailabilityType =
| 1 /* in_stock */
| 2 /* out_of_stock */
| 3 /* preorder */
| 4; /* unavailable */
...
export type PinType = $Shape<{|
+status?: PinStatusType | null,
+availability?: PinAvailabilityType | null,
...
|}> ...
As we would like to provide the most optimal integration that would suit most of the needs, with this issue we would like to start gathering feedback from the community about our current approach and were we should heading to.
@@ -83,10 +83,15 @@ struct ObjCRootsRenderer {
switch schema {
case .Array(itemType: .none):
return "NSArray *"
case .Array(itemType: .some(let itemType)) where itemType.isObjCPrimitiveType:
This is probably not worth changing, but -- if it's possible to represent something as a pure pattern match without guards, it's faster because then the switch can compile into a jump table (I don't know if Swift takes advantage of this optimization though).
The way to do that in this case would be to change the way we parse the JSON schema for the Array and Map cases and change itemType to something like:
enum CollectionElement {
case Unknown
case ObjcClass(Schema)
case Primitive(name: String)
}
instead of what is something like Schema?And then you can do a deeper pattern match on those cases to avoid the where clause
In my current application I need to mutate some of my models eventually, but I'm not entirely sure if I'm doing it correctly, two examples:
Remove a single object from an array inside my object:
self.object = [self.object copyWithBlock:^(THFeedBuilder * _Nonnull builder) {
NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:self.object.array];
[mutableArray removeObject:item];
builder.array = mutableArray;
}];
Change a bool value from a complex model:
self.object = [self.object copyWithBlock:^(THPostBuilder * _Nonnull builder) {
builder.object2 = [builder.object2 copyWithBlock:^(THPostpictureobjectBuilder * _Nonnull builder) {
builder.object3 = [builder.object3 copyWithBlock:^(THPostpictureBuilder * _Nonnull builder) {
builder.bool = YES;
}];
}];
}];
Also I'm doing self.object = [self.object copy...
instead of creating a new object because I need to modify a class property.
We recently had a crash where the type information validation could have prevented a serious high volume crash. while this shouldn't happen in practice it might be valuable to validate in-line in initWithDictionary
or having a separate validation method entirely.
Hi, I tried parsing a string with a data to a NSDate object but it didn't work, my property always ended being nil. I believe its probably a mistake on my part, could you provide an example of it?
I did it as suggested below.
Date Parsing: Due to the variance of possible date formats, NSDate or DateTime objects are created using an instance of NSValueTransformer. It is up to the host application to register an instance of NSValueTransformer for the key kPINModelDateValueTransformerKey.
Thank you for this cool lib and immutable models concept.
The generated PinBuilder is as following.
@interface PinBuilder : NSObject
@property (nullable, nonatomic, copy, readwrite) NSString * identifier;
but the PinBuilder setter implementation didn't reflect the copy
attribute at property.
- (void)setIdentifier:(NSString *)identifier
{
_identifier = identifier;
_pinDirtyProperties.PinDirtyPropertyIdentifier = 1;
}
Example schema
{
"id": "pin.json",
"title": "pin",
"description" : "Schema definition of a Pin",
"$schema": "http://json-schema.org/schema#",
"type": "object",
"properties": {
"id": { "type": "string" },
"link": { "type": "string", "format": "uri"}
}
}
This is similar to #71
Is there possibility to serialise generated object to JSON? We can do initWithDictionary, but what about to have something like dictionary
method, which will return dictionary to be send as JSON via REST?
We have a boolean called default from one of our API responses, this doesn't seem to get renamed in the same way as identifier or description_text does.
Confirmed that default is included in objectiveCReservedWords.
Not sure if I'm missing a trick to rename etc.
Say I'm interfacing with an API that has an img_url
key. If I'm understanding everything correctly, plank would incorporate that as imgUrl
. It'd be nice to be able to specify it as imageURL
to be more inline with Objective-C convention (I don't have server-side control over this API, so I can't change it there).
Or is there a goal to keep it absolutely 1:1 with the server-side spec?
If I have a schema with the following properties:
{
"action_type": {
"type": "integer",
"enum": [
{ "default": 1, "description": "foobar" }
]
}
}
And a model is attempting to be generated with the following dictionary:
{
"action_type": 2
}
(i.e. one where the value that is attempting to be generated is not contained within the values the properties define)
I would expect something to be logged and for the model to not be generated.
Hi! I have a similar project and am currently working on Objective-C support. Check it out: https://github.com/quicktype/quicktype
We have automated tests, JSON inference in addition to Schema, support for GraphQL queries, an Xcode extension in the App Store, a cross-platform CLI, a web app, and support for 8 output languages.
I would love to find a way to collaborate on at least Objective-C and Swift if you're interested. We use the same license as you, but are implemented in TypeScript.
Is it possible to modify the same model in different instances of different views?
For example, I have an user model in the profile view and in the home view. If I change the model in the profile view it should also change in home screen.
This gif shows what I'm trying to achieve using Facebook like button state as example.
NSArray<NSNumber *> * renders in the .h file as expected but the .m file complains about trying to assign a primitive to an id.
Releases should go out weekly assuming there have been changes and all CI measures have passed
We should add tests for our generated models to make changes way more safer and see immediately if we break something.
We should look into adding the tests to the Example/
folder and we could swift test
for that project.
I ran into the issue in #67 as I wanted to be sure that the model dictionary is the same as calling dictionaryRepresentation
on it. The tests could look like something like that:
NSDictionary *imageModelDictionary = @{
@"height" : @(12),
@"width" : @(11),
@"url" : @"http://google.com"
};
Image *image = [[Image alloc] initWithModelDictionary:imageModelDictionary];
XCTAssert([imageModelDictionary isEqualToDictionary:[image dictionaryRepresentation]], @"Should be the same");
NSDictionary *userModelDictionary = @{
@"id" : @(123),
@"first_name" : @"Michael",
@"last_name" : @"Schneider",
@"image" : imageModelDictionary,
};
User *user = [[User alloc] initWithModelDictionary:userModelDictionary];
XCTAssert([userModelDictionary isEqualToDictionary:[user dictionaryRepresentation]], @"Should be the same");
var imageModelDictionary: [AnyHashable: Any] = ["height": (12), "width": (11), "url": "http://google.com"]
var image = Image(modelDictionary: imageModelDictionary)
XCTAssert(imageModelDictionary.isEqual(to: image.dictionaryRepresentation), "Image dictionary representation should be the same as the model dictionary.")
var userModelDictionary: [AnyHashable: Any] = ["id": (123), "first_name": "Michael", "last_name": "Schneider", "image": imageModelDictionary]
var user = User(modelDictionary: userModelDictionary)
XCTAssert(userModelDictionary.isEqual(to: user.dictionaryRepresentation), "User dictionary representation should be the same as the model dictionary")
My intuition was to use the field "description" on an enum value to describe the purpose of the value. However, "description" is used to generate the enum name. It seems like it might make more sense to decouple name and description.
Setting a description like this:
"enum": [
{ "default" : "unknown", "description" : "Default value. This is not a value that the API will return."},
Generates this output which doesn't compile:
typedef NS_ENUM(NSInteger, PIPinSafetyStatusRedirectStatusType) {
PIPinSafetyStatusRedirectStatusTypeDefault Value. This Is Not A Value That The Api Will Return. /* unknown */,
Hi!
Just going over the tutorial, and had a question. In the initWithModelDictionary:
method which created, if a property is defined as copy
, doesn’t the copy method should be called on the variable when initialized?
For example, in the tutorial, the user object has a firstName
property, which is defined as copy
; but the relevant line in the aforementioned method is self->_username = value;
. Doesn’t it need to be self->_username = [value copy];
?
These should all be the same but can technically be different.
It would be nice for Builders and objects both to conform to NSCopying
and NSMutableCopying
. -copy
would return the corresponding model type, and -mutableCopy
would return the corresponding Builder type. Could allow for a ton of flexibility!
This method was added but has some schema combinations that actually have preventing it from compiling. The scenario I found that errored was a property of type Map<String,Array<String>>
We should also add a integration test case that is exhaustive of all schema types to prevent this from happening
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.